์ฐธ๊ณ
QueryDSL ํ๋ก์ ํธ ์ ์ฉ ํ๊ธฐ ๋ฐ ํธ๋ฌ๋ธ์ํ
plugins {
...
// QueryDsl
id "com.ewerk.gradle.plugins.querydsl" version "1.0.10"
}
dependencies {
...
// QueryDsl
implementation "com.querydsl:querydsl-jpa:5.0.0"
annotationProcessor "com.querydsl:querydsl-apt:5.0.0"
}
// script for querydsl
def querydslDir = "$buildDir/generated/querydsl"
querydsl {
jpa = true
querydslSourcesDir = querydslDir
}
sourceSets {
main.java.srcDir querydslDir
}
configurations {
querydsl.extendsFrom compileClasspath
}
compileQuerydsl {
options.annotationProcessorPath = configurations.querydsl
}
@Configuration
public class QueryDslConfig {
@PersistenceContext
private EntityManager em;
@Bean
public JPAQueryFactory query(){
return new JPAQueryFactory(em);
}
}
@Entity
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@EntityListeners(AuditingEntityListener.class)
public class Board {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, length = 30)
private String title;
@Column(nullable = false, length = 3000)
private String content;
@Column(nullable = false)
private String writer;
@Column(nullable = true, columnDefinition = "bigint default 0")
private long heart;
@LastModifiedDate
private LocalDateTime modifiedAt;
@CreatedDate
private LocalDateTime createdAt;
public static Board createBoard(PostBoard request){
return new Board(null, request.getTitle(), request.getContent(), Naming.getName(), 0L, null, null);
}
public void addHeart(){
this.heart++;
}
}
@Entity
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@EntityListeners(AuditingEntityListener.class)
public class Comment {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private Long memberId;
@ManyToOne
@JoinColumn(name = "board_id")
private Board board;
@Column(nullable = false, length = 100)
private String comment;
@LastModifiedDate
private LocalDateTime modifiedAt;
@CreatedDate
private LocalDateTime createdAt;
}
Board
์ Comment
๋ฅผ QueryDsl์ ํตํด ๋ฟ๋ ค๋ณด๋ ค๊ณ ํ๋ค.
gradlew compileQuerydsl
๋ฅผ ์ ๋ ฅํด์ฃผ๋ฉด
์๋์ผ๋ก gradle์ ํตํด Qclass๋ฅผ ์์ฑํด์ค๋ค.
์์ ์ค์ ์ ๊ทธ๋๋ก ๋ฐ๋ผํ๋ค๋ฉด ๋ค์ ์์น์์ Qclass๋ค์ ํ์ธํด๋ณผ ์ ์๋ค.
public interface BoardRepositoryCustom {
Page<BoardDto> findBoardPage(Pageable pageable);
}
@Repository
@RequiredArgsConstructor
public class BoardRepositoryImpl implements BoardRepositoryCustom{
private final QueryDslConfig qd;
@Override
public Page<BoardDto> findBoardPage(Pageable pageable) {
List<BoardDto> content = qd.query()
.select(Projections.constructor(BoardDto.class,
board.boardId,
board.title,
board.content,
board.writer,
ExpressionUtils.as(
JPAExpressions.select(comment1.board.count())
.from(comment1)
.where(comment1.board.boardId.eq(board.boardId)),"commentCount"
),
board.heart,
board.modifiedAt,
board.createdAt
))
.from(board)
.orderBy(board.boardId.desc())
.offset(pageable.getOffset())
.limit(pageable.getPageSize())
.fetch();
for(BoardDto dto : content){
List<Comment> comments = qd.query()
.selectFrom(comment1)
.orderBy(comment1.commentId.desc())
.limit(5L)
.where(comment1.board.boardId.eq(dto.getBoardId()))
.fetch();
dto.setComments(comments);
}
Long total = qd.query()
.from(board)
.stream().count();
return new PageImpl<>(content, pageable, total);
}
}
QueryDsl์ ์ ์ฉํ์ฌ ์ฌ์ฉํ BoardRepositoryCustom
์ธํฐํ์ด์ค๋ฅผ ๋ง๋ค๊ณ ๊ตฌํ์ฒด๋ฅผ ์ ์ํด์ฃผ์๋ค.
JPA๋ณด๋ค QueryDsl์ ์ฌ์ฉํด๋ณด๋ ์ ๋ง๋ก ์ฟผ๋ฆฌ๋ฅผ ์ง ๋ค๊ธฐ ๋ณด๋ค๋ java ๋ฌธ๋ฒ์ผ๋ก ๊ฐ์ฒด๋ฅผ ๋ง๋ค์ด๋ด๋ ๋๋์ด ๋์ ๋ ์ฌ๋ฐ์๋ค.
๊ทธ๋ฆฌ๊ณ ๋ฐํํ ๋ Entity๋ฅผ ๊ทธ๋๋ก ๋ฐํํ์ง ์๋ ๊ฒ์ด ์ข๋ค. ๋์ฒด๋ก๋ Tuple๊ณผ Dto ์์ฑ์ด ์๋๋ฐ. Tuple์ ๋ง์น Map์ ์ฌ์ฉํ์ฌ ๊ฐ์ฒด๋ฅผ ์ ๋ฌํ๋ ๊ธฐ๋ถ์ด๋ผ Dto๋ฅผ ๋ฐ๋ก ์ ์ํด์ ๋ฐํํ๋๋ก ํ๋ค.
๋ ์ฌ๊ธฐ์ QueryDsl์์ ์ ์ํ๋ @QueryProjection
์ด๋
ธํ
์ด์
์ Entity๋ Dto ์์ฑ์์ ๋ถ์ฌ์ฃผ๋ฉด ๋๋๋ฐ. ์ด๊ฒ ๋ํ ๋๋ฌด QueryDsl ์์กด๋๊ฐ ๋์์ง๋๊ฑฐ ๊ฐ์์ ๊ทธ๋ฅ ๋ด๊ฐ ๋ฐ๋ก ์ ์ํ Dto ๊ฐ์ฒด์ ๊ฐ์ ๋ฃ์ด์ฃผ๋ ๊ฒ์ผ๋ก ์ ์ํ๋ค.
๊ทธ๋ฆฌ๊ณ Projections.constructor('์์ฑํ ๊ฐ์ฒด class', ์์ฑ์ ๋งค๊ฐ๋ณ์...)
์ ์ฌ์ฉํ์ฌ ๋ฐํํ ํ์
์ ์ ์ํ๊ณ ExpressionUtils.as('์ฟผ๋ฆฌ', 'alias')
๋ฅผ ์ฌ์ฉํ์ฌ ๋ณ์นญ์ ์ ์ํด์ฃผ์๋ค.
์ฟผ๋ฆฌ ๋ถ๋ถ์๋ JPAExpressions.select()
๋ฅผ ์ฌ์ฉํ์ฌ ์๋ธ์ฟผ๋ฆฌ๋ฅผ ์ถ๊ฐํด์ฃผ์ด ๋ค๋ฅธ ํ
์ด๋ธ์ ๋ฐ์ดํฐ๋ฅผ ์กฐํํด์ฌ ์ ์๋ค.
for(BoardDto dto : content){...}
๊ทธ๋ฆฌ๊ณ ํด๋น ๋ถ๋ถ์ ํตํด list ํํ์ ๋๊ธ ํ
์ด๋ธ์ ๋ฐ์ดํฐ๋ฅผ ์กฐํํด์ ๋ฃ์ด์ฃผ์๋ค. ์ด๋ถ๋ถ์ ๋์ค์ 5๋ฒ ์กฐํ๊ฐ ์๋ 1๋ฒ ์กฐํ๋ก ์ค์ผ ์ ์์๊ฑฐ ๊ฐ๋ค. ๋ฆฌํฉํ ๋ง ๋์์ผ๋ก ์ ํด๋๊ณ ์์ ํ๋ ๊ธ์ ๋ค์์ ์ฌ๋ ค์ผ๊ฒ ๋ค.
public interface BoardRepository extends JpaRepository<Board, Long>, BoardRepositoryCustom {
}
๊ธฐ๋ณธ์ ์ธ JPA์ ๋ฉ์๋๋ค์ ์ฌ์ฉํ๊ธฐ ์ํด ์ ์ํด์ค๋ค. ๊ทธ๋ฆฌ๊ณ BoardRepositoryCustom
์ฐ๋ฆฌ๊ฐ ์ ์ํ Repository๋ฅผ ์ถ๊ฐ๋ก ์ ์ฉํด์ฃผ์ด Service์์ ํ๋์ Repository๋ฅผ ์ฃผ์
ํ์ฌ ์ฌ์ฉํ ์ ์๊ฒ ํ์.
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
@Slf4j
public class BoardServiceImpl implements BoardService{
private final BoardRepository boardRepository;
@Override
public GetBoardResponse getBoard(Pageable page) {
Page<BoardDto> findAll = boardRepository.findBoardPage(page);
return GetBoardResponse.builder()
.last(findAll.isLast())
.empty(findAll.isEmpty())
.totalPage(findAll.getTotalPages())
.totalElements(findAll.getTotalElements())
.boardList(findAll.getContent())
.numberOfElements(findAll.getNumberOfElements())
.build();
}
}
๊ทธ๋ฆฌ๊ณ ํ ์คํธ๋ฅผ ์์ฑํด์ ํ์ธํด๋ณด๋ฉด
@SpringBootTest
@Execution(ExecutionMode.SAME_THREAD)
class BoardServiceImplTest {
@Autowired
private BoardService boardService;
@Test
@DisplayName("๊ฒ์๊ธ ํ์ด์ง ์ฑ๊ณตํ๋ค.")
void getBoardSuccess() throws Exception {
//given
for(int i=1; i<=20; i++){
PostBoard postBoard = new PostBoard("์ ๋ชฉ" + i, "๋ด์ฉ" + i);
boardRepository.save(Board.createBoard(postBoard));
}
Pageable pageable = Pageable.ofSize(5);
pageable = pageable.next();
pageable = pageable.next();
//when
GetBoardResponse board = boardService.getBoard(pageable);
//then
Stream<String> title = board.getBoardList().stream().filter(b -> b.getContent().equals("๋ด์ฉ10")).map(b -> b.getTitle());
assertTrue(title.count() == 1);
}
}
์ ์์ ์ผ๋ก ํ์ด์งํ ๋ด์ฉ์ ๊ฐ์ ธ์ค๋ ๊ฒ์ ํ์ธํ ์ ์๋ค!
์ ์ฟผ๋ฆฌ๋ก ์คํํ๋ฉด
๋ค์๊ณผ ๊ฐ์ด ํ๋์ ํ์ด์ง ์์ฒญ๋น 5๊ฐ์ ๋๊ธ ์ฐพ๋ ์ฟผ๋ฆฌ๊ฐ ๋ฐ์ํ๋ค. ํด๋น ์ฟผ๋ฆฌ๊ฐ ๋ง์ฝ ๋๊ธ์ 10๊ฐ ์ฐพ๋ ์ฟผ๋ฆฌ๋ผ๋ฉด 10๊ฐ์ ์ฟผ๋ฆฌ๊ฐ ์ถ๊ฐ๋ก ๋๊ฐ๋ ๊ฒ์ด๋ค. ์ด๊ฒ์ด N+1 ๋ฌธ์ ์ธ๋ฐ QueryDsl๋ก ๋ณ๊ฒฝํ ์๋ฏธ๊ฐ ์๋ค. ๊ทธ๋์ ์ต๋ํ ์ค์ฌ๋ณด๋ ค๊ณ ํ๋ค.
@Repository
@RequiredArgsConstructor
public class BoardRepositoryImpl implements BoardRepositoryCustom{
private final QueryDslConfig qd;
@Override
public Page<BoardDto> findBoardPage(Pageable pageable) {
List<BoardDto> content = qd.query()
.select(Projections.constructor(BoardDto.class,
board.boardId,
board.title,
board.content,
board.writer,
ExpressionUtils.as(
JPAExpressions.select(comment1.board.count())
.from(comment1)
.where(comment1.board.boardId.eq(board.boardId)),"commentCount"
),
board.heart,
board.modifiedAt,
board.createdAt
))
.from(board)
.orderBy(board.boardId.desc())
.offset(pageable.getOffset())
.limit(pageable.getPageSize())
.fetch();
ArrayList<Long> boardIdList = new ArrayList<>();
for(BoardDto dto : content){
boardIdList.add(dto.getBoardId());
}
List<CommentDto> findComment = qd.query().select(Projections.constructor(CommentDto.class,
comment1.commentId,
comment1.memberId,
comment1.board.boardId,
comment1.comment,
comment1.modifiedAt,
comment1.createdAt
))
.from(comment1)
.where(comment1.board.boardId.in(boardIdList))
.fetch();
for(BoardDto dto : content){
Stream<CommentDto> commentStream = findComment.stream().filter(c -> c.getBoardId() == dto.getBoardId());
List<CommentDto> comments = commentStream.collect(Collectors.toList());
dto.setComments(comments);
}
Long total = qd.query()
.from(board)
.stream().count();
return new PageImpl<>(content, pageable, total);
}
}
์ฟผ๋ฆฌ๊ฐ board๋ฅผ ์ต์ด๋ก select ํ๋ ์ฟผ๋ฆฌ์ boardId๋ฅผ ๊ธฐ์ค์ผ๋ก Comment๋ฅผ selectํ๋ ์ฟผ๋ฆฌ 2๊ฐ๋ก ์ค์๋ค. ์ด ์ฟผ๋ฆฌ๋ ๋๊ธ์ ๋ช๊ฐ๋ฅผ ์ฐพ์๋ ๋ณ๊ฒฝ๋์ง ์์ผ๋ฏ๋ก ์ข๋ ๊ฐ์ ํ๋ค๊ณ ํ ์ ์๋ค!
@Test
@DisplayName("๊ฒ์๊ธ ํ์ด์ง ํ ๋๊ธ์ ๋ถ๋ฌ์ค๋๋ฐ ์ฑ๊ณตํ๋ค.")
void getBoardSuccess2() throws Exception {
//given
for(int i=1; i<=20; i++){
PostBoard postBoard = new PostBoard("์ ๋ชฉ" + i, "๋ด์ฉ" + i);
Board saveBoard = boardRepository.save(Board.of(postBoard));
for(int j=1; j<=3; j++){
commentRepository.save(Comment.of(0L, "๋๊ธ"+j, saveBoard));
}
}
Pageable pageable = Pageable.ofSize(5);
pageable = pageable.next();
pageable = pageable.next();
//when
GetBoardResponse board = boardService.getBoard(pageable);
}
ํ ์คํธ ์ฝ๋๋ ๋ค์๊ณผ ๊ฐ์ด ์์ฑํด์ ํ ์คํธ๋ฅผ ๋๋ ค๋ณด๋ฉด
๋ฐ์ดํฐ๋ ๋ด๊ฐ ์๊ฐํ๋๋ก ์ ๋์ค๋ ๊ฒ์ ํ์ธํ ์ ์๋ค.