Spring & Spring boot

[Spring Boot] JPA - 7. QueryDSL 사용 방법

jh4dev 2024. 8. 11. 18:21
<목차>
앞선 포스팅에서, QueryDSL 을 사용하기 위한 설정 방법에 대해 알아보았다.
이번 포스팅에서는, 간단한 테스트 코드를 통해 QueryDSL을 사용하는 방법에 대해 알아보자.

1. JPAQuery / JPAQueryFactory
2. QuerydslPredicateExecutor
3. QuerydslRepositorySupport

 

QueryDSL 설정 방법은 아래 포스팅을 참고

 

[Spring Boot] JPA - 6. QueryDSL 설정

아래 환경에서 QueryDSL을 사용하기 위한 설정 방법에 대해 알아보자.IntelliJSpringboot 2.7.18Maven1. QueryDSL ?2. QueryDSL 설정 방법 (QDomain) [QueryDSL ?]JPQL의 경우, 쿼리 메소드 외에도 @Query 어노테이션을 사용

jh4dev.tistory.com

 


 

 

[JPAQuery / JPAQueryFactory]

<JPAQuery>

JPAQuer 는 EntityManager 를 활용하여 생성한다.

    @Autowired
    EntityManager entityManager;
    @Test
    void queryDslTest01(){

        JPAQuery<Product> query = new JPAQuery<Product>(entityManager);
        QProduct qProduct = QProduct.product;

        List<Product> productList = query.from(qProduct).where(qProduct.name.eq("펜")).orderBy(qProduct.price.asc()).fetch();

        StringBuilder stringBuilder = new StringBuilder();
        stringBuilder.append("==============================");
        for(Product product : productList) {
            stringBuilder.append("---------------------------- \n")
                    .append("Product Number : ").append(product.getNumber()).append("\n")
                    .append("Product Name : ").append(product.getName()).append("\n")
                    .append("Product Price : ").append(product.getPrice()).append("\n")
                    .append("Product Stock : ").append(product.getStock()).append("\n")
                    .append("---------------------------- \n");
        }
        log.info(stringBuilder.toString());
    }
  • EntityManager 를 통해 JPAQuery 객체를 생성해야 하므로, EntityManager 의존성을 주입해줬다.
  • APT 설정을 통해 생성된 Product Entity 의 QClass 인 QProduct 를 사용한다.
  • query.from().where().... 과 같이 builder 형식으로 쿼리를 작성한다.
  • List 타입으로 리턴받기 위해 fetch() 메서드를 사용하였다.
    이 외에도, 아래 메서드들이 있다.
    • T fetchOne() : 단 건 리턴
    • T fetchFirst() : 조회 결과 중 1건 리턴 (orderby)
    • Long fetchCount() : 조회 결과의 Row 수 리턴
    • QueryResult<T> fetchResults() : 조회 결과 리스트와 개수를 포함한 QueryResult 리턴


<JPAQueryFactory>

JPAQuery 는 from 절부터 시작했다면, JPAQueryFactory는 select 절부터 작성 가능하다. (selectFrom())
만약, 일부 컬럼만 조회하겠다면, select() 와 from() 을 나누어 작성한다. (단, 조회 결과를 Tuple 객체로 리턴)

    @Autowired
    EntityManager entityManager;
    @Test
    void queryDslTest03() {
        JPAQueryFactory jpaQueryFactory = new JPAQueryFactory(entityManager);
        QProduct qProduct = QProduct.product;

        // Single select
        List<String> productList = jpaQueryFactory
                .select(qProduct.name)
                .from(qProduct)
                .where(qProduct.name.eq("펜"))
                .orderBy(qProduct.price.asc())
                .fetch();

        StringBuilder stringBuilder = new StringBuilder();
        stringBuilder.append("\n==============================\n");
        for(String product : productList) {
            stringBuilder.append("---------------------------- \n")
                    .append("Product Name : ").append(product).append("\n")
                    .append("---------------------------- \n");
        }
        log.info(stringBuilder.toString());

        // List Select
        List<Tuple> tupleList = jpaQueryFactory
                .select(qProduct.name, qProduct.price)
                .from(qProduct)
                .where(qProduct.name.eq("펜"))
                .orderBy(qProduct.price.asc())
                .fetch();

        stringBuilder.setLength(0);
        stringBuilder.append("\n==============================\n");
        for(Tuple tuple : tupleList) {
            stringBuilder.append("---------------------------- \n")
                    .append("Product Name : ").append(tuple.get(qProduct.name)).append("\n")
                    .append("Product Price : ").append(tuple.get(qProduct.price)).append("\n")
                    .append("---------------------------- \n");
        }
        log.info(stringBuilder.toString());

    }

 

