실무에서 사용하기
실무에서 사용하기 위해 다음과 같은 설계를 합니다
MemberRepository interface는 JpaRepository 이외에도 MemberRepositoryCustm interface를 상속합니다.
MemberRepositoryCustom interface는 search()라는 메서드가 있는데 이는 MemberRepositoryImpl에서 상속하여 구현합니다. 여기서 Querydsl이 사용됩니다.
개발자가 개발한 쿼리문이 사용되는 곳에는 Custom을 붙입니다.
또한 직접 구현하는 클래스 뒤에 Impl이 붙습니다.
*MemberRepository
public interface MemberRepository extends JpaRepository<Member, Long>, MemberRepositoryCustom {
List<Member> findByUsername(String username);
}
*MemberRepositoryCustom
public interface MemberRepositoryCustom {
List<MemberTeamDto> search(MemberSearchCondition condition);
}
*MemberRepositoryImpl
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);
}
}
하지만 모든 사용자 쿼리를 Custom에 담는 것이 정답은 아닙니다. 특정 화면의 특화된 쿼리의 경우에 그냥 @Repository로 정의한 클래스에서 바로 조회하도록 하는 방법도 있습니다.
@Repository
public class MemberQueryRepository {
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);
}
}
Querydsl 페이징 연동
*MemberRepositoryCustom
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);
}
페이징 관련으로 Pageable이 사용됩니다.
1. offset은 어디 index부터 시작할 것인가, limit은 제한을 몇번까지 할 것인가 정하는 것입니다.
2. fetchResults()로 조회하기 때문에 카운트쿼리도 나가기 때문에 2번의 쿼리가 나갑니다.
3. 리턴형은 Page이며 PageImpl을 통해서 페이징 결과를 리턴합니다.
searchPageSimple을 테스트합니다.
@Test
void searchPageSimpleTest(){
MemberSearchCondition condition = new MemberSearchCondition();
PageRequest pageRequest = PageRequest.of(0, 3);
Page<MemberTeamDto> result = memberRepository.searchPageSimple(condition, pageRequest);
Assertions.assertEquals(result.getSize(), 3);
assertThat(result.getContent()).extracting("username").containsExactly("member0","member1","member2");
}
쿼리는 총 2번 나갑니다.
조회쿼리와 프로젝션 조회를 2번에 걸쳐서 나눌 수도 있습니다. 왜냐하면 fetchResults()는 모든 join이 다 붙어버리기 때문에, 최적화를 위해서는 나눠서 조회해야 합니다. 먼저 count를 확인하고 결과가 있을 때만 fetch()로 조회하는 것도 하나의 방법입니다. 데이터가 몇천만 개 정도로 많을 때는 분리합니다.
@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);
}
CountQuery 최적화
count 쿼리가 생략 가능한 경우 생략해서 처리합니다.
- 페이지 시작이면서 컨텐츠 사이즈가 페이지 사이즈보다 작을 때
- 마지막 페이지 일 때 (offset + 컨텐츠 사이즈를 더해서 전체 사이즈 구함)
시작 페이지가 100인데 결과가 3개밖에 없다면, 그냥 3개만 count로 넣습니다.
@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);
return PageableExecutionUtils.getPage(content, pageable, countQuery::fetchCount);
}
컨트롤러 개발
컨트롤러 계층에서 바로 repository로 조회를 할 수 있습니다.
@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);
}
}
* 출처
Inflearn : 실전! Querydsl 강의
'Spring > Querydsl' 카테고리의 다른 글
순수 JPA와 Querydsl (0) | 2022.02.13 |
---|---|
QueryDsl 수정, 삭제 (0) | 2022.02.13 |
Querydsl 동적쿼리 (0) | 2022.02.13 |
QueryDsl 프로젝션 문법 (0) | 2022.02.13 |
QueryDsl 기본문법 (0) | 2022.02.12 |