본문 바로가기

Development/Spring

[Spring] QueryDSL

이미지 출처: https://images.velog.io/images/solchan/post/9c738f91-91c4-4fd8-be68-62aa31cafcdc/querydsl.png

QueryDSL

  • QueryDSL은 정적 타입을 이용해 SQL과 같은 쿼리를 생성할 수 있도록 지원하는 프레임워크
  • 문자열이나 XML파일을 통해 쿼리를 작성하는 대신 QueryDSL이 제공하는 플루언트(Fluent) API를 활용해 쿼리를 생성할 수 있다.

QueryDSL의 장점

  • IDE가 제공하는 코드 자동 완성 기능을 사용할 수 있다.
  • 문법적으로 잘못된 쿼리를 허용하지 않는다. 따라서 정상적으로 활용된 QueryDSL은 문법 오류를 발생시키지 않는다.
  • 고정된 SQL 쿼리를 작성하지 않기 때문에 동적으로 쿼리를 생성할 수 있다.
  • 코드로 작성하므로 가독성 및 생산성이 향상된다.
  • 도메인 타입과 프로퍼티를 안전하게 참조할 수 있다. 

QueryDSL 의존성 추가하기

Spring 3.x.x이상, Java 21, Gradle 기준

build.gradle에 다음과 같은 코드를 추가

implementation 'com.querydsl:querydsl-core:5.0.0'
	implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta'
	annotationProcessor(
			"jakarta.persistence:jakarta.persistence-api",
			"jakarta.annotation:jakarta.annotation-api",
			"com.querydsl:querydsl-apt:5.0.0:jakarta")

 

위에서 querydsl-apt는 JPAAnnotationProcessor가 포함되어 @Entity 어노테이션으로 정의된 엔티티 클래스를 찾아서 쿼리 타입을 생성한다.

 

위 의존성을 추가하고 build를 수행하면 build/generated/sources/annotationProcessor/java/main/org/spring/study/data/entity/ 경로에 Q{엔티티이름}.java가 추가된다.

위 경로는 기본 경로로, 쿼리 객체 위치 설정도 가능하다.

기본적인 QueryDSL 사용하기

@PersistenceContext
    EntityManager entityManager;

    @Test
    void queryDslTest() {
        JPAQuery<Product> query = new JPAQuery(entityManager);
        QProduct qProduct = QProduct.product;

        List<Product> productList = query
                .from(qProduct)
                .where(qProduct.name.eq("펜"))
                .orderBy(qProduct.price.asc())
                .fetch();
  • 위 코드는 QueryDSL을 사용하여 Product 객체를 조회하는 간단한 테스트 코드이다.
  • @PersistenceContext는 JPA provider(e.g. Hibernate)에 의해 제공받은 EntityManager를 주입하여 영속성 컨텍스트와 JPA 관련 동작들로 상호작용을 가능케 한다.
  • JPAQuery 객체는 QueryDSL 문법으로 JPQL의 query를 작성하도록 한다. 인자로 entityManager를 넘겨주어 쿼리가 수행될 영속성 컨텍스트를 지정한다.
  • QProduct객체는 QueryDSL을 사용하기 위한 Product 엔티티를 나타내는 클래스로, 이 클래스를 통해 쿼리를 작성한다.
  • .fetch()함수를 통해 List 타입으로 값을 리턴받는다.

반환 메서드로 사용할 수 있는 메서드는 다음과 같다.

  • List<T> fetch(): 조회 결과를 리스트로 반환
  • T fetchOne: 한 건의 조회 결과를 반환
  • T fetchFirst(): 여러 건의 조회 결과 중 1건을 반환. 내부 로직을 살펴보면 '.limit(1).fetchOne()'으로 구현돼 있다.
  • Long fetchCount(): 조회 결과의 개수를 반환
  • QueryResult<T> fetchResults(): 조회 결과 리스트와 개수를 포함한 QueryResults를 반환

다음은 JPAQueryFactory를 사용한 테스트 코드 예제이다.

JPAQueryFactory jpaQueryFactory = new JPAQueryFactory(entityManager);
        QProduct qProduct = QProduct.product;

        List<Product> productList = jpaQueryFactory.selectFrom(qProduct)
                .where(qProduct.name.eq("펜"))
                .orderBy(qProduct.price.asc())
                .fetch();
  • JPAQueryFactory는 JPAQuery와 달리 select절부터 작성이 가능하다. 
  • 만약 전체 칼럼을 조회하지 않고 일부만 조회하고 싶다면 jpaQueryFactory.select(qProduct.name).from(qProduct) 처럼 사용 가능하다.

실제 비즈니스 로직에서 활용할 수 있게 QueryDSL을 설정하는 클래스를 생성할 수 있다.

@Configuration
public class QueryDSLConfiguration {

    @PersistenceContext
    EntityManager entityManager;

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

}

JPAQueryFactory를 Bean에 등록하여 JPAQueryFactory를  스프링 컨테이너에서 가져다 쓸 수 있다.

@Autowired
    JPAQueryFactory jpaQueryFactory;

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

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

        for (String product : productList) {
            System.out.println("Product Name : " + product);
        }
    }