매번 EntityManager 를 주입받아 JPAQueryFactory를 생성하는 것은 매우 번거롭기 때문에, EntityManager로 생성한 JPAQueryFactory 객체를 빈으로 등록하여 사용하도록 하자.

 

** JPAQueryFactory Bean 등록 **

    @Configuration
    public class QueryDSLConfig {

        @PersistenceContext
        EntityManager entityManager;

        @Bean
        public JPAQueryFactory jpaQueryFactory() {
            return new JPAQueryFactory(entityManager);
        }
    }

 

** Bean으로 등록한 JPAQueryFactory 를 주입받아 작성

    @Autowired
    JPAQueryFactory jpaQueryFactory;

    @Test
    void queryDslTest04() {
        QProduct qProduct = QProduct.product;

        List<String> productList = jpaQueryFactory
                .select(qProduct.name)
                .from(qProduct)
                .where(qProduct.name.eq("펜"))
                .orderBy(qProduct.price.asc())
                .fetch();

        StringBuilder stringBuilder = new StringBuilder();
        stringBuilder.append("\n==============================\n");
        for(String product : productList) {
            stringBuilder.append("---------------------------- \n")
                    .append("Product Name : ").append(product).append("\n")
                    .append("---------------------------- \n");
        }
        log.info(stringBuilder.toString());
    }

 


[QuerydslPredicateExecutor]

QuerydslPredicateExecutor 는 Spring Data JPA 프레임워크에서 QueryDSL을 더 펴한게 사용할 수 있도록 제공하는 인터페이스이다.

 

JpaRepository 와 함께 리포지토리에서 QueryDSL 을 사용할 수 있도록 제공되는 인터페이스이며, 아래와 같이 리포지토리를 생성하여 사용할 수 있다.

** Repository 생성

    public interface QProductRepository
            extends JpaRepository<Product, Long>, QuerydslPredicateExecutor<Product> {
    }

 

그렇다면, QuerydslPredicateExecutor 인터페이스에서 제공되는 메서드를 살펴보자.
<QuerydslPredicateExecutor Interface>

    //
    // Source code recreated from a .class file by IntelliJ IDEA
    // (powered by FernFlower decompiler)
    //

    package org.springframework.data.querydsl;

    import com.querydsl.core.types.OrderSpecifier;
    import com.querydsl.core.types.Predicate;
    import java.util.Optional;
    import java.util.function.Function;
    import org.springframework.data.domain.Page;
    import org.springframework.data.domain.Pageable;
    import org.springframework.data.domain.Sort;
    import org.springframework.data.repository.query.FluentQuery;

    public interface QuerydslPredicateExecutor<T> {
        Optional<T> findOne(Predicate predicate);

        Iterable<T> findAll(Predicate predicate);

        Iterable<T> findAll(Predicate predicate, Sort sort);

        Iterable<T> findAll(Predicate predicate, OrderSpecifier<?>... orders);

        Iterable<T> findAll(OrderSpecifier<?>... orders);

        Page<T> findAll(Predicate predicate, Pageable pageable);

        long count(Predicate predicate);

        boolean exists(Predicate predicate);

        <S extends T, R> R findBy(Predicate predicate, Function<FluentQuery.FetchableFluentQuery<S>, R> queryFunction);
    }

 

  • QuerydslPredicateExecutor 의 메서드들을 살펴보면, 위와 같이 Predicate 타입을 파라미터로 받고 있다.
  • Predicate 는 표현식을 작성할 수 있도록 QueryDSL 에서 제공하는 인터페이스이며, 간단하게 표현식으로 정의하는 쿼리로 생각하면 된다.
  • 다만, QuerydslPredicateExecutor 의 경우, join 이나 fetch 기능은 사용할 수 없다는 단점이 있다.
    <Predicat 인터페이스를 활용한 QueryDSL 사용 방법>
    @Autowired
    QProductRepository qProductRepository;

    @Test
    void predicateFindOneTest() {
        Predicate predicate = QProduct.product.name.containsIgnoreCase("펜")
                .and(QProduct.product.price.between(1000, 2500));

        Optional<Product> foundProduct = qProductRepository.findOne(predicate);

        if(foundProduct.isPresent()) {
            Product product = foundProduct.get();

            System.out.println(product);
        }
    }

 

QuerydslPredicateExecutor 공식 문서

 

QuerydslPredicateExecutor (Spring Data Core 3.3.2 API)

