스프링 데이터 페이징 활용
Querydsl 페이징 연동
public interface MemberRepositoryCustom {
Page<MemberTeamDto> searchPageSimple(MemberSearchCondition condition, Pageable pageable);
Page<MemberTeamDto> searchPageComplex(MemberSearchCondition condition, Pageable pageable);
}
단순한 페이징과 복잡한 페이징을 나눠서 설명할 것이다.
단순한 페이징
public class MemberRepositoryImpl implements MemberRepositoryCustom {
@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);
}
}
전체 카운트를 한번에 조회한다.
실제 DB에 쿼리는 fetch와 count 둘 다 나간다.
searchPageSimple()
fetchResults()
내용과 전체 카운트를 한번에 조회할 수 있다.
지금은 fetch(), count()로 나뉘었다.
복잡한 페이징
public class MemberRepositoryImpl implements MemberRepositoryCustom {
@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);
}
}
데이터 내용과 전체 카운트를 별도로 조회한다.
전체 카운트 조회 방법을 최적화 할 수 있다면 유리하다.
ex. 전체 카운트 조회 시 조인 쿼리를 줄인다.
코드를 리팩터링 해서 내용 쿼리와 전체 카운트 쿼리를 읽기 좋게 분리하면 좋다.
CountQuery 최적화
count 쿼리를 생략 할 수 있는 경우
페이지 시작이면서 컨텐츠 사이즈가 페이지 사이즈보다 작을 때
첫번째 페이지가 100개를 출력할 수 있는데 데이터가 3개라면 굳이 count 쿼리를 날릴 필요가 없다.
마지막 페이지 일 때
offset + 컨텐츠 사이즈로 전체 사이즈를 구한다.
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();
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);
}
}
참고
Querydsl fetchResults(), fetchCount() Deprecated
fetchResults()는 select를 단순히 count 처리하는 용도로 바꾸는 정도라 복잡한 쿼리에서는 제대로 동작하지 않는다.
public class MemberRepositoryImpl implements MemberRepositoryCustom {
@Override
public Page<MemberTeamDto> searchPageSimple(MemberSearchCondition condition, Pageable pageable) {
Long totalCount = queryFactory
// .select(Wildcard.count) // select count(*)
.select(member.count()) // select count(member.id)
.from(member)
.fetchOne();
}
}
select(Wildcard.count)
count(*)을 사용하고 싶을 때 사용한다.
select(member.count())
select count(member.id)로 처리한다.
fetchOne()
응답 결과는 숫자 하나이므로 fetchOne()을 사용한다.
CountQuery 최적화
count 쿼리가 생략 가능한 경우 생략해서 처리한다.
페이지 시작점이면서 컨텐츠 사이즈가 페이지 사이즈보다 작을 때
마지막 페이지일 때
offset + 컨텐츠 사이즈를 더해 전체 사이즈를 구한다.
public class MemberRepositoryImpl implements MemberRepositoryCustom {
@Override
public Page<MemberTeamDto> searchPageSimple(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 쿼리는 fetchCount()를 하지 않으면 실제 쿼리가 날아가지 않는다.
인자로 함수를 넘기면 content, pageable의 토탈 사이즈를 보고 실행한다.
컨트롤러 개발
지금까지 한 내용이 실제 동작하는지 확인한다.
@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);
}
}
http://localhost:8080/v2/members?size=5&page=2
select count(member1)
from Member member1
left join member1.team as team
count 쿼리가 나간다.
http://localhost:8080/v3/members?size=110&page=0
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 쿼리만 나간다.
두 번째 페이지로 넘어갈 컨텐츠 개수가 없기 때문에 count 쿼리를 날리지 않는다.
정렬
스프링 데이터 JPA가 OrderSpecifier로 정렬 기능을 제공한다.
public class MemberRepositoryImpl implements MemberRepositoryCustom {
@Override
public Page<MemberTeamDto> searchPageSimple(MemberSearchCondition condition, Pageable pageable) {
...
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();
}
}
스프링 데이터 JPA의 정렬을 Querydsl의 정렬로 직접 변환할 수 있다.
참고
정렬은 조건이 조금만 복잡해져도 Pageable의 Sort를 쓰기 어렵다.
루트 엔티티 범위를 넘어서는 동적 정렬이 필요하면 Sort보다는 파라미터를 받아서 직접 처리하자.
Last updated
Was this helpful?