ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • JPA QueryDSL 프레임워크 - 4 스프링 데이터 JPA와 Querydsl
    DBMS/JPA 2024. 8. 20. 04:57
    728x90
    반응형

    JPA QueryDSL 프레임워크 - 4 스프링 데이터 JPA와 Querydsl

     

     

    1. 스프링 데이터 JPA 리포지토리로 변경

    스프링 데이터 JPA - MemberRepository 생성

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

    Querydsl 전용 기능인 회원 search를 작성할 수 없다.

    사용자 정의 리포지토리 필요

     

     

     

    2. 사용자 정의 리포지토리

    사용자 정의 리포지토리 사용법

    1. 사용자 정의 인터페이스 작성

    2. 사용자 정의 인터페이스 구현

    3. 스프링 데이터 리포지토리에 사용자 정의 인터페이스 상속

     

     

    2 - 1. 사용자 정의 리포지토리 구성

     

     

    2 - 2. 사용자 정의 인터페이스 작성

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

     

     

    2 - 3. 사용자 정의 인터페이스 구현

    public class MemberRepositoryImpl implements MemberRepositoryCustom {
        private final JPAQueryFactory queryFactory;
        public MemberRepositoryImpl(EntityManager em) {
            this.queryFactory = new JPAQueryFactory(em);
        }
        @Override
        //회원명, 팀명, 나이(ageGoe, ageLoe)
        public List<MemberTeamDto> search(MemberSearchCondition condition) {
            return queryFactory
                    .select(new QMemberTeamDto(
                            member.id,
                            member.username,
                            member.age,
                            team.id,
                            team.name))
                    .from(member)
                    .leftJoin(member.team, team)
                    .where(usernameEq(condition.getUsername()),
                            teamNameEq(condition.getTeamName()),
                            ageGoe(condition.getAgeGoe()),
                            ageLoe(condition.getAgeLoe()))
                    .fetch();
        }
        private BooleanExpression usernameEq(String username) {
            return isEmpty(username) ? null : member.username.eq(username);
        }
        private BooleanExpression teamNameEq(String teamName) {
            return isEmpty(teamName) ? null : team.name.eq(teamName);
        }
        private BooleanExpression ageGoe(Integer ageGoe) {
            return ageGoe == null ? null : member.age.goe(ageGoe);
        }
        private BooleanExpression ageLoe(Integer ageLoe) {
            return ageLoe == null ? null : member.age.loe(ageLoe);
        }
    }

     

     

    2 - 4. 스프링 데이터 리포지토리에 사용자 정의 인터페이스 상속

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

     

     

     

    3. 스프링 데이터 페이징 활용

    3 - 1. Querydsl 페이징 연동

    스프링 데이터의 Page, Pageable을 활용해보자.

    전체 카운트를 한번에 조회하는 단순한 방법

    데이터 내용과 전체 카운트를 별도로 조회하는 방법

     

    사용자 정의 인터페이스에 페이징 2가지 추가

    public interface MemberRepositoryCustom {
        List<MemberTeamDto> search(MemberSearchCondition condition);
        Page<MemberTeamDto> searchPageSimple(MemberSearchCondition condition,
                                             Pageable pageable);
        Page<MemberTeamDto> searchPageComplex(MemberSearchCondition condition,
                                              Pageable pageable);
    }

     

     

    3 - 1 - 1. 전체 카운트를 한번에 조회하는 단순한 방법

    searchPageSimple(), fetchResults() 사용

    /**
     * 단순한 페이징, fetchResults() 사용
     */
    @Override
    public Page<MemberTeamDto> searchPageSimple(MemberSearchCondition condition,
                                                Pageable pageable) {
        QueryResults<MemberTeamDto> results = queryFactory
                .select(new QMemberTeamDto(
                        member.id,
                        member.username,
                        member.age,
                        team.id,
                        team.name))
                .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);
    }

    Querydsl이 제공하는 fetchResults() 를 사용하면 내용과 전체 카운트를 한번에 조회할 수 있다.

    (실제 쿼리는 2번 호출)

    fetchResult() 는 카운트 쿼리 실행시 필요없는 order by 는 제거한다.

     

     

    3 - 1 - 2. 데이터 내용과 전체 카운트를 별도로 조회하는 방법

    searchPageComplex()

    /**
     * 복잡한 페이징
     * 데이터 조회 쿼리와, 전체 카운트 쿼리를 분리
     */
    @Override
    public Page<MemberTeamDto> searchPageComplex(MemberSearchCondition condition,
                                                 Pageable pageable) {
        List<MemberTeamDto> content = queryFactory
                .select(new QMemberTeamDto(
                        member.id,
                        member.username,
                        member.age,
                        team.id,
                        team.name))
                .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);
    }

    전체 카운트를 조회 하는 방법을 최적화 할 수 있으면 이렇게 분리하면 된다.

    (예를 들어서 전체 카운트를 조회할 때 조인 쿼리를 줄일 수 있다면 상당한 효과가 있다.)

     

    코드를 리펙토링해서 내용 쿼리과 전체 카운트 쿼리를 읽기 좋게 분리하면 좋다.

     

     

    3 - 2. CountQuery 최적화

    PageableExecutionUtils.getPage()로 최적화

    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);

    스프링 데이터 라이브러리가 제공

     

    count 쿼리가 생략 가능한 경우 생략해서 처리

     

    페이지 시작이면서 컨텐츠 사이즈가 페이지 사이즈보다 작을 때

    마지막 페이지 일 때

    (offset + 컨텐츠 사이즈를 더해서 전체 사이즈 구함,

    더 정확히는 마지막 페이지이면서 컨텐츠 사이즈가 페이지 사이즈보다 작을 때)

     

     

    3 - 3. 컨트롤러 개발

    @RestController
    @RequiredArgsConstructor
    public class MemberController {
        private final MemberJpaRepository memberJpaRepository;
        private final MemberRepository memberRepository;
        @GetMapping("/v1/members")
        public List<MemberTeamDto> searchMemberV1(MemberSearchCondition condition) {
            return memberJpaRepository.search(condition);
        }
        @GetMapping("/v2/members")
        public Page<MemberTeamDto> searchMemberV2(MemberSearchCondition condition,
                                                  Pageable pageable) {
            return memberRepository.searchPageSimple(condition, pageable);
        }
        @GetMapping("/v3/members")
        public Page<MemberTeamDto> searchMemberV3(MemberSearchCondition condition,
                                                  Pageable pageable) {
            return memberRepository.searchPageComplex(condition, pageable);
        }
    }

     

     

     

    4. 스프링 데이터 정렬(Sort)

    스프링 데이터 JPA는 자신의 정렬(Sort)을 Querydsl의 정렬(OrderSpecifier)로 편리하게 변경하는 기능을 제공한다.

    이 부분은 뒤에 스프링 데이터 JPA가 제공하는 Querydsl 기능에서 살펴보겠다.

     

    스프링 데이터의 정렬을 Querydsl의 정렬로 직접 전환하는 방법은 다음 코드를 참고하자.

     

    스프링 데이터 Sort를 Querydsl의 OrderSpecifier로 변환

    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();

     

    참고

    정렬( Sort )은 조건이 조금만 복잡해져도 Pageable 의 Sort 기능을 사용하기 어렵다.

    루트 엔티티 범위를 넘어가는 동적 정렬 기능이 필요하면 스프링 데이터 페이징이 제공하는 Sort 를 사용하기 보다는

    파라미터를 받아서 직접 처리하는 것을 권장한다.

    728x90
    반응형
Designed by Tistory.