All Known Subinterfaces: ListQuerydslPredicateExecutor public interface QuerydslPredicateExecutor Interface to allow execution of QueryDsl Predicate instances. Author: Oliver Gierke, Thomas Darimont, Christoph Strobl, Mark Paluch See Also: Method Summary A

docs.spring.io

 


[QuerydslRepositorySupport]

QuerydslRepositorySupport 클래스는 이름에서 볼 수 있듯이, Repository 를 서포트하는 기능을 위한 클래스이다.

주로 커스텀 리포지토리 구현체를 작성할 때 사용되며, Spring Data JPA 의 리포지토리 인터페이스에 추가적인 메서드를 구현해야하는 상황에 QuerydslRepositorySupport 활용할 수 있다.

 

커스텀 리포지토리를 구현하는 방식은 다음과 같다. (다양한 방식이 있으며, 일반적인 방식에 대한 설명입니다.)
이해를 쉽게 하기 위하여, Product 라는 Entity 가 존재한다고 가정한다.
* 클래스
* 인터페이스

  1. JpaRepository 를 상속받는 ProductRepository 인터페이스 생성 (기본 리포지토리)
  2. JpaRepository 를 상속받지 않는 ProductRepositoryCustom 인터페이스 생성 및 메서드 정의
  3. ProductRepository 에서 ProductRepositoryCustom 상속
  4. ProductRepositoryCustom 의 구현체 ProductRepositoryCustomImpl 생성 및 작성
  5. ProductRepositoryCustomImpl 에서 QueryDSL 을 사용하기 위해 QuerydslRepositorySupport 상속

 

이와 같이 구성함으로써, 서비스나 DAO 레이어에서는 ProductRepository 라는 엔티티명 기반의 리포지토리를 사용할 수 있으며, JPA 기본 메서드와 더불어 QueryDSL 의 기능 또한 사용할 수 있다.

 

위 구조로 QuerydslRepositorySupport 를 사용하기 위해, 각 인터페이스와 소스는 다음과 같이 구성한다.


<ProductRepositoryCustom>

public interface ProductRepositoryCustom {

    //Custom 메서드 작성
    List<Product> findByName(String name);

}

<ProductRepositoryCustomImpl>

    @Component
    public class ProductRepositoryCustomImpl 
    	extends QuerydslRepositorySupport 
        implements ProductRepositoryCustom{

        //QuerydslRepositorySupport 를 상속받는 경우, 
        //부모 클래스로 도메인 클래스 전달
        public ProductRepositoryCustomImpl() {
            super(Product.class);
        }
        @Override
        public List<Product> findByName(String name){
            QProduct product = QProduct.product;

            List<Product> productList = from(product)
                    .where(product.name.eq(name))
                    .select(product)
                    .fetch();

            return productList;
        }
    }
  • QuerydslRepositorySupport 를 상속 받는 경우,
    생성자를 통해 도메인 클래스를 부모 클래스(QuerydslRepositorySupport) 로 전달해야 한다.
  • findByName() 메서드를 살펴보자
    • QuerydslRepositorySupport 의 기능을 사용하기 위해서는, QClass(QProduct) 를 사용한다.

<ProductRepository>

    @Repository
    public interface ProductRepository 
    	extends JpaRepository<Product, Long>
        , ProductRepositoryCustom {
    }

 

 

<테스트 코드>

    @SpringBootTest
    public class ProductRepositoryTest extends RepositoryTestBase{

    	//위 과정을 걸쳐 생성한 productRepository를 주입받는다.
        @Autowired
        ProductRepository productRepository;

        @Test
        void findByNameTest() {
            List<Product> productList = productRepository.findByName("펜");

            for(Product product : productList) {
                System.out.println(product.toString());
            }
        }
    }

 

QuerydslRepositorySupport 공식 문서

 

QuerydslRepositorySupport (Spring Data JPA Parent 3.3.2 API)

java.lang.Object org.springframework.data.jpa.repository.support.QuerydslRepositorySupport Base class for implementing repositories using Querydsl library. Author: Oliver Gierke, Mark Paluch Constructor Summary Constructors Method Summary All MethodsInstan

docs.spring.io

 


[Github]

https://github.com/jh4dev/spring-boot-study/tree/main/advjpa

 

spring-boot-study/advjpa at main · jh4dev/spring-boot-study

스프링 부트 스터디. Contribute to jh4dev/spring-boot-study development by creating an account on GitHub.

github.com

 


[참고 도서]

 

<스프링 부트 핵심 가이드>
저자 : 장정우

출판사 : 위키북스