스프링 data jpa와 querydsl

OneTwoThree·2023년 8월 22일
0

실전querydsl

목록 보기
6/6

출처


구조 잡기

spring data jpa에서 querydsl을 사용하려면 구조를 먼저 잡아줘야 한다.
MemberRepository를 예로 들면
MemberRepository가 JpaRepository를 상속받고, MemberRepositoryCustom을 상속받는다. (셋 다 인터페이스)
MemberRepositoryCustom을 MemberRepositoryImpl이 구현한다. 필요한 querydsl 메소드들은 여기서 구현한다.

public interface MemberRepository extends JpaRepository<Member, Long>, MemberRepositoryCustom {
   List<Member> findByUsername(String username);
}

이렇게 MemberRepository가 MemberRepositoryCustom을 상속받는다.
인터페이스는 여러 개 상속받을 수 있다.

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

MemberRepositoryCustom에는 이렇게 메소드를 선언해준다

public class MemberRepositoryImpl implements MemberRepositoryCustom{

    private final JPAQueryFactory queryFactory;

    public  MemberRepositoryImpl(EntityManager em){
        this.queryFactory = new JPAQueryFactory(em);
    }

    @Override
    public List<MemberTeamDto> search(MemberSearchCondition condition){
        return 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())
                )
                .fetch();
    }

    private BooleanExpression ageLoe(Integer ageLoe) {
        return ageLoe !=null? member.age.loe(ageLoe) : null;
    }

    private BooleanExpression ageGoe(Integer ageGoe) {
        return ageGoe != null? member.age.goe(ageGoe) : null;
    }

    private BooleanExpression teamNameEq(String teamName) {
        return hasText(teamName) ? team.name.eq(teamName) : null;
    }

    private BooleanExpression usernameEq(String username) {
        return hasText(username) ? member.username.eq(username) : null;
    }

}

MemberRepositoryImpl에는 이렇게 메소드를 querydsl을 사용해 구현해준다.

페이징 활용

    @Override
    public Page<MemberTeamDto> searchPageSimple(MemberSearchCondition condition, Pageable pageable) {
        QueryResults<MemberTeamDto> results = 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()) // 한 페이지에서 몇 개 표시할지
                .fetchResults();

        List<MemberTeamDto> content = results.getResults();
        long total = results.getTotal();

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

offset : 앞에서 몇 개 페이지를 건너뛸지
limit : 한 페이지에서 몇 개 표시할지
를 의미한다.
그리고 getTotal()로 전체 페이지 수를 얻을 수 있다.
얻은 데이터들로 PageImpl을 생성해서 반환하면 된다
첫번째 인자는 content, 두번째 인자는 pageable, 세번째 인자는 total이다.

테스트

   @Test
    public void searchPageSimple(){
        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);

        MemberSearchCondition condition = new MemberSearchCondition();
        PageRequest pageRequest = PageRequest.of(0,3);

        Page<MemberTeamDto> result = memberRepository.searchPageSimple(condition, pageRequest);

        assertThat(result.getSize()).isEqualTo(3);
        assertThat(result.getContent()).extracting("username")
                .containsExactly("member1","member2","member3");
    }

테스트는 위와 같은 방식으로 작성한다.
PageRequest.of(0,3)으로 테스트 한다.
그리고 result.getSize()로 총 크기를 얻는다.
result.getContent()로 내용을 얻을 수 있다.
extracting으로 username을 뽑아서
containsExactly로 member1, 2, 3을 포함하는지 확인한다.

Data 내용과 전체 Count 별도 조회

    @Override
    public Page<MemberTeamDto> searchPageComplex(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();

        long total = queryFactory
                .select(member)
                .from(member)
                .leftJoin(member.team, team)
                .where(
                        usernameEq(condition.getUsername()),
                        teamNameEq(condition.getTeamName()),
                        ageGoe(condition.getAgeGoe()),
                        ageLoe(condition.getAgeLoe())
                )
                .fetchCount();
        
        return new PageImpl<>(content,pageable,total);
    }

이런 식으로 content는 fetch로 뽑고 fetchCount로 count를 위한 쿼리를 따로 날린다.

이렇게 하면 좋은 점은 content 쿼리는 복잡한데 count 쿼리는 쉽게 만들 수 있는 경우가 있다.

전체 count를 조회하는 방법을 최적화 하려면 이렇게 별도 쿼리를 작성하는 것이 좋다

Count 쿼리 최적화

때에 따라서 Count 쿼리가 생략 가능한 경우가 있다.

  • 페이지 시작하면서 페이지 사이즈보다 컨텐츠 사이즈가 작을 때
  • 마지막 페이지 일 때
    @Override
    public Page<MemberTeamDto> searchPageComplex(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<Member> countQuery = queryFactory
                .select(member)
                .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.fetchCount());
    }

이런 식으로 PageableExecutionUtils를 활용해서 넘겨주면 fetchCount가 불필요한 경우에는 호출하지 않아서 쿼리가 날아가지 않는다.

이런 식으로 성능 최적화를 할 수 있다.

Sort

JPAQuery<Member> query = queryFactory
 .selectFrom(member);
for (Sort.Order o : pageable.getSort()) {
 PathBuilder pathBuilder = new PathBuilder(member.getType(),
member.getMetadata());
 query.orderBy(new OrderSpecifier(o.isAscending() ? Order.ASC : Order.DESC,
 pathBuilder.get(o.getProperty())));
}
List<Member> result = query.fetch();

Spring Data Jpa의 sort를 querydsl로 변환하려면 위 코드와 같은 방법으로 해야 한다.
정렬 조건이 조금만 복잡해져도 Pageable의 sort 기능을 사용하기 어렵다.
따라서 그냥 파라미터를 받아서 사용하는 편이 낫다.

0개의 댓글