김영한 님의 실전! Querydsl 강의를 보고 작성한 내용입니다.
Spring Data JPA 를 사용하면 findByUsername 과 같은 쿼리를 자동으로 생성해주지만, 검색 조건에 따른 동적 쿼리를 작성할 수 없습니다. 그래서 사용자 정의 인터페이스를 사용하여 QueryDSL 을 활용한 동적 쿼리를 작성합니다.
public interface MemberRepositoryCustom {
List<MemberTeamDto> search(MemberSearchCond condition);
}
public interface MemberRepository extends JpaRepository<Member, Long>,
MemberRepositoryCustom {
...
}
JpaRepository 를 상속 받는 인터페이스가 새롭게 정의한 인터페이스를 상속 받도록 합니다. 이렇게 하면 생성한 동적 쿼리를 MemberRepository.search()
로 사용할 수 있습니다.
public class MemberRepositoryCustomImpl implements MemberRepositoryCustom {
private final JPAQueryFactory queryFactory;
public MemberRepositoryCustomImpl(EntityManager em) {
this.queryFactory = new JPAQueryFactory(em);
}
@Override
public List<MemberTeamDto> search(MemberSearchCond condition) {
...
}
}
사용자 정의 인터페이스의 구현체를 만들 때 MemberRepositoryImpl 혹은 MemberRepositoryCustomImpl 둘 중 아무거나 선택해서 이름을 지정하면 됩니다.
( Spring Data JPA 강의 참고 )
QueryDSL 을 활용하기 때문에 생성자를 통해 EntityManager 를 주입받아 JpaQueryFactory 를 생성하고, 이를 이용해 동적 쿼리를 작성하면 됩니다. 동적 쿼리는 이전 시간에 작성한 것과 동일하게 작성하면 됩니다.
fetchResult()
를 사용하면 데이터를 가져오는 쿼리와 총 데이터 수를 가져오는 쿼리가 실행되는데 해당 기능은 deprecated 되었습니다.
그래서 QueryDSL 5.0 부터 페이징 처리를 할 때는 데이터를 조회하는 쿼리, 데이터 수를 가져오는 쿼리를 따로 작성해서 구현해야 합니다.
또 content 를 가져올 때는 조인이 필요하지만 count 를 가져올 때는 조인 여부에 관계 없이 데이터의 수가 동일한 경우가 있습니다. 이러한 이유로 count 쿼리를 따로 작성하는 것이 성능 상 이점을 가져갈 수 있습니다.
public class MemberRepositoryCustomImpl implements MemberRepositoryCustom {
...
@Override
public Page<MemberTeamDto> searchPageComplex(MemberSearchCond condition,
Pageable pageable) {
// content 쿼리
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();
// count 쿼리
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()))
.fetchOne();
return new PageImpl<>(content, pageable, total);
}
}
content 를 가져올 때는 fetch()
를 사용합니다.
count 를 가져올 때는 select 에 count() 를 사용하며, fetchOne()
으로 실행합니다.
페이지의 시작이면서 content 사이즈가 page 사이즈보다 작은 경우, 혹은 마지막 페이지인 경우에는 count 쿼리를 생략할 수 있습니다.
Spring Data 가 제공하는 라이브러리를 사용하면 count 쿼리를 생략할 수 있는 경우, 자동으로 count 쿼리를 생략합니다.
public class MemberRepositoryCustomImpl implements MemberRepositoryCustom {
...
@Override
public Page<MemberTeamDto> searchPageComplex(MemberSearchCond condition,
Pageable pageable) {
// content 쿼리 생략
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);
}
}
count 쿼리를 작성한 후에 fetchXXX
를 사용하지 않으면 실제 쿼리가 수행되지 않고 fetchXXX
를 호출해야 쿼리가 실행됩니다.
count 쿼리를 반환받고, 위처럼 반환하면 getPage()
에서 content 와 pageable 의 totalSize 를 보고 count 쿼리를 생략할 수 있다면 count 쿼리를 실행하지 않게 됩니다.
여기서 소개하는 기능은 제약이 커서 복잡한 실무 환경에서 사용하기에는 많이 부족하다고 합니다.
public interface QuerydslPredicateExecutor<T> {
Optional<T> findOne(Predicate predicate);
Iterable<T> findAll(Predicate predicate);
Page<T> findAll(Predicate predicate, Pageable pageable);
long count(Predicate predicate);
boolean exists(Predicate predicate);
...
}
@Test
void querydslPredicateExecutor() {
QMember member = QMember.member;
Iterable<Member> result = memberRepository.findAll(
member.age.between(10, 40)
.and(member.username.eq("member1")));
}
JpaRepository 를 상속 받는 인터페이스에서 해당 인터페이스를 상속 받으면 인터페이스가 제공하는 모든 기능을 사용할 수 있으며, 파라미터로 QueryDSL 조건을 넣을 수 있게 됩니다.
Pagable, Sort를 모두 지원하고 정상적으로 동작하지만 left join 이 불가능하며, 서비스 클래스가 QueryDSL 이라는 구현 기술에 의존해야 합니다.
public class MemberRepositoryCustomImpl extends QuerydslRepositorySupport
implements MemberRepositoryCustom {
public MemberRepositoryCustomImpl() {
super(Member.class);
}
@Override
public List<MemberTeamDto> search(MemberSearchCond condition) {
return from(member)
.leftJoin(member.team, team)
.where(
usernameEq(condition.getUsername()),
teamNameEq(condition.getTeamName()),
ageGoe(condition.getAgeGoe()),
ageLoe(condition.getAgeLoe())
)
.select(new QMemberTeamDto(
member.id,
member.username,
member.age,
team.id,
team.name
))
.fetch();
}
}
기존에는 EntityManager 를 주입 받고 JpaQueryFactory 를 생성했는데 해당 인터페이스가 엔티티 매니저를 주입 받기 때문에 super(Member.class)
만 하면 됩니다.
JpaQueryFactory 없이 from
으로 시작하도록 하고 마지막에 select
를 넣는 형식으로 쿼리를 작성할 수 있습니다.
public class MemberRepositoryCustomImpl extends QuerydslRepositorySupport {
private final JPAQueryFactory queryFactory;
// queryFactory 사용
public MemberRepositoryCustomImpl(EntityManager em) {
super(Member.class);
this.queryFactory = new JPAQueryFactory(em);
}
// entityManager
private final EntityManager entityManager = getEntityManager();
}
생성자에서 EntityManager 를 주입 받아 JpaQueryFactory 를 사용할 수 있으며, getEntityManager()
를 통해 엔티티 매니저를 사용할 수 있습니다.
@Override
public void searchPage(MemberSearchCond condition, Pageable pageable) {
JPQLQuery<MemberTeamDto> query =
from(member)
.leftJoin(member.team, team)
.where(
usernameEq(condition.getUsername()),
teamNameEq(condition.getTeamName()),
ageGoe(condition.getAgeGoe()),
ageLoe(condition.getAgeLoe()))
.select(new QMemberTeamDto(
member.id,
member.username,
member.age,
team.id,
team.name
));
JPQLQuery<MemberTeamDto> pagingQuery = getQuerydsl()
.applyPagination(pageable, query);
List<MemberTeamDto> result = pagingQuery.fetch();
}
getQuerydsl().applyPagination()
을 사용하면 Spring Data 가 제공하는 페이징을 QueryDSL 로 편리하게 변환할 수 있으며, offset 과 limit 를 자동으로 넣어주게 됩니다.
이 메서드로 스프링 데이터가 제공하는 페이징을 Querydsl로 편리하게 변환할 수 있지만 Sort는 오류가 발생합니다.
Querydsl 3.x 버전을 대상으로 만들었기 때문에 Querydsl 4.x에 나온 JPAQueryFactory로 시작할 수 없습니다.
Spring Data 가 제공하는 Sort 기능이 정상적으로 동작하지 않습니다.