[Query DSL] 스프링 데이터 페이징 활용

컴공생의 코딩 일기·2023년 2월 8일
0

Query DSL

목록 보기
5/6
post-thumbnail

Query DSL 페이징 연동

fetchResults(), fetchCount()는 Querydsl 5.0 부터 deprecated 되었다. fetchResults(), fetchCount()는 둘 다 Querydsl 내부에서 count 쿼리를 만들어서 실행해야 하는데, 이때 작성한 select 쿼리를 기반으로 count 쿼리를 만들어낸다. 그런데 이 기능이 select 구문을 단순히 count 처리하는 것으로 바꾸는 정도여서, 단순히 쿼리에서는 잘 동작하지만, 복잡한 쿼리에서는 잘 동작하지 않는다. 그렇기 때문에 count 쿼리를 별도로 작성하고, fetch()를 사용해서 해결해야 한다.

fetch()를 사용한 count 쿼리 대안 예제

Page, Pageable 활용

사용자 정의 인터페이스에 페이징 정의

public interface MemberRepositoryCustom {
    List<MemberTeamDto> search(MemberSearchCondition condition);
}

사용자 정의 클래스에 인터페이스에 정의한 페이징 구현

public class MemberRepositoryImpl implements MemberRepositoryCustom {

  private final JPAQueryFactory queryFactory;

  public MemberRepositoryImpl(JPAQueryFactory queryFactory) {
      this.queryFactory = queryFactory;
   }
    @Override
    public Page<MemberTeamDto> searchPage(MemberSearchCondition condition, Pageable pageable) {
       List<MemberTeamDto> content =  queryFactory
                .select(new QMemberTeamDto(
                        member.id.as("memberId"),
                        member.username,
                        member.age,
                        team.id.as("teamId"),
                        team.name.as("teamName")))
                .from(member)
                .leftJoin(member.team, team)
                .where(
                        usernameEq(condition.getUsername()),
                        teamNameEq(condition.getTeamName()),
                        ageGoe(condition.getAgeGoe()),
                        ageLoe(condition.getAgeLoe())
                )
                .offset(pageable.getOffset())
                .limit(pageable.getPageSize())
                .fetch();

        JPAQuery<Long> countQuery = queryFactory
                .select(member.count())
                .from(member)
                //.leftJoin(member.team, team) count 최적화 
                .where(
                        usernameEq(condition.getUsername()),
                        teamNameEq(condition.getTeamName()),
                        ageGoe(condition.getAgeGoe()),
                        ageLoe(condition.getAgeLoe())
                );

        return new PageImpl<>(content,pageable,totalCount);
    }
}

fetchResults()fetchCount()deprecated 되었기 때문에 content를 가져올려면 fetch()로 가져오고 Count는 따로 Count 쿼리로 구현해서 가져오면 된다.

PageImpl : PageImplPage 인터페이스의 구현체이다. PageImpl에 첫번째 인자로 content(조회된 컨펜츠), Pageable요청으로부터 가져온 페이지 요청 데이터), totalCount(전체 컨텐츠의 개수)를 주면 된다.

테스트 코드로 결과 확인

@Test
    void searchPageTest(){
        Team teamA = new Team("teamA");
        Team teamB = new Team("teamB");
        em.persist(teamA);
        em.persist(teamB);

        Member member1 = new Member("member1", 10, teamA);
        Member member2 = new Member("member2", 20, teamA);
        Member member3 = new Member("member3", 30, teamB);
        Member member4 = new Member("member4", 40, teamB);
        em.persist(member1);
        em.persist(member2);
        em.persist(member3);
        em.persist(member4);
        //초기화
        em.flush();
        em.clear();

        MemberSearchCondition condition = new MemberSearchCondition();

        PageRequest pageRequest = PageRequest.of(0, 3);


        Page<MemberTeamDto> result = memberRepository.searchPage(condition, pageRequest);
        assertThat(result.getContent()).extracting("username")
                .containsExactly("member1", "member2", "member3");
    }

CountQuery 최적화 방법 (PageableExecutionUtils.getPage()로 최적화)

    @Override
    public Page<MemberTeamDto> searchPage(MemberSearchCondition condition, Pageable pageable) {
       List<MemberTeamDto> content =  queryFactory
                .select(new QMemberTeamDto(
                        member.id.as("memberId"),
                        member.username,
                        member.age,
                        team.id.as("teamId"),
                        team.name.as("teamName")))
                .from(member)
                .leftJoin(member.team, team)
                .where(
                        usernameEq(condition.getUsername()),
                        teamNameEq(condition.getTeamName()),
                        ageGoe(condition.getAgeGoe()),
                        ageLoe(condition.getAgeLoe())
                )
                .offset(pageable.getOffset())
                .limit(pageable.getPageSize())
                .fetch();

        JPAQuery<Long> countQuery = queryFactory
                .select(member.count())
                .from(member)
                //.leftJoin(member.team, team)
                .where(
                        usernameEq(condition.getUsername()),
                        teamNameEq(condition.getTeamName()),
                        ageGoe(condition.getAgeGoe()),
                        ageLoe(condition.getAgeLoe())
                );

        return PageableExecutionUtils.getPage(content, pageable, countQuery::fetchOne);
        //return new PageImpl<>(content,pageable,totalCount);

    }

