[Query DSL] Spring Data JPA와 Querydsl

홍정완·2022년 8월 16일
0

JPA

목록 보기
28/38
post-thumbnail

Spring Data JPA Repository로 변경


이전 포스팅에서 작성했던 순수 JPA를 아래와 같이 Spring Data JPA로 변경해 보자.


Spring Data JPA - MemberRepository

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



사용자 정의 Repository


사용자 정의 Repository 구성

  • MemberRepositoryCustom 👉 MemberCustomRepository 변경
  • MemerRepositoryImpl 👉 MemberCustomRepositoryImpl 변경

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

public interface MemberCustomRepository {

	List<MemberTeamDto> search(MemberSearchCondition condition);
}

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

public class MemberCustomRepositoryImpl implements MemberCustomRepository{

	private final JPAQueryFactory queryFactory;

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

    @Override
    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 hasText(teamName) ? team.name.eq(teamName):null;
    }

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

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

Spring Data JPA에 사용자 정의 인터페이스 상속

public interface MemberRepository extends JpaRepository<Member, Long>, MemberCustomRepository {

	List<Member> findByUsername(String username);
}



💡 Querydsl 페이징 연동


Spring Data의 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);
}



전체 카운트를 한 번에 조회하는 단순한 방법 - 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는 제거한다.



데이터 내용과 전체 카운트를 별도로 조회하는 방법 - 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);
}
  • 전체 카운트를 조회하는 방법을 최적화할 수 있으면 이렇게 분리하면 된다.

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

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



💡 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 + 컨텐츠 사이즈를 더해서 전체 사이즈 구함)



Spring Data Sort


Spring Data JPA는 정렬(Sort)을 Querydsl의 정렬(OrderSpecifier)로 편리하게 변경하는
기능을 제공한다.


Spring Data의 정렬을 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)은 조건이 조금만 복잡해져도 PageableSort 기능을 사용하기 어렵다.
루트 엔티티 범위를 넘어가는 동적 정렬 기능이 필요하면 스프링 데이터 페이징이 제공하는 Sort를 사용하기보다는 파라미터를 받아서 직접 처리하는 것을 권장한다.

profile
습관이 전부다.

0개의 댓글