간단한 페이지네이션 - fetchResults()
offset() 메서드와 limit() 메서드를 통해서 querydsl로 페이지네이션을 할 수 있다.
offset은 몇번째 페이지를 가져올지, limit은 한 페이지에 몇개의 데이터를 가져올지를 의미한다.
package study.querydsl.repository;
import com.querydsl.core.types.dsl.BooleanExpression;
import com.querydsl.jpa.impl.JPAQueryFactory;
import jakarta.persistence.EntityManager;
import org.springframework.data.domain.Pageable;
import org.springframework.util.StringUtils;
import study.querydsl.dto.MemberSearchCondition;
import study.querydsl.dto.MemberTeamDto;
import study.querydsl.dto.QMemberTeamDto;
import java.util.List;
import static study.querydsl.entity.QMember.member;
import static study.querydsl.entity.QTeam.team;
// 여기는 이름을 꼭 맞춰주어야함 ~~Repository + Impl
public class MemberRepositoryImpl implements MemberRepositoryCustom {
private final JPAQueryFactory queryFactory;
// JPAQueryFactory가 스프링 빈으로 등록돼 있다면 그걸 사용. 아니라면 이렇게 사용해도 상관없다.
public MemberRepositoryImpl(EntityManager em) {
queryFactory = new JPAQueryFactory(em);
}
//.... 생략
@Override
public List<MemberTeamDto> searchPageSimple(MemberSearchCondition condition, Pageable pageable) {
List<MemberTeamDto> result = 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();
return result;
}
private BooleanExpression usernameEq(String username) { // BooleanExpression은 조합이 가능하다.
return StringUtils.hasText(username) ? member.username.eq(username) : null;
}
private BooleanExpression teamNameEq(String teamName) {
return StringUtils.hasText(teamName) ? member.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;
}
}
이렇게 마지막에 .fetch() 메서드로 끝내게 되면 반환 타입이 List이다.
마지막을 .fetchResult()로 끝내게 되면 QueryResults 객체로 리턴되고,
카운트 쿼리와 데이터를 가져오는 쿼리가 한번씩 총 2번 나가게 된다.
@Override
public List<MemberTeamDto> searchPageSimple(MemberSearchCondition condition, Pageable pageable) {
QueryResults<MemberTeamDto> result= 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 = result.getResults();
long totalCnt = result.getTotal();
return content;
}
페이지 객체로 리턴하고 싶다면 아래처럼하면 된다.
@Override
public Page<MemberTeamDto> searchPageSimple(MemberSearchCondition condition, Pageable pageable) {
QueryResults<MemberTeamDto> result= 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 = result.getResults();
long totalCnt = result.getTotal();
return new PageImpl<>(content, pageable, totalCnt);
}
만든 페이징 조회 메서드는 아래와 같이 사용하면 된다
MemberSearchCondition condition = new MemberSearchCondition();
PageRequest pageRequest = PageRequest.of(0, 3); // PageRequest 객체로 0번 페이지에 3개의 데이터를 담아 조회
Page<MemberTeamDto> result = memberRepository.searchPageSimple(condition, pageRequest); // 페이지 객체로 결과 반환
전체 테스트 코드
@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", 10, 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");
}
이렇게 fetchResults()를 사용하면 컨텐츠와 전체 카운트를 한번에 조회할 수 있다.
실제 쿼리는 카운트 쿼리와 내용 조회 쿼리 2개가 나가게 된다.
fetchResults()는 카운트 쿼리 실행 시에 order by를 제거한다. (필요 없으므로)
데이터 조회와 카운트 쿼리 분리
위의 fetchResults()를 사용하게 되면, 카운트 쿼리가 나갈 때 최적화가 되지 않다.
(예를 들면, 카운트 쿼리가 나갈 때는 조인이 필요 없어지는 경우가 있는데, 그런 조인을 제거하는 등의 최적화가 되지 않는다.)
따라서 카운트 쿼리를 최적화 할 수 있다면, fetchResults()를 사용하지 않고 아래와 같이 분리하는 것을 추천한다.
@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);
}
페이지네이션에서의 카운트 쿼리 최적화
count 쿼리가 생략 가능한 경우
count 쿼리가 생략 가능한 경우, 생략에서 처리가 가능하다.
아래 두 가지 경우일 때 생략이 가능하다.
- 페이지 시작(0 페이지)이면서 컨턴츠 사이즈가 페이지 사이즈보다 작을 때
- 마지막 페이지이면서, 컨텐츠의 사이즈가 페이지의 사이즈보다 작을 때(offset + 컨텐츠 사이즈를 더해서 전체 사이즈 구함)
PageableExecutuonUtils()를 활용하면 카운트 쿼리가 생략 가능한 조건인지를 확인한 후, 생략 가능하다면 카운트 쿼리를 날리지 않는다.
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()));
PageableExecutionUtils.getPage(content, pageable, countQuery::fetchCount); // 두가지 조건중 하나를 충족하면, 맨뒤 countQuery::fetchCount를 생략
public Page<MemberTeamDto> searchPageComplex2(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();
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);
}
deprecated된 fetchResults와 fetchCount
해당 포스팅은 querydsl 강의를 듣고 작성한 글이고, 강의에서는 fetchResults와 fetchCount를 사용하고 있는데, 이 두 메서드는 Deprecated되었다.
deprecated된 이유
fetchResults()와 fetchCount() 모두 querydsl 내부에서 count 쿼리를 만들어서 실행해야하는데, 이때 작성한 select 쿼리를 기반으로 count 쿼리를 만든다.
이 두 메서드는 단순한 쿼리에서는 카운트가 잘 되는데, group by, having절이 포함되는 복잡한 쿼리에서는 잘 작동하지 않는 메서드라고 한다.
또하 메서드를 실행하면 메모리에서 카운트를 생성하기에 결과 데이터의 크기가 큰 경우, 성능에 문제가 생갈 수 있다고 한다.
대안 - 데이터 조회와 카운트 쿼리 분리 & member.count() 이용
fetchCount의 대안은 아래처럼 count() 메서드를 이용하는 것이다.
AS-IS
Long totalCnt = queryFactory
.selectFrom(member)
.fetchCount();
TO-BE
Long totalCnt = queryFactory
.select(member.count())
.from(member)
.fetchOne();
fetchResults의 대안은 컨텐츠를 조회하는 쿼리와 카운트 조회 쿼리를 분리하고, PageImpl 객체를 생성하여 반환하는 것이다.
이는 TO-BE 예시만 작성하고 넘어가겠다..ㅎㅎ
TO-BE
@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.count()) // 여기가 변경됨!!
.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);
}
Reference
- 인프런 강의 <실전! Querydsl> (김영한)
- https://ururuwave.tistory.com/151
[Querydsl] fetchResults(), fetchCount() 대체하기
🔽Querydsl의 미지원 API 목록 링크 Deprecated List (Querydsl 5.0.0 API)JavaScript is disabled on your browser. Deprecated Methods Method and Description com.querydsl.sql.codegen.NamingStrategy.appendSchema(String, String) com.querydsl.jpa.impl.
ururuwave.tistory.com
'개념 공부 > Spring & ORM' 카테고리의 다른 글
[Spring / Jpa / querydsl ] 커스텀 리포지토리 구조 (0) | 2025.03.23 |
---|---|
[Java / querydsl] querydsl로 동적쿼리를 처리하기 (3) | 2025.03.18 |
[Java / querydsl] 여러 Projection 방법 (0) | 2025.03.16 |
[Java / Spring] @Transational과 ChainedTransactionManager (0) | 2025.03.12 |
[JPA 개념] 연관 관계 매핑 (2) | 2024.06.06 |