QuerydslPredicateExecutor, QuerydslRepositorySupport

Spring Data JPA는 QueryDSL을 편하게 사용할 수 있도록 QuerydslPredicateExecutor 인터페이스와 QuerydslRepositorySupport 클래스를 제공한다. 

 

QuerydslPredicateExecutor

  • QuerydslPredicateExecutor 인터페이스는 JpaRepository와 함께 리포지토리에서 QueryDSL을 사용할 수 있게 인터페이스를 제공한다.
  • QuerydslPredicateExecutor를 상속받도록 다음과 같이 repository 인터페이스에 추가할 수 있다.

 

QuerydslPredicateExecutor 인터페이스는 다양한 메서드를 제공한다.

  • findOne(Predicate predicate): Predicate와 일치하는 하나의 엔티티를 찾음
  • findAll(Predicate predicate): Predicate와 일치하는 모든 엔티티를 찾음
  • findAll(Predicate predicate, OrderSpecifier<?>... orderSpecifiers): Predicate와 일치하는 모든 엔티티를 찾는데, 정렬 조건에 맞게 정렬함
  • findAll(Predicate predicate, Pageable pageable): Predicate와 일치하는 특정 페이지의 모든 엔티티를 찾음
  • count(Predicate predicate): Predicate와 일치하는 엔티티의 총 개수를 리턴
  • exists(Predicate predicate): Predicate와 일치하는 엔티티가 존재하는지를 확인

Predicate 타입은 표현식을 작성할 수 있게 QueryDSL에서 제공하는 인터페이스이다.

다음과 같이 사용할 수 있다.

QProduct qProduct = QProduct.product;
        Predicate predicate = QProduct.product.name.containsIgnoreCase("pen").and(QProduct.product.price.between(500, 10000));

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

        if (foundProduct.isPresent()) {
            Product product = foundProduct.get();
            System.out.println(product.getName());
            System.out.println(product.getPrice());
            System.out.println(product.getStock());
        }
        
        Iterable<Product> productList = qProductRepository.findAll(
                qProduct.name.contains("ruler").and(qProduct.price.between(100, 1000))
        );

        for (Product product : productList) {
            System.out.println(product.getName());
            System.out.println(product.getPrice());
            System.out.println(product.getStock());
        }

 

QuerydslRepositorySupport

QuerydslRepositorySupport 클래스는 Spring Data JPA에서 제공하는 클래스이다.

JpaRepository와 결합하여 사용할 수 있으며, QueryDSL 문법으로 JPA 메서드를 커스텀할 수 있다. 

예를 들어, 다음과 같이 구현할 수 있습니다.

 

ProductRepositoryCustom

public interface ProductRepositoryCustom {

    List<Product> findByName(String name);

}

ProductRepositoryCustomImpl

@Component
public class ProductRepositoryCustomImpl extends QuerydslRepositorySupport implements ProductRepositoryCustom {

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

이 때 생성자에 도메인 클래스를 부모 클래스에 전달해주어야 한다. findByName 메서드를 QueryDSL 문법으로 커스텀할 수 있다.

 

ProductRepository

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

JpaRepository와 QuerydslRepositorySupport를 상속한 ProductRepositoryCustom을 상속받아, QueryDSL과 JpaRepository를 함께 사용할 수 있다.

 

참고