스프링 데이터 JPA가 제공하는 Querydsl 기능

LeeKyoungChang·2022년 5월 9일
0
post-thumbnail

Querydsl 수업을 듣고 정리한 내용입니다.

 

지금부터 소개하는 기능은 제약이 커서 복잡한 실무 환경에서 사용하기는 많이 부족하다!
그래도 스프링 데이터에 제공하는 기능이므로 어떤 기능인지 간단히 알아보고, 어떤 점이 부족한지 알아본다.

 

📚 1. 인터페이스 지원 - QuerydslPredicateExecutor

공식 URL

 

QuerydslPredicateExecutor 인터페이스

public interface QuerydslPredicateExecutor<T> {

    Optional<T> findById(Predicate predicate); // (1)
    
    Iterable<T> findAll(Predicate predicate); // (2)
    
    long count(Predicate predicate); // (3)
    
    boolean exists(Predicate predicate); // (4)
    
    // … more functionality omitted.
    
}

(1) : predicate와 일치하는 단일 엔티티를 찾아서 반환한다.
(2) : predicate와 일치하는 모든 항목을 찾아서 반환한다.
(3) : predicate와 일치하는 엔티티의 수를 반환한다.
(4) : predicate와 일치하는 엔티티가 있는지 여부를 반환한다.

 

✔️ 리포지토리에 적용

Querydsl 지원을 사용하려면 다음과 같이 리포지토리 인터페이스에 extend를 해주자!

interface MemberRepository extends JpaRepository<User, Long>, QuerydslPredicateExecutor<User> {
}

 

Iterable result = memberRepository.findAll(
        member.age.between(10, 40)
                .and(member.username.eq("member1"))
);

 

✔️ 한계점

  • 조인이 되지 않는다. (묵시적 조인은 가능하지만 left join이 불가능하다.)
  • 클라이언트가 Querydsl에 의존해야 한다. 서비스 클래스가 Querydsl이라는 구현 기술에 의존해야 한다.
  • 복잡한 실무환경에서 사용하기에는 한계가 명확하다.

 

MemberRepositoryTest에 테스트 추가

  
@Test  
public void querydslPredicateExecutorTest() {  
  
    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);  
  
  
    QMember member = QMember.member;  
    Iterable<Member> result = memberRepository.findAll(  
            member.age  
                    .between(10, 40)  
                    .and(member.username.eq("member1"))  
    );  
    for (Member findMember : result) {  
        System.out.println("member1 = " + findMember);  
    }  
}

 

실행 결과

 

💡 참고
QuerydslPredicateExecutor는 Pageable, Sort를 모두 지원하고 정상 동작한다.

 

📚 2. Querydsl Web 지원

공식 URL

 

✔️ 한계점

  • 단순한 조건만 가능하다.
  • 조건을 커스텀하는 기능이 복잡하고 명시적이지 않다.
  • 컨트롤러가 Querydsl에 의존한다.
  • 복잡한 실무환경에서 사용하기에는 한계가 명확하다.

➡️ Querydsl Web은 쓰지 말자!

 

📚 3. 리포지토리 지원 - QuerydslRepositorySupport

✔️ 장점

  • getQuerydsl().applyPagination() 스프링 데이터가 제공하는 페이징을 Querydsl로 편리하게 변환 가능하다. (단! Sort는 오류발생)
  • from() 으로 시작하는 것이 가능하다. (최근에는 QueryFactory를 사용해서 select() 로 시작하는 것이 더 명시적이다.)
  • EntityManager를 제공한다.

 

✔️ 한계

  • Querydsl 3.x 버전을 대상으로 만들어서 Querydsl 4.x에 나온 JPAQueryFactory로 시작할 수 없다.
    • select로 시작할 수 없다. from으로 시작해야 한다.
  • QueryFactory를 제공하지 않는다.
  • 스프링 데이터 Sort 기능이 정상 동작하지 않는다.

 

📚 4. Querydsl 지원 클래스 직접 만들기

스프링 데이터가 제공하는 QuerydslRepositorySupport가 지닌 한계를 극복하기 위해 직접 Querydsl 지원 클래스를 만들어보자!

 

