반응형
250x250
Notice
Recent Posts
Recent Comments
Link
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 |
Tags
- 스프링
- 서브쿼리
- 스프링 컨테이너
- 데이터모델링
- 친절한 SQL 튜닝
- 페치조인
- 정렬
- 자바의 정석
- querydsl
- TCP/IP
- 오라클
- index
- 엔티티
- Spring
- 성능최적화
- INDEX SCAN
- SQL 튜닝
- 값 타입
- @MappedSuperclass
- JPA
- JPQL
- 그룹함수
- Spring data JPA
- SQL
- 데이터베이스
- fetch join
- 컬렉션 조회 최적화
- 페이징
- 스프링 데이터 JPA
- DTO
Archives
- Today
- Total
nu_s
[QueryDSL] 스프링 데이터 JPA & QueryDSL 🦋 본문
728x90
반응형
1. 사용자 정의 리포지토리
사용자 정의 리포지토리 사용법
- 사용자 정의 인터페이스 작성
- 사용자 정의 인터페이스 구현
- 스프링 데이터 리포지토리에 사용자 정의 인터페이스 상속
사용자 정의 리포지토리 구성
스프링 데이터 JPA - MemberRepository 생성
public interface MemberRepository extends JpaRepository<Member, Long> {
List<Member> findByUsername(String username);
}
1. 사용자 정의 인터페이스 작성
public interface MemberRepositoryCustom {
List<MemberTeamDto> search(MemberSearchCondition condition);
}
2. 사용자 정의 인터페이스 구현
public class MemberRepositoryImpl implements MemberRepositoryCustom {
private final JPAQueryFactory queryFactory;
public MemberRepositoryImpl(EntityManager em) {
this.queryFactory = new JPAQueryFactory(em);
}
@Override
//회원명, 팀명, 나이(ageGoe, ageLoe)
public List<MemberTeamDto> search(MemberSearchCondition condition) {
return 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()))
.fetch();
}
private BooleanExpression usernameEq(String username) {
return isEmpty(username) ? null : member.username.eq(username);
}
private BooleanExpression teamNameEq(String teamName) {
return isEmpty(teamName) ? null : team.name.eq(teamName);
}
private BooleanExpression ageGoe(Integer ageGoe) {
return ageGoe == null ? null : member.age.goe(ageGoe);
}
private BooleanExpression ageLoe(Integer ageLoe) {
return ageLoe == null ? null : member.age.loe(ageLoe);
}
}
3. 스프링 데이터 리포지토리에 사용자 정의 인터페이스 상속
public interface MemberRepository extends JpaRepository<Member, Long>, MemberRepositoryCustom {
List<Member> findByUsername(String username);
}
2. 스프링 데이터 페이징 활용1 - Querydsl 페이징 연동
- 스프링 데이터의 Page, Pageable을 활용
- 전체 카운트를 한번에 조회하는 단순한 방법
- 데이터 내용과 전체 카운트를 별도로 조회하는 방법
사용자 정의 인터페이스에 페이징 2가지 추가
public interface MemberRepositoryCustom {
List<MemberTeamDto> search(MemberSearchCondition condition);
Page<MemberTeamDto> searchPageSimple(MemberSearchCondition condition, Pageable pageable);
Page<MemberTeamDto> searchPageComplex(MemberSearchCondition condition, Pageable pageable);
}
전체 카운트를 한번에 조회하는 단순한 방법
/**
* 단순한 페이징, fetchResults() 사용
*/
@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);
}
- Querydsl이 제공하는 fetchResults()를 사용하면 내용과 전체 카운트를 한번에 조회할 수 있다. (쿼리 2번 호출)
- fetchResults()는 카운트 쿼리 실행시 필요없는 order by는 제거한다.
데이터 내용과 전체 카운트를 별도로 조회하는 방법
/**
* 복잡한 페이징
* 데이터 조회 쿼리와, 전체 카운트 쿼리를 분리
*/
@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);
}
- 전체 카운트를 조회하는 방법을 최적화 할 수 있으면 이렇게 분리하면 된다.
- 코드를 리팩토링해서 내용 쿼리와 전체 카운트 쿼리를 읽기 좋게 분리하면 좋다.
스프링 데이터 페이징 활용2 - CountQuery 최적화
PageableExecutionUtils.getPage()로 최적화
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 new PageImpl<>(content, pageable, total);
return PageableExecutionUtils.getPage(content, pageable, countQuery::fetchCount);
- 스프링 데이터 라이브러리가 제공
- count 쿼리가 생략 가능한 경우 생략해서 처리
- 페이지 시작이면서 컨텐츠 사이즈가 페이지 사이즈보다 작을 때
- 마지막 페이지 일 때 (offset + 컨텐츠 사이즈를 더해서 전체 사이즈를 구함, 더 정확히는 마지막 페이지이면서 컨텐츠 사이즈가 페이지 사이즈보다 작을 때)
참고
- 최신 스프링 부트 3.X(2.6 이상)은 Querydsl 5.0을 사용한다.
- 최신 버전에서는 fetchResults(), fetchCount()가 Deprecated(향후 미지원) 되었다.
- 따라서 count 쿼리는 위와 같이 사용하지 말고 다음과 같이 사용하자.
count 쿼리
@Test
public void count() {
Long totalCount = queryFactory
// .select(Wildcard.count) // select count(*)
.select(member.count()) // select count(member.id)
.from(member)
.fetchOne();
}
- count(*)를 사용하고 싶으면 Wildcard.count를 사용하면 된다.
- member.count()를 사용하면 count(member.id)로 처리된다.
- 응답 결과는 숫자 하나이므로 fetchOne()을 사용한다.
- 이것을 참고해서 searchPageComplex 메서드를 수정해보자.
수정된 searchPageComplex
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);
}
스프링 데이터 페이징 활용3 - 컨트롤러 개발
MemberController
@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);
}
}
스프링 데이터 정렬(Sort)
- 스프링 데이터 JPA는 자신의 정렬을 Querydsl의 정렬(OrderSpecifier)로 편하게 변경하는 기능을 제공한다.
스프링 데이터 Sort를 Querydsl의 OrderSpeicifier로 변환
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();
- 정렬은 조건이 조금만 복잡해져도 Pageable의 Sort 기능을 사용하기 어렵다.
- 루트 엔티티 범위를 넘어가는 동적 정렬 기능이 필요하면 파라미터를 받아서 직접 처리하는 것이 좋다.
출처 : 인프런 실전! Querydsl
728x90
반응형
'QueryDSL' 카테고리의 다른 글
[QueryDSL] 스프링 데이터 JPA가 제공하는 QueryDSL 기능 🦋 (0) | 2024.02.26 |
---|---|
[QueryDSL] 순수 JPA & QueryDSL 🦋 (0) | 2024.02.08 |
[QueryDSL] 벌크 연산 & SQL function 🦋 (0) | 2024.02.03 |
[QueryDSL] 동적 쿼리 🦋 (0) | 2024.02.01 |
[QueryDSL] 프로젝션과 결과 반환 🦋 (0) | 2024.01.30 |