[QueryDSL] 5. 실무 활용 - 스프링 데이터 JPA와 Querydsl
실무 활용 - 스프링 데이터 JPA와 Querydsl
3. 스프링 데이터 페이징 활용1 - Querydsl 페이징 연동
4. 스프링 데이터 페이징 활용2 - CountQuery 최적화
📌 스프링 데이터 JPA 리포지토리로 변경
순수 JPA 리포지토리를 스프링 데이터 JPA 리포지토리로 변경
스프링 데이터 JPA - MemberRepository 생성
package study.querydsl.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import study.querydsl.entity.Member;
import java.util.List;
public interface MemberRepository extends JpaRepository<Member, Long> {
List<Member> findByUsername(String username);
}
스프링 데이터 JPA 테스트
@SpringBootTest
@Transactional
class MemberRepositoryTest {
@Autowired
private EntityManager em;
@Autowired
private MemberRepository memberRepository;
@Test
void basicTest() {
Member member = new Member("member1", 10);
memberRepository.save(member);
Member findMember = memberRepository.findById(member.getId()).get();
assertThat(findMember).isEqualTo(member);
List<Member> result1 = memberRepository.findAll();
assertThat(result1).containsExactly(member);
List<Member> result2 = memberRepository.findByUsername(member.getUsername());
assertThat(result2).containsExactly(member);
}
}
- Querydsl 전용 기능인 회원 Search를 작성할 수 없다. → 사용자 정의 리포지토리 필요
📌 사용자 정의 리포지토리
사용자 정의 리포지토리 사용법
- 사용자 정의 인터페이스 정의
- 사용자 정의 인터페이스 구현
- 사용자 데이터 리포지토리에 사용자 정의 인터페이스 상속
사용자 정의 리포지토리 구성
- MemberRepository는 JpaRepository, MemberRepositoryCustom을 상속받는다.
- 사용자 정의 인터페이스인 MemberRepositoryCustom의 구현 클래스는 MemberRepositoryImpl이다.
- 따라서 MemberRepository에서 사용자 정의 메서드 search()를 사용할 수 있다.
1. 사용자 정의 인터페이스 작성
public interface MemberRepositoryCustom {
List<MemberTeamDto> search(MemberSearchCondition condition);
}
2. 사용자 정의 인터페이스 구현
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 usernameEq(String username) {
return hasText(username) ? member.username.eq(username) : null;
}
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;
}
public List<Member> searchMember(MemberSearchCondition condition) {
return queryFactory
.selectFrom(member)
.leftJoin(member.team, team)
.where(usernameEq(condition.getUsername()),
teamNameEq(condition.getTeamName()),
ageGoe(condition.getAgeLoe()),
ageLoe(condition.getAgeLoe()))
.fetch();
}
}
- 사용자 정의 구현클래스명에는 규칙이 존재한다.
- 리포지토리 인터페이스 명 + Impl
스프링 데이터 2.x부터는 사용자 정의 구현 클래스에 리포지토리 인터페이스 명 + Impl을 적용하는 대신 사용자 정의 인터페이스 명 + Impl 방식도 지원한다.
예를 들어 위 예제의 MemberRepositroyImpl 대신 MemberRepositoryCustomImpl 같이 구현해도 된다.
3. 사용자 데이터 리포지토리에 사용자 정의 인터페이스 상속
public interface MemberRepository extends JpaRepository<Member, Long>, MemberRepositoryCustom {
List<Member> findByUsername(String username);
}
사용자 정의 리포지토리 테스트 코드
@Test
public void searchTest() {
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();
condition.setAgeGoe(35);
condition.setAgeLoe(40);
condition.setTeamName("teamB");
List<MemberTeamDto> result = memberRepository.search(condition);
assertThat(result).extracting("username").containsExactly("member4");
}
📌 스프링 데이터 페이징 활용1 - Querydsl 페이징 연동
- 스프링 데이터의 Page, Pageable을 활용하여 Querydsl에서 페이징 처리를 할 수 있다.
- 전체 카운트를 한번에 조회하는 단순한 방법
- 데이터 내용과 전체 카운트를 분리하여 조회하는 방법
예제에 앞서 사용자 정의 인터페이스에 페이징 2가지를 추가한다.
사용자 정의 인터페이스에 페이징 2가지를 추가
public interface MemberRepositoryCustom {
List<MemberTeamDto> search(MemberSearchCondition condition);
Page<MemberTeamDto> searchPageSimple(MemberSearchCondition condition, Pageable pageable);
Page<MemberTeamDto> searchPageComplex(MemberSearchCondition condition, Pageable pageable);
}
전체 카운트를 한번에 조회하는 단순한 방법 → fetchResults 사용
public class MemberRepositoryImpl implements MemberRepositoryCustom {
// ...
@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(); // fetch()가 아닌 fetchResults()를 사용
List<MemberTeamDto> content = results.getResults();
long total = results.getTotal();
return new PageImpl<>(content, pageable, total);
}
}
- fetch()를 사용하면 data content를 바로 가져오기 때문에 fetchResults()를 사용
fetchResults() 사용 시 실제 쿼리는 2번 실행된다.(데이터 조회 쿼리 + count 쿼리) - fetchResult()는 카운트 쿼리 실행 시 필요없는 order by 조건은 제외하고 실행된다.
참고 : PageImpl은 Page인터페이스의 구현 클래스이다.
데이터 내용과 전체 카운트를 분리하여 조회하는 방법
public class MemberRepositoryImpl implements MemberRepositoryCustom {
// ...
@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);
}
}
- fetch()를 사용해서 content를 가져온다.
- totalCount는 fetchCount()를 이용해서 가져온다.
- 전체 카운트를 조회하는 방법을 최적화 할 수 있으면 성능 향상에 상당한 효과가 있기 때문에 분리하는게 좋다.
📌 스프링 데이터 페이징 활용2 - CountQuery 최적화
PageableExecutionUtils.getPage()로 최적화
@Override
public Page<MemberTeamDto> searchPageComplex(MemberSearchCondition condition, Pageable pageable) {
//...
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 - 컨트롤러 개발
@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);
}
}
- 테스트 URL : http://localhost:8080/v2/members?size=5&page=2
size와 page를 request 파라미터로 넘겨서 테스트한다.
스프링 데이터 정렬(Sort)
스프링 데이터 JPA는 자신의 정렬(Sort)을 Querydsl의 정렬(OrderSpecifier)로 편리하게 변경하는 기능을 제공한다.
스프링 데이터의 정렬을 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 를 사용하기 보다는 파라미터를 받아서 직접 처리하는 것을 권장한다.
해당 글은 인프런의 [실전! Querydsl] 강의를 정리한 내용입니다.
실전! Querydsl - 인프런 | 강의
Querydsl의 기초부터 실무 활용까지, 한번에 해결해보세요!, - 강의 소개 | 인프런
www.inflearn.com