내가 공부한 것을 올리며, 중요한 단원은 저 자신도 곱씹어 볼 겸 상세히 기록하고 얕은 부분들은 가볍게 포스팅하겠습니다.
1. 인터페이스 지원 - QuerydslPredicateExecutor
여기서 소개하는 기능은 제약이 커서 복잡한 실무 환경에서 사용하기에는 많이 부족하다.
그래도 Spring Data 에서 제공하는 기능이므로 간단히 소개하고, 왜 부족한지 설명하겠다.
1-1) Interface 지원 - QuerydslPredicateExecutor
기존의 repository에 다음과 같이 QuerydslPredicateExecutor<Member>를 추가로 상속하게 되었다.
public interface MemberRepository extends JpaRepository<Member, Long>, MemberCustomRepository, QuerydslPredicateExecutor<Member> {
List<Member> findByUsername(String username);
}
이를 사용하는 테스트 코드는 다음과 같다.
@Test
public void querydsl_predicate_executor_test() {
// given
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", 20, 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);
// then
QMember member = QMember.member;
Iterable<Member> results = memberRepository.findAll(member.age.between(10, 40).and(member.username.eq("member1")));
for (Member result : results) {
System.out.println("result = " + result);
}
}
생성되는 쿼리는 다음과같다.
select
member0_.member_id as member_i1_1_,
member0_.age as age2_1_,
member0_.team_id as team_id4_1_,
member0_.username as username3_1_
from
member member0_
where
(member0_.age between ? and ?) and member0_.username=?
1-2) 한계점
- 조인X (묵시적 조인은 가능하지만 left join이 불가능하다.)
- 클라이언트가 Querydsl에 의존해야 한다.
- 서비스 클래스가 Querydsl이라는 구현 기술에 의존해야 한다. 복잡한 실무환경에서 사용하기에는 한계가 명확하다.
테이블이 작은거면 모를까 실무에서는 권장하지 않는 방식이다!
2. Querydsl Web 지원
https://docs.spring.io/spring-data/jpa/docs/2.2.3.RELEASE/reference/html/#core.web.type-safe
▶ 한계점
- 단순한 조건만 가능
- 조건을 커스텀하는 기능이 복잡하고 명시적이지 않음
- 컨트롤러가 Querydsl에 의존
- 복잡한 실무환경에서 사용하기에는 한계가 명확
3. 리포지토리 지원 - QuerydslRepositorySupport
▶ 장점
- getQuerydsl().applyPagination() 스프링 데이터가 제공하는 페이징을 Querydsl로 편리하게 변환 가능(단! Sort는 오류발생)
- from() 으로 시작 가능(최근에는 QueryFactory를 사용해서 select() 로 시작하는 것이 더 명시적)
- EntityManager 제공
▶ 한계
- Querydsl 3.x 버전을 대상으로 만듬
- Querydsl 4.x에 나온 JPAQueryFactory로 시작할 수 없음
- select로 시작할 수 없음 (from으로 시작해야함) QueryFactory 를 제공하지 않음
- 스프링 데이터 Sort 기능이 정상 동작하지 않음
다음시간에는 이러한 한계를 극복하도록 직접 구현해보자.
4. Querydsl 지원 클래스 직접 만들기
Spring Data 가 제공하는 QuerydslRepositorySupport 가 지닌 한계를 극복하기 위해 직접 Querydsl 지원 클래스를 만들어보자.
4-1) Querydsl4RepositorySupport
우선 이번에 사용할 Querydsl4RepositorySupport 코드는 다음고 같다.
@Repository
public abstract class Querydsl4RepositorySupport {
private final Class domainClass;
private Querydsl querydsl;
private EntityManager entityManager;
private JPAQueryFactory queryFactory;
public Querydsl4RepositorySupport(Class<?> domainClass) {
Assert.notNull(domainClass, "Domain class must not be null!");
this.domainClass = domainClass;
}
@Autowired
public void setEntityManager(EntityManager entityManager) {
Assert.notNull(entityManager, "EntityManager must not be null!");
JpaEntityInformation entityInformation = JpaEntityInformationSupport.getEntityInformation(domainClass, entityManager);
SimpleEntityPathResolver resolver = SimpleEntityPathResolver.INSTANCE;
EntityPath path = resolver.createPath(entityInformation.getJavaType());
this.entityManager = entityManager;
this.querydsl = new Querydsl(entityManager, new PathBuilder<>(path.getType(), path.getMetadata()));
this.queryFactory = new JPAQueryFactory(entityManager);
}
@PostConstruct
public void validate() {
Assert.notNull(entityManager, "EntityManager must not be null!");
Assert.notNull(querydsl, "Querydsl must not be null!");
Assert.notNull(queryFactory, "QueryFactory must not be null!");
}
protected JPAQueryFactory getQueryFactory() {
return queryFactory;
}
protected Querydsl getQuerydsl() {
return querydsl;
}
protected EntityManager getEntityManager() {
return entityManager;
}
protected <T> JPAQuery<T> select(Expression<T> expr) {
return getQueryFactory().select(expr);
}
protected <T> JPAQuery<T> selectFrom(EntityPath<T> from) {
return getQueryFactory().selectFrom(from);
}
protected <T> Page<T> applyPagination(Pageable pageable, Function<JPAQueryFactory, JPAQuery> contentQuery) {
JPAQuery jpaQuery = contentQuery.apply(getQueryFactory());
List<T> content = getQuerydsl().applyPagination(pageable, jpaQuery).fetch();
return PageableExecutionUtils.getPage(content, pageable, jpaQuery::fetchCount);
}
protected <T> Page<T> applyPagination(Pageable pageable,
Function<JPAQueryFactory, JPAQuery> contentQuery, Function<JPAQueryFactory,
JPAQuery> countQuery) {
JPAQuery jpaContentQuery = contentQuery.apply(getQueryFactory());
List<T> content = getQuerydsl().applyPagination(pageable, jpaContentQuery).fetch();
JPAQuery countResult = countQuery.apply(getQueryFactory());
return PageableExecutionUtils.getPage(content, pageable, countResult::fetchCount);
}
}
위 코드를 상속하여 사용하는 MemberTestRepository를 하나 만들어 보자.
4-2) Querydsl4RepositorySupport의 applyPagination 기능을 적용시키기 전 (V1)
우선 Querydsl4RepositorySupport의 applyPagination 기능을 적용시키기 전의 코드는 다음과같다.
public Page<Member> applyPaginationV1(MemberSearchCondition condition, Pageable pageable) {
JPAQuery<Member> query = selectFrom(member)
.leftJoin(member.team, team)
.where(usernameEq(condition.getUsername()),
teamNameEq(condition.getTeamName()),
ageGoe(condition.getAgeGoe()),
ageLoe(condition.getAgeLoe())
);
List<Member> content = getQuerydsl().applyPagination(pageable, query).fetch();
return PageableExecutionUtils.getPage(content, pageable, query::fetchCount);
}
쿼리가 한번에 메서드 체인으로 연결되면서 진행되는 방식이 아니라 매우 아쉽다.
결과를 우선 JPAQuery<Member> query로 받은 후, 해당 query를 다시 applyPagination() 의 인자로 넘기고 있다.
4-3) Querydsl4RepositorySupport의 applyPagination 기능을 적용시킨 후 (V2)
이를 좀더 깔끔하게 변경한 버전은 다음과 같다.
public Page<Member> applyPaginationV2(MemberSearchCondition condition, Pageable pageable) {
return applyPagination(pageable, query -> query
.selectFrom(member)
.leftJoin(member.team, team)
.where(usernameEq(condition.getUsername()),
teamNameEq(condition.getTeamName()),
ageGoe(condition.getAgeGoe()),
ageLoe(condition.getAgeLoe())
)
);
}
두번째 인자로 람다식을 전달하게 되었다.
이를 다음 applyPagination의 두번째 인자로 전달받아 사용하게 된다.
protected <T> Page<T> applyPagination(Pageable pageable, Function<JPAQueryFactory, JPAQuery> contentQuery) {
JPAQuery jpaQuery = contentQuery.apply(getQueryFactory());
List<T> content = getQuerydsl().applyPagination(pageable, jpaQuery).fetch();
return PageableExecutionUtils.getPage(content, pageable, jpaQuery::fetchCount);
}
사실 그냥 applyPaginationV1 버전을 한번더 wrapping한 것 이다.
4-4) Querydsl4RepositorySupport의 applyPagination 기능 + CountQuery 기능 (V3)
이번에는 아예 count 쿼리까지 전달하는 V3를 만들어 보자.
public Page<Member> applyPaginationV3(MemberSearchCondition condition, Pageable pageable) {
return applyPagination(pageable,
contentQuery -> contentQuery
.selectFrom(member)
.leftJoin(member.team, team)
.where(usernameEq(condition.getUsername()),
teamNameEq(condition.getTeamName()),
ageGoe(condition.getAgeGoe()),
ageLoe(condition.getAgeLoe()))
, countQuery -> countQuery
.select(Wildcard.count)
.from(member)
.leftJoin(member.team, team)
.where(usernameEq(condition.getUsername()),
teamNameEq(condition.getTeamName()),
ageGoe(condition.getAgeGoe()),
ageLoe(condition.getAgeLoe()))
);
}
4-5) 전체 MemberTestRepository 코드
이를 사용한 전체 Repository 코드는 다음과 같다.
@Repository
public class MemberTestRepository extends Querydsl4RepositorySupport {
public MemberTestRepository() {
super(Member.class);
}
public List<Member> basicSelect() {
return select(member)
.from(member)
.fetch();
}
public List<Member> basicSelectFrom() {
return selectFrom(member).fetch();
}
public Page<Member> applyPaginationV1(MemberSearchCondition condition, Pageable pageable) {
JPAQuery<Member> query = selectFrom(member)
.leftJoin(member.team, team)
.where(usernameEq(condition.getUsername()),
teamNameEq(condition.getTeamName()),
ageGoe(condition.getAgeGoe()),
ageLoe(condition.getAgeLoe())
);
List<Member> content = getQuerydsl().applyPagination(pageable, query).fetch();
return PageableExecutionUtils.getPage(content, pageable, query::fetchCount);
}
public Page<Member> applyPaginationV2(MemberSearchCondition condition, Pageable pageable) {
return applyPagination(pageable, query -> query
.selectFrom(member)
.leftJoin(member.team, team)
.where(usernameEq(condition.getUsername()),
teamNameEq(condition.getTeamName()),
ageGoe(condition.getAgeGoe()),
ageLoe(condition.getAgeLoe())
)
);
}
public Page<Member> applyPaginationV3(MemberSearchCondition condition, Pageable pageable) {
return applyPagination(pageable,
contentQuery -> contentQuery
.selectFrom(member)
.leftJoin(member.team, team)
.where(usernameEq(condition.getUsername()),
teamNameEq(condition.getTeamName()),
ageGoe(condition.getAgeGoe()),
ageLoe(condition.getAgeLoe()))
, countQuery -> countQuery
.select(Wildcard.count)
.from(member)
.leftJoin(member.team, team)
.where(usernameEq(condition.getUsername()),
teamNameEq(condition.getTeamName()),
ageGoe(condition.getAgeGoe()),
ageLoe(condition.getAgeLoe()))
);
}
private BooleanBuilder usernameEq(String username) {
if (StringUtils.hasText(username)) {
return new BooleanBuilder(member.username.eq(username));
}
return new BooleanBuilder();
}
private BooleanBuilder teamNameEq(String teamName) {
if (StringUtils.hasText(teamName)) {
return new BooleanBuilder(team.name.eq(teamName));
}
return new BooleanBuilder();
}
private BooleanBuilder ageGoe(Integer ageGoe) {
if (ageGoe != null) {
return new BooleanBuilder(member.age.goe(ageGoe));
}
return new BooleanBuilder();
}
private BooleanBuilder ageLoe(Integer ageLoe) {
if (ageLoe != null) {
return new BooleanBuilder(member.age.loe(ageLoe));
}
return new BooleanBuilder();
}
}
'BackEnd > JPA' 카테고리의 다른 글
[JPA] SpringBoot 2.5 이후부터 data.sql 초기화 시점 (0) | 2022.06.06 |
---|---|
[JPA] JSON 직렬화 순환 참조 해결하기 (0) | 2022.06.05 |
[JPA] 실무 활용 - Spring Data JPA와 Querydsl (0) | 2022.05.15 |
[JPA] 실무 활용 - 순수 JPA와 Querydsl (0) | 2022.05.15 |
[JPA] QueryDSL 중급문법 - 2 (0) | 2022.05.14 |
댓글