반응형
해당 글은 김영한 님의 querydsl을 수강하며 정리하려고 적는 포스팅입니다.
먼저 스프링 데이터의 Page, Pageable을 활용해서 페이징 결과를 조회할 것이다.
그리고 전체 카운트를 한 번에 조회하는 단순 방법을 사용해 볼 것이다.
또한 데이터 내용과 전체 카운트를 별도로 조회하는 방법을 사용해 볼 것이다.
💡 참고 : Spring Boot 2.6 이상, Querydsl 5.0 지원 방법
Querydsl fetchResults() , fetchCount() Deprecated(향후 미지원)
Querydsl에서 제공하는 fetchCount()와 fetchResult()는 개발자가 작성한 select 쿼리로 count 쿼리를 내부에서 만들어서 실행시킨다. 그런데 이 기능은 select 구문을 단순히 count 쿼리로 변경하는 정도이다. 따라서 단순한 쿼리에서는 잘 작동하겠지만, 복잡한 쿼리로 넘어가면 잘 작동하지 않고 문제가 발생한다.
따라서 Queyrdsl은 향후 fetchCount()와 fetchResult()를 지원하지 않는다고 한다. Querydsl의 변화가 빠르지 않기 때문에 당장에 기능을 제거할 것 같지는 않다.
따라서 count 쿼리가 필요하면 별도로 작성해야 한다.
count 쿼리 예제
@Test
public void count() {
Long totalCount = queryFactory
//.select(Wildcard.count) //select count(*)
.select(member.count()) //select count(member.id)
.from(member)
.fetchOne();
System.out.println("totalCount = " + totalCount);
}
- count(*)을 사용하고 싶다면, 위의 주석처럼 WildCard.count를 사용하면 된다.
- member.count()를 사용하면 count(member.id)로 처리된다.
- 응답 결과는 숫자 하나 이므로 fetchOne()을 사용한다.
📌 이전 버전으로 예제 작성 - fetchResults() , fetchCount() 사용
사용자 정의 인터페이스에 페이징 두 가지 추가
public interface MemberRepositoryCustom {
List<MemberTeamDto> search(MemberSearchCondition condition);
Page<MemberTeamDto> searchPageSimple(MemberSearchCondition condition, Pageable pageable);
Page<MemberTeamDto> searchPageComplex(MemberSearchCondition condition, Pageable pageable);
}
전체 카운드를 한 번에 조회하는 단순 방법
- searchPageSimple(), fetchResults() 사용
public class MemberRepositoryImpl implements MemberRepositoryCustom{
private final JPAQueryFactory queryFactory;
public MemberRepositoryImpl(JPAQueryFactory queryFactory) {
this.queryFactory = queryFactory;
}
@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 total = result.getTotal();
return new PageImpl<>(content, pageable, total);
}
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;
}
}
- Querydsl이 제공하는 fetchResults()를 사용하면 내용과 전체 카운트를 한번에 조회할 수 있다. (실제 쿼리는 2번 호출)
- fetchResult()는 카운트 쿼리 실행 시 필요 없는 order by를 제거한다.
데이터 내용과 전체 카운트를 별도로 조회하는 방법
- searchPageComplex()
public class MemberRepositoryImpl implements MemberRepositoryCustom{
private final JPAQueryFactory queryFactory;
public MemberRepositoryImpl(JPAQueryFactory queryFactory) {
this.queryFactory = queryFactory;
}
@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
.selectFrom(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);
}
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;
}
}
- 별도로 나누어 두면 전체 count를 조회하는 방법을 최적화할 수 있으며, 위와 같이 분리하면 된다.
- content는 쿼리가 복잡하지만 count 쿼리는 join 등이 필요 없어서 불필요한 작업을 더 하는 것을 방지한다.
- 즉 전체 카운트를 조회할 때, 조인 쿼리를 줄일 수 있다면 상당한 효과가 있다.
- 참고로 코드를 리펙토링해서 내용 쿼리와 전체 카운트 쿼리를 읽기 좋게 분리하면 좋다. (ctrl + alt + m)
💡 Count Query 최적화
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 new PageImpl<>(content, pageable, total);
return PageableExecutionUtils.getPage(content, pageable, () -> countQuery.fetchCount());
- 스프링 데이터 라이브러리가 제공한다.
- count 쿼리가 생략 가능한 경우 생략해서 처리한다. (제일 아래 예제 참고)
- 페이지 시작이면서 컨텐츠 사이즈가 페이지 사이즈 보다 작을 경우
- 마지막 페이지 일때 (offset + 컨텐츠 사이즈를 더해서 전체 사이즈를 구한다.)
- countQuery 자체를 처음엔 세팅만 해두고, return 할때 필요하면 query를 날린다고 보면 된다.
📌 searchPageComplex - Spring Boot 2.6 이상, Querydsl 5.0 ver으로 수정
@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 new PageImpl<>(content, pageable, total);
return PageableExecutionUtils.getPage(content, pageable, countQuery::fetchOne);
}
- PageableExecutionUtils Deprecated(향후 미지원) 패키지 변경
- import org.springframework.data.support.PageableExecutionUtils; → 패키지 변경
- 사용 패키지 위치가 변경되었습니다. 기존 위치를 신규 위치로 변경해 주시면 문제없이 사용할 수 있습니다.
- 기존 : org.springframework.data.repository.support.PageableExecutionUtils
- 신규 : org.springframework.data.support.PageableExecutionUtils
📌 컨트롤러 개발
실제 컨트롤러
@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);
}
}
결과 - postman 사용
- http://localhost:8080/v2/members?teamName=teamB&ageGoe=31&ageLoe=35&page=0&size=2
- count 쿼리 생략없이 무조건 조회
2023-01-18T14:40:33.951+09:00 DEBUG 8796 --- [nio-8080-exec-5] org.hibernate.SQL :
/* select
count(member1)
from
Member member1
left join
member1.team as team
where
team.name = ?1
and member1.age >= ?2
and member1.age <= ?3 */ select
count(m1_0.member_id)
from
member m1_0
left join
team t1_0
on t1_0.id=m1_0.team_id
where
t1_0.name=?
and m1_0.age>=?
and m1_0.age<=?
2023-01-18T14:40:33.955+09:00 DEBUG 8796 --- [nio-8080-exec-5] org.hibernate.SQL :
/* 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
where
team.name = ?1
and member1.age >= ?2
and member1.age <= ?3 */ select
m1_0.member_id,
m1_0.username,
m1_0.age,
m1_0.team_id,
t1_0.name
from
member m1_0
left join
team t1_0
on t1_0.id=m1_0.team_id
where
t1_0.name=?
and m1_0.age>=?
and m1_0.age<=? offset ? rows fetch first ? rows only
V3 결과
- http://localhost:8080/v3/members?teamName=teamB&ageGoe=31&ageLoe=100&page=0&size=110
- count 쿼리가 생략 가능한 경우 생략해서 처리한다.
2023-01-18T14:33:46.027+09:00 DEBUG 8796 --- [nio-8080-exec-2] org.hibernate.SQL :
/* 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
where
team.name = ?1
and member1.age >= ?2
and member1.age <= ?3 */ select
m1_0.member_id,
m1_0.username,
m1_0.age,
m1_0.team_id,
t1_0.name
from
member m1_0
left join
team t1_0
on t1_0.id=m1_0.team_id
where
t1_0.name=?
and m1_0.age>=?
and m1_0.age<=? offset ? rows fetch first ? rows only
참고 : 스프링 데이터 정렬(sort)
스프링 데이터 JPA는 자신의 Sort를 Querydsl의 정렬(OrderSpecifier)로 편리하게 변경하는 기능을 제공한다.
정렬( Sort )은 조건이 조금만 복잡해져도 Pageable 의 Sort 기능을 사용하기 어렵다.
루트 엔티티 범위를 넘어가는 동적 정렬 기능이 필요하면 스프링 데이터 페이징이 제공하는 Sort 를 사용하기 보다는 파라미터를 받아서 직접 처리하는 것을 권장한다.
반응형
'Java' 카테고리의 다른 글
[Java] 람다식과 작성 방법 - 함수형 인터페이스 사용 (매개변수, 반환타입) (0) | 2023.01.31 |
---|---|
QuerydslPredicateExecutor - querydsl 조건 조회 간단히 사용하기 (0) | 2023.01.18 |
순수 JPA를 Spring data JPA로 변경하는 법 (0) | 2023.01.18 |
[spring] 환경에 따른 설정 파일 나누기 - application.yml/@Profile (0) | 2023.01.17 |
[Querydsl] 동적 쿼리와 성능 최적화 조회 - where 조건 절 파라미터 (0) | 2023.01.17 |