PageableExecutionUtils.getPage() : PageableExecutionUtils.getPage()PageImpl과 같은 역할을 하지만 한 가지 추가된 점은 마지막 인자로 함수를 전달하는데 내부 작동에 의해서 totalCount가 페이지 사이즈 보다 적거나, 마지막 페이지 일 경우 해당 함수를 실행하지 않는다. 즉 쿼리를 조금 더 줄일 수 있다.

전체 데이터를 100으로 설정하고 포스트맨을 이용해 페이지의 사이즈를 50으로 주고 실행하면 다음과 같은 쿼리를 볼 수 있다.

/* select
        member1.id as memberId,
        member1.username,
        member1.age,
        team.id as teamId,
        team.name as teamName 
    from
        Member member1   
    left join
        member1.team as team */ select
            member0_.member_id as col_0_0_,
            member0_.username as col_1_0_,
            member0_.age as col_2_0_,
            team1_.team_id as col_3_0_,
            team1_.name as col_4_0_ 
        from
            member member0_ 
        left outer join
            team team1_ 
                on member0_.team_id=team1_.team_id limit ?
=============================================================================
/* select
        count(member1) 
    from
        Member member1 */ select
            count(member0_.member_id) as col_0_0_ 
        from
            member member0_

select 쿼리와 count 쿼리가 각각 한 번씩 나가는 걸 볼 수 있다.

만약 페이즈의 사이즈를 200으로 설정할 경우 전체 데이터 수 보다 크기 때문에 PageableExecutionUtils.getPage() 의 내부 기능으로 인해count 쿼리를 실행시키지 않는다.

/* select
        member1.id as memberId,
        member1.username,
        member1.age,
        team.id as teamId,
        team.name as teamName 
    from
        Member member1   
    left join
        member1.team as team */ select
            member0_.member_id as col_0_0_,
            member0_.username as col_1_0_,
            member0_.age as col_2_0_,
            team1_.team_id as col_3_0_,
            team1_.name as col_4_0_ 
        from
            member member0_ 
        left outer join
            team team1_ 
                on member0_.team_id=team1_.team_id limit ?

조금이라도 최적화를 위해서는 PageImpl보다 PageableExecutionUtils.getPage() 을 사용하는게 좋을거 같다.

QueryDSL 동적 정렬하기

QueryDSL에서 동적으로 정렬하려면 OrderSpecifier 객체를 사용하면 된다.

OrderSpecifier 객체에 인자로는 SortOrderExpression 객체, Null값을 핸들링하기 위한 Enum 값을 받는다.

public Page<Board> searchBoard(String word, Pageable pageable) {
        JPAQuery<Board> query = queryFactory
                .selectFrom(board)
                .where(
                        board.title.contains(word)
                                .or(board.content.contains(word))
                )
                .offset(pageable.getOffset())
                .limit(pageable.getPageSize());

        if(!ObjectUtils.isEmpty(pageable)){
            for(Sort.Order order: pageable.getSort()){
                PathBuilder<Board> pathBuilder = new PathBuilder<>(board.getType(), board.getMetadata());
                query.orderBy(
                        new OrderSpecifier(
                                order.getDirection().isAscending()?
                                        Order.ASC:Order.DESC,
                                pathBuilder.get(order.getProperty())));
            }
        }
        List<Board> content = query.fetch();
        JPAQuery<Long> countQuery = queryFactory
                .select(board.count())
                .from(board)
                .where(
                        board.title.contains(word)
                                .or(board.content.contains(word)));
        return PageableExecutionUtils.getPage(content, pageable, countQuery::fetchOne);
}
  • ObjectUtils 객체를 통해 Pageable의 값 유무를 확인한다.
  • Expression 객체에 값을 넣어주기 위해 PathBuilder 객체를 생성한다. PathBuilder 객체에 파라미터 값으로 QtypegetTypegetMetadata() 메서드를 넣어주면 된다.
  • Order에는 정렬 하고자 하는 방식을 넣어주는 자리다. order.getDirection().isAscending()? Order.ASC:Order.DESC에서 isAscending() 메서드는 순서가 오름차순인지 내림차순인지 여부를 나타내는 값이다.
profile
더 좋은 개발자가 되기위한 과정

0개의 댓글