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을 포함하는지 확인한다.
@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 쿼리가 생략 가능한 경우가 있다.
@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가 불필요한 경우에는 호출하지 않아서 쿼리가 날아가지 않는다.
이런 식으로 성능 최적화를 할 수 있다.
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 기능을 사용하기 어렵다.
따라서 그냥 파라미터를 받아서 사용하는 편이 낫다.