fetchResults(), fetchCount()는 Querydsl 5.0 부터 deprecated 되었다. fetchResults(), fetchCount()는 둘 다 Querydsl 내부에서 count 쿼리를 만들어서 실행해야 하는데, 이때 작성한 select 쿼리를 기반으로 count 쿼리를 만들어낸다. 그런데 이 기능이 select 구문을 단순히 count 처리하는 것으로 바꾸는 정도여서, 단순히 쿼리에서는 잘 동작하지만, 복잡한 쿼리에서는 잘 동작하지 않는다. 그렇기 때문에 count 쿼리를 별도로 작성하고, fetch()를 사용해서 해결해야 한다.
사용자 정의 인터페이스에 페이징 정의
public interface MemberRepositoryCustom {
List<MemberTeamDto> search(MemberSearchCondition condition);
}
사용자 정의 클래스에 인터페이스에 정의한 페이징 구현
public class MemberRepositoryImpl implements MemberRepositoryCustom {
private final JPAQueryFactory queryFactory;
public MemberRepositoryImpl(JPAQueryFactory queryFactory) {
this.queryFactory = queryFactory;
}
@Override
public Page<MemberTeamDto> searchPage(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<Long> countQuery = queryFactory
.select(member.count())
.from(member)
//.leftJoin(member.team, team) count 최적화
.where(
usernameEq(condition.getUsername()),
teamNameEq(condition.getTeamName()),
ageGoe(condition.getAgeGoe()),
ageLoe(condition.getAgeLoe())
);
return new PageImpl<>(content,pageable,totalCount);
}
}
fetchResults()
와 fetchCount()
가 deprecated 되었기 때문에 content를 가져올려면 fetch()
로 가져오고 Count는 따로 Count 쿼리로 구현해서 가져오면 된다.
PageImpl
:PageImpl
은Page
인터페이스의 구현체이다.PageImpl
에 첫번째 인자로content
(조회된 컨펜츠),Pageable
요청으로부터 가져온 페이지 요청 데이터),totalCount
(전체 컨텐츠의 개수)를 주면 된다.
@Test
void searchPageTest(){
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);
//초기화
em.flush();
em.clear();
MemberSearchCondition condition = new MemberSearchCondition();
PageRequest pageRequest = PageRequest.of(0, 3);
Page<MemberTeamDto> result = memberRepository.searchPage(condition, pageRequest);
assertThat(result.getContent()).extracting("username")
.containsExactly("member1", "member2", "member3");
}
@Override
public Page<MemberTeamDto> searchPage(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<Long> countQuery = queryFactory
.select(member.count())
.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::fetchOne);
//return new PageImpl<>(content,pageable,totalCount);
}
PageableExecutionUtils.getPage()
:PageableExecutionUtils.getPage()
은PageImpl
과 같은 역할을 하지만 한 가지 추가된 점은 마지막 인자로 함수를 전달하는데 내부 작동에 의해서totalCount
가 페이지 사이즈 보다 적거나, 마지막 페이지 일 경우 해당 함수를 실행하지 않는다. 즉 쿼리를 조금 더 줄일 수 있다.
전체 데이터를 100으로 설정하고 포스트맨을 이용해 페이지의 사이즈를 50으로 주고 실행하면 다음과 같은 쿼리를 볼 수 있다.
/* select
member1.id as memberId,
member1.username,
member1.age,
team.id as teamId,
team.name as teamName
from
Member member1
left join
member1.team as team */ select
member0_.member_id as col_0_0_,
member0_.username as col_1_0_,
member0_.age as col_2_0_,
team1_.team_id as col_3_0_,
team1_.name as col_4_0_
from
member member0_
left outer join
team team1_
on member0_.team_id=team1_.team_id limit ?
=============================================================================
/* select
count(member1)
from
Member member1 */ select
count(member0_.member_id) as col_0_0_
from
member member0_
select 쿼리와 count 쿼리가 각각 한 번씩 나가는 걸 볼 수 있다.
만약 페이즈의 사이즈를 200으로 설정할 경우 전체 데이터 수 보다 크기 때문에 PageableExecutionUtils.getPage()
의 내부 기능으로 인해count 쿼리를 실행시키지 않는다.
/* select
member1.id as memberId,
member1.username,
member1.age,
team.id as teamId,
team.name as teamName
from
Member member1
left join
member1.team as team */ select
member0_.member_id as col_0_0_,
member0_.username as col_1_0_,
member0_.age as col_2_0_,
team1_.team_id as col_3_0_,
team1_.name as col_4_0_
from
member member0_
left outer join
team team1_
on member0_.team_id=team1_.team_id limit ?
조금이라도 최적화를 위해서는 PageImpl
보다 PageableExecutionUtils.getPage()
을 사용하는게 좋을거 같다.
QueryDSL에서 동적으로 정렬하려면 OrderSpecifier
객체를 사용하면 된다.
OrderSpecifier
객체에 인자로는 Sort
의 Order
와 Expression
객체, Null
값을 핸들링하기 위한 Enum
값을 받는다.
public Page<Board> searchBoard(String word, Pageable pageable) {
JPAQuery<Board> query = queryFactory
.selectFrom(board)
.where(
board.title.contains(word)
.or(board.content.contains(word))
)
.offset(pageable.getOffset())
.limit(pageable.getPageSize());
if(!ObjectUtils.isEmpty(pageable)){
for(Sort.Order order: pageable.getSort()){
PathBuilder<Board> pathBuilder = new PathBuilder<>(board.getType(), board.getMetadata());
query.orderBy(
new OrderSpecifier(
order.getDirection().isAscending()?
Order.ASC:Order.DESC,
pathBuilder.get(order.getProperty())));
}
}
List<Board> content = query.fetch();
JPAQuery<Long> countQuery = queryFactory
.select(board.count())
.from(board)
.where(
board.title.contains(word)
.or(board.content.contains(word)));
return PageableExecutionUtils.getPage(content, pageable, countQuery::fetchOne);
}
ObjectUtils
객체를 통해 Pageable
의 값 유무를 확인한다.Expression
객체에 값을 넣어주기 위해 PathBuilder
객체를 생성한다. PathBuilder
객체에 파라미터 값으로 Qtype
에 getType
과 getMetadata()
메서드를 넣어주면 된다.Order
에는 정렬 하고자 하는 방식을 넣어주는 자리다. order.getDirection().isAscending()? Order.ASC:Order.DESC
에서 isAscending()
메서드는 순서가 오름차순인지 내림차순인지 여부를 나타내는 값이다.