✔️ 장점

  • 스프링 데이터가 제공하는 페이징을 편리하게 변환한다.
  • 페이징과 카운트 쿼리 분리 가능하다.
  • 스프링 데이터 Sort 지원한다.
  • select(), selectFrom() 으로 시작 가능하다.
  • EntityManager, QueryFactory 제공한다.

 

Querydsl4RepositorySupport

package study.querydsl.repository.support;  
  
import com.querydsl.core.types.EntityPath;  
import com.querydsl.core.types.Expression;  
import com.querydsl.core.types.dsl.PathBuilder;  
import com.querydsl.jpa.impl.JPAQuery;  
import com.querydsl.jpa.impl.JPAQueryFactory;  
import org.springframework.beans.factory.annotation.Autowired;  
import org.springframework.data.domain.Page;  
import org.springframework.data.domain.Pageable;  
import org.springframework.data.jpa.repository.support.JpaEntityInformation;  
import org.springframework.data.jpa.repository.support.JpaEntityInformationSupport;  
import org.springframework.data.jpa.repository.support.Querydsl;  
import org.springframework.data.querydsl.SimpleEntityPathResolver;  
import org.springframework.data.support.PageableExecutionUtils;  
import org.springframework.stereotype.Repository;  
import org.springframework.util.Assert;  
  
import javax.annotation.PostConstruct;  
import javax.persistence.EntityManager;  
import java.util.List;  
import java.util.function.Function;  
  
@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);  
    }  
}

 

Querydsl4RepositorySupport 사용 코드

package study.querydsl.repository;  
  
import com.querydsl.core.types.dsl.BooleanExpression;  
import com.querydsl.jpa.impl.JPAQuery;  
import org.springframework.data.domain.Page;  
import org.springframework.data.domain.Pageable;  
import org.springframework.data.support.PageableExecutionUtils;  
import org.springframework.stereotype.Repository;  
import study.querydsl.dto.MemberSearchCondition;  
import study.querydsl.entity.Member;  
import study.querydsl.repository.support.Querydsl4RepositorySupport;  
  
import static org.springframework.util.StringUtils.hasText;  
import static study.querydsl.entity.QMember.*;  
import static study.querydsl.entity.QTeam.team;  
  
import java.util.List;  
  
@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> searchPageByApplyPage(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> applyPagination(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> applyPagination2(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(member.id)  
                .from(member)  
                .leftJoin(member.team, team)  
                .where(usernameEq(condition.getUsername()),  
                        teamNameEq(condition.getTeamName()),  
                        ageGoe(condition.getAgeGoe()),  
                        ageLoe(condition.getAgeLoe()))  
        );  
    }  
  
    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;  
    }  
  
}

 

📚 5. 이외

📖 A. PageableExecutionUtils Deprecated(향후 미지원) 패키지 변경

PageableExecutionUtils 클래스 사용 패키지 변경
기능이 Deprecated 된 것은 아니고, 사용 패키지 위치가 변경되었습니다. 기존 위치를 신규 위치로 변경해주시면 문제 없이 사용할 수 있다.

  • 기존 : org.springframework.data.repository.support.PageableExecutionUtils
  • 신규 : org.springframework.data.support.PageableExecutionUtils

 

📖 B. Querydsl fetchResults(), fetchCount() Deprecated(향후 미지원)

Querydsl의 fetchCount(), fetchResult()는 개발자가 작성한 select 쿼리를 기반으로 count용 쿼리를 내부에서 만들어서 실행합니다.
그런데 이 기능은 강의에서 설명드린 것 처럼 select 구문을 단순히 count 처리하는 용도로 바꾸는 정도입니다. 따라서 단순한 쿼리에서는 잘 동작하지만, 복잡한 쿼리에서는 제대로 동작하지 않습니다. Querydsl은 향후 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() 을 사용합니다.

 

MemberRepositoryImpl.searchPageComplex() 예제에서 보여드린 것처럼 select 쿼리와는 별도로 count 쿼리를 작성하고 fetch() 를 사용해야 합니다. 다음은 최신 버전에 맞추어 수정된 예제입니다.

수정된 searchPageComplex 예제

import org.springframework.data.support.PageableExecutionUtils; //패키지 변경

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);
}
			

 

profile
"야, (오류 만났어?) 너두 (해결) 할 수 있어"

0개의 댓글