10장 객체지향 쿼리 언어 2 (QueryDSL)

이주호·2025년 1월 3일

QueryDSL

쿼리를 문자가 아닌 코드로 작성해도, 쉽고 간결하며 그 모양도 쿼리와 비슷하게 개발할 수 있게 해주는 오픈소스 프로젝트.
처음에는 HQL(하이버네이트 쿼리언어)을 코드로 작성할 수 있도록 해주는 프로젝트로 시작해서 지금은 JPA, JDO, JDBC, Lucence, Hibernate Search, 몽고DB, 자바 컬렉션 등을 다양하게 지원한다.
QueryDSL은 이름 그대로 데이터를 조회하는데 기능이 특화되어 있다.

QueryDSL 설정

책은 3.6.3버전이라 패키지가 다르고 에러가 다른 등 차이가 있어 5.xx 최신버전으로 설명을 약간 수정했다.

pom.xml에 QueryDSL 쿼리 타입을 생성하기 위한 플러그인을 추가하려면, maven-compiler-pluginapt-maven-plugin을 함께 설정해야 합니다. apt-maven-plugin은 QueryDSL의 Q 클래스를 생성하는 역할을 하며, maven-compiler-plugin은 컴파일을 제어합니다. 아래는 이를 위한 설정 예시입니다:

<dependencies>
    <!-- QueryDSL JPA 의존성 -->
    <dependency>
        <groupId>com.querydsl</groupId>
        <artifactId>querydsl-jpa</artifactId>
        <version>5.0.0</version> <!-- 최신 버전 -->
    </dependency>

    <!-- QueryDSL APT 의존성 (컴파일 타임에 Q 클래스 생성) -->
    <dependency>
        <groupId>com.querydsl</groupId>
        <artifactId>querydsl-apt</artifactId>
        <version>5.0.0</version> <!-- 최신 버전 -->
        <scope>provided</scope>
    </dependency>
</dependencies>

<build>
    <plugins>
        <!-- APT 플러그인: QueryDSL Q 클래스 생성 -->
        <plugin>
            <groupId>org.codehaus.mojo</groupId>
            <artifactId>apt-maven-plugin</artifactId>
            <version>1.1.3</version>
            <executions>
                <execution>
                    <goals>
                        <goal>process</goal>
                    </goals>
                </execution>
            </executions>
            <configuration>
                <!-- 생성된 Q 클래스가 src/main/java 폴더에 위치하게 설정 -->
                <outputDirectory>${project.build.directory}/generated-sources/apt</outputDirectory>
            </configuration>
        </plugin>

        <!-- 컴파일러 플러그인: APT 처리 후 Java 컴파일 -->
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <version>3.8.1</version>
            <configuration>
                <source>1.8</source>
                <target>1.8</target>
                <!-- APT 플러그인에서 생성한 파일을 컴파일 대상으로 포함 -->
                <generatedSourcesDirectory>${project.build.directory}/generated-sources/apt</generatedSourcesDirectory>
            </configuration>
        </plugin>
    </plugins>
</build>

설명:

  1. apt-maven-plugin: 이 플러그인은 Q 클래스를 생성하는 역할을 하며, 지정된 outputDirectory에 결과를 저장합니다.
  2. maven-compiler-plugin: 이 플러그인은 Q 클래스를 생성한 후 그 파일을 실제로 컴파일하여 프로젝트의 클래스 경로에 포함시킵니다.
  3. querydsl-jpa: QueryDSL의 핵심 라이브러리로, JPA와 연동하여 쿼리 생성 기능을 제공합니다.
  4. querydsl-apt: Q 클래스를 생성하는 라이브러리로, 컴파일 타임에 실행됩니다.

(QueryDSL을 사용하려면 엔티티를 기반으로 쿼리 타입이라는 쿼리용 클래스, Q클래스를 생성해야함)

이 설정을 추가한 후 mvn clean install을 실행하면, Q 클래스가 생성되고, 이를 통해 QueryDSL을 사용하여 JPA 기반 쿼리를 작성할 수 있습니다.

QueryDSL 시작

		public void queryDSL() {

            EntityManager em = emf.createEntityManager();
            
            JPAQuery query = new JPAQuery(em);
            QMember qMember = new QMember("m"); //생성되는 JPQL의 별칭이 m
            List<Member> members = query.from(qMember)
                                    .where(qMember.name.eq("회원1"))
                                    .orderBy(qMember.name.desc())
                                    .list(qMember);
        }

QueryDSL을 사용하려면 com.querydsl.jpa.impl.JPAQuery (queryDSL 5.xx 버전부터 패키지명 변경) 객체를 생성해야 하는데 이때 엔티티 매니저를 생성자에 넘겨준다.
사용할 쿼리 타입(Q)을 생성하는데 생성자에는 별칭을 주면 된다. 이 별칭을 JPQL에서 별칭으로 사용한다.
다음 where, orderBy, list는 코드만 봐도 유추가 가능하다.

//실행 JPQL
select m from Member m
where m.name =?1
order by m.name desc

기본 Q 생성

쿼리 타입(Q)은 사용하기 편리하도록 기본 인스턴스를 보관하고 있다. 하지만 엔티티를 조인하거나 같은 엔티티를 서브쿼리에 사용하면 같은 별칭이 사용되므로 이때는 별칭을 직접 지정해서 사용해야 한다.

public class QMember extends EntityPathBase<Member> {

	public static final QMember member = new QMember("member1"); //기본 인스턴스
    ...
}    

//쿼리 타입 사용
QMember qMember = new QMember("m");	//직접 지정
QMember qMember = QMember.member;	//기본 인스턴스 사용

기본 인스턴스를 사용하면 import static을 이용해서 다음과 같이 사용할 수 있다.

import static jpabook.jpashop.domain.Qmember.member;	//기본인스턴스

public void queryDSL() {

	EntityManager em = emf.createEntityManager();

    JPAQuery query = new JPAQuery(em);
    List<Member> members = query.from(member)  // member1로 정적 정의한 QMember 사용
                                .where(member.name.eq("회원1"))
                                .orderBy(member.name.desc())
                                .list(member);
}

검색 조건 쿼리

JPAQuery query = new JPAQuery(em);
QItem item = QItem.item;
List<Item> list = query.from(item)
						.where(item.name.eq("상품").and(item.price.gt(20000)))
                        .list(item);	//조회할 프로젝션 지정
//실행된 JPQL
select item
from Item item
where item.name = ?1 and item.price > ?2

gt는 greater than의 약자로 queryDSL은 이와 같은 약자를 여러 이용한다. (eq는 equals)

이외에도 where() 에서 사용되는 메소드의 예시를 보자.

item.price.between(10000,20000);	//가격이 10000원 ~ 20000원
item.name.contains("상품1");			//"상품1"이라는 이름을 포함한 상품
									//SQL에서 like '%상품1%' 검색
item.name.startsWith("최고급");		//이름이 "최고급"으로 시작하는 상품
									//SQL에서 like '최고급%' 검색

결과 조회 쿼리

QueryDSL에서 결과를 조회할 때 사용할 수 있는 다양한 메서드들이 있습니다. 대표적인 메서드로는 uniqueResult(), singleResult(), list() 등이 있습니다. 각 메서드는 쿼리 결과가 하나일지 여러 개일지에 따라 선택하여 사용합니다.

1. list() 메서드

list() 메서드는 여러 개의 결과를 반환할 때 사용됩니다. 반환되는 결과는 List 형태입니다.

예시:

List<Member> members = query.from(qMember)
                            .where(qMember.name.eq("회원1"))
                            .orderBy(qMember.name.desc())
                            .fetch(); // list() 메서드와 동일

이 메서드는 결과가 여러 개일 때 유용하며, 조건에 맞는 모든 엔티티를 List로 반환합니다.

2. uniqueResult() 메서드

uniqueResult() 메서드는 단 하나의 결과만 반환할 때 사용됩니다. 이 메서드는 여러 개의 결과가 있을 경우 IllegalStateException을 던집니다.

예시:

Member member = query.from(qMember)
                     .where(qMember.username.eq("kim"))
                     .uniqueResult(); // 단 하나의 결과를 기대

결과가 하나일 것으로 예상할 때 유용하며, 다수의 결과가 나올 경우 예외가 발생합니다.

3. singleResult() 메서드

singleResult() 메서드는 결과가 하나일 때만 정상적으로 결과를 반환합니다. 이 메서드도 결과가 여러 개일 경우 javax.persistence.NoResultException이나 javax.persistence.NonUniqueResultException을 던질 수 있습니다.

예시:

Member member = query.from(qMember)
                     .where(qMember.username.eq("kim"))
                     .singleResult();

결과가 하나일 때 사용되며, 값이 없으면 NoResultException, 여러 개가 있으면 NonUniqueResultException을 던집니다.

패키지

QueryDSL에서 결과 조회를 위한 클래스와 메서드는 보통 com.querydsl.jpa.impl.JPAQuery 패키지 내에서 제공됩니다. 따라서 해당 클래스에서 위 메서드들을 사용하여 쿼리 결과를 가져올 수 있습니다.

예시:

import com.querydsl.jpa.impl.JPAQuery;
import com.querydsl.core.types.dsl.BooleanExpression;

JPAQuery<Member> query = new JPAQuery<>(entityManager);
QMember qMember = QMember.member;

List<Member> members = query.from(qMember)
                             .where(qMember.name.eq("회원1"))
                             .orderBy(qMember.name.desc())
                             .fetch(); // list() 메서드

요약

  • list()는 여러 개의 결과를 반환하며, List로 결과를 받습니다.
  • uniqueResult()는 단 하나의 결과를 기대할 때 사용하며, 결과가 없거나 여러 개일 경우 예외가 발생합니다.
  • singleResult()는 결과가 정확히 하나여야 할 때 사용되며, 결과가 없거나 여러 개일 경우 예외를 던집니다.

각 메서드는 쿼리의 특성과 기대하는 결과에 맞춰 적절히 선택하여 사용할 수 있습니다.

페이징과 정렬

QueryDSL에서 페이징(Paging)정렬(Sorting)을 설정하는 방법을 알아보겠습니다. 페이징과 정렬은 대량 데이터를 처리할 때 매우 중요하며, QueryDSL을 사용하면 간편하게 구현할 수 있습니다.

1. 페이징(Paging)

페이징을 설정하려면 offset()limit() 메서드를 사용하여 조회 범위를 지정합니다. offset()은 조회를 시작할 위치를, limit()은 한 번에 조회할 개수를 설정합니다.

예시:

List<Member> members = query.from(qMember)
                            .where(qMember.name.eq("회원1"))
                            .offset(0) // 시작 위치
                            .limit(10) // 가져올 데이터 수
                            .fetch();
  • offset(0)은 첫 번째 데이터부터 시작하도록 설정합니다.
  • limit(10)은 10개의 결과만 가져오도록 설정합니다.

2. 정렬(Sorting)

정렬을 설정하려면 orderBy() 메서드를 사용합니다. orderBy() 안에는 정렬할 필드를 지정하고, 각 필드에 대해 오름차순(ascending) 또는 내림차순(descending)을 선택할 수 있습니다.

예시:

List<Member> members = query.from(qMember)
                            .where(qMember.name.eq("회원1"))
                            .orderBy(qMember.name.asc()) // 오름차순 정렬
                            .offset(0)
                            .limit(10)
                            .fetch();

위 예제에서 qMember.name.asc()는 이름을 오름차순으로 정렬하는 것을 의미합니다. 내림차순으로 정렬하려면 desc()를 사용합니다.

예시 (내림차순):

List<Member> members = query.from(qMember)
                            .where(qMember.name.eq("회원1"))
                            .orderBy(qMember.name.desc()) // 내림차순 정렬
                            .offset(0)
                            .limit(10)
                            .fetch();

페이징과 정렬을 결합한 예시:

페이징과 정렬을 함께 사용할 수도 있습니다. 다음은 이름을 내림차순으로 정렬하고, 첫 번째 페이지(10개의 항목)만 가져오는 예시입니다.

List<Member> members = query.from(qMember)
                            .where(qMember.name.eq("회원1"))
                            .orderBy(qMember.name.desc()) // 이름 내림차순 정렬
                            .offset(0) // 첫 번째 페이지
                            .limit(10) // 10개 항목
                            .fetch();

요약

  • 페이징: offset()limit()을 사용하여 데이터를 페이지 단위로 가져옵니다.
  • 정렬: orderBy()asc(), desc()를 사용하여 정렬을 적용합니다.
  • 결합 사용: 페이징과 정렬을 동시에 적용하여 원하는 데이터만 효율적으로 가져올 수 있습니다.

이렇게 QueryDSL을 사용하면 페이징과 정렬을 간단하게 설정하여 데이터를 효율적으로 조회할 수 있습니다.

결과 조회가 list()가 아니라 fetch()로 변경 (4.xx 버전부터)

책에서는 list()로 결과를 받았지만 위의 예시를 보면 fetch()를 사용했다. 이에 대한 설명은 다음과 같다.

fetch() 메서드는 QueryDSL 4.x 버전부터 도입된 메서드로, 결과를 실제로 조회하는 역할을 합니다. 이전 3.x 버전에서는 fetch() 대신 list()나 uniqueResult() 같은 메서드를 사용하여 결과를 얻었습니다.
QueryDSL 4.x 이상 버전에서는 결과 조회 시 fetch() 메서드를 사용하는 것이 표준입니다. 이전에는 list()uniqueResult()와 같은 메서드를 사용했으나, fetch() 메서드는 그 역할을 대체하며 더 직관적인 결과 조회 방식을 제공합니다.

차이점:

  • list(): 결과가 여러 개일 때 사용합니다. list()fetch() 메서드로 대체되었습니다.
  • uniqueResult(): 결과가 하나만 있을 때 사용합니다. 결과가 하나가 아니면 예외가 발생합니다. fetchOne()이 그 역할을 합니다.
  • fetch(): 4.x 버전에서는 list()와 같은 역할을 하며, 쿼리 실행 후 결과 리스트를 반환합니다.

즉, 최신 버전에서는 fetch() 메서드를 사용하면 모든 결과 조회를 처리할 수 있기 때문에, 별도로 list()uniqueResult() 같은 메서드를 사용할 필요가 없습니다.

예시:

List<Member> members = query.from(qMember)
                             .where(qMember.name.eq("회원1"))
                             .orderBy(qMember.name.desc())
                             .fetch(); // 결과 조회

따라서 최신 버전에서는 fetch()를 사용하는 것이 일반적입니다.

그룹 groubBy, having

간단한 예시로 groupByhaving을 사용하는 방법을 설명하겠습니다.

예시: groupByhaving

QMember qMember = QMember.member;

List<Tuple> result = query.from(qMember)
                          .groupBy(qMember.age)
                          .having(qMember.age.gt(30))  // 나이가 30살 초과인 그룹만 선택
                          .list(qMember.age, qMember.count());
  • groupBy(qMember.age): age를 기준으로 그룹화합니다.
  • having(qMember.age.gt(30)): 그룹화된 결과에서 나이가 30살 초과인 그룹만 필터링합니다.
  • list(qMember.age, qMember.count()): age별로 몇 명이 있는지 카운트합니다.

이 예제에서는 groupBy를 사용해 나이를 기준으로 그룹을 나눈 후, having을 사용해 나이가 30살 이상인 그룹만 필터링하고 있습니다.

fetch() 말고 list() 사용 이유

fetch()list()의 차이는 QueryDSL의 API에서 결과를 반환하는 방식에 따라 다릅니다.

list()fetch()의 차이점

  • list(): groupByhaving을 사용할 때는 보통 list()를 사용합니다. groupBy로 그룹화된 결과를 반환하려면 list()가 가장 적합합니다. groupBy는 집계 결과를 반환하기 때문에 Tuple로 결과를 받습니다.
  • fetch(): fetch()Query 실행 시 여러 개의 결과를 한 번에 처리하는 방식입니다. fetch()는 반환 결과가 List<T> 형태로 여러 개의 엔티티를 반환할 때 사용됩니다. fetch()Query에서 반환되는 결과가 단일 객체가 아닌 복수의 객체일 때 적합합니다.

groupByhavinglist() 사용 이유

  • groupByhaving집계를 의미하는 기능입니다. 결과는 여러 컬럼으로 그룹화된 결과를 반환하므로, 여러 값을 포함한 Tuple 형태로 반환됩니다. list()는 여러 값이 포함된 결과를 처리하는 데 적합합니다.

따라서 groupByhaving은 결과가 여러 항목을 포함한 집계된 결과를 반환하므로 fetch() 대신 list()를 사용하는 것이 더 자연스럽습니다.


조인

조인(Join)은 관계형 데이터베이스에서 여러 테이블을 결합하는 방법을 말합니다. 데이터베이스 쿼리에서 조인을 사용하면, 여러 테이블에서 관련된 데이터를 함께 조회할 수 있습니다.

주요 조인 유형

  1. 내부 조인(Inner Join):

    • 기본적인 조인 방식으로, 두 테이블의 조건에 맞는 행만 반환합니다. 예를 들어, MemberTeam 테이블이 있을 때, 특정 Member가 속한 Team 정보를 조회하고 싶다면, 내부 조인을 사용하여 두 테이블에서 조건을 만족하는 데이터만 반환합니다.
    JPAQuery query = new JPAQuery(em);
    QMember qMember = QMember.member;
    QTeam qTeam = QTeam.team;
    List<Member> members = query.from(qMember)
                                 .innerJoin(qMember.team, qTeam)
                                 .where(qMember.name.eq("John"))
                                 .fetch();
  2. 외부 조인(Outer Join):

    • 왼쪽 외부 조인(Left Outer Join): 왼쪽 테이블의 모든 행을 반환하고, 오른쪽 테이블에서 조건을 만족하지 않는 행은 NULL 값을 가집니다. 예를 들어, 모든 Member를 조회하되, 해당하는 Team이 없는 Member도 포함시키고 싶을 때 사용합니다.
    • 오른쪽 외부 조인(Right Outer Join): 오른쪽 테이블의 모든 행을 반환하고, 왼쪽 테이블에서 조건을 만족하지 않는 행은 NULL 값을 가집니다.
    List<Member> members = query.from(qMember)
                                 .leftJoin(qMember.team, qTeam)
                                 .where(qTeam.name.isNull())
                                 .fetch();
  3. 크로스 조인(Cross Join):

    • 두 테이블의 모든 행을 조합하여 반환합니다. 즉, 테이블의 첫 번째 행과 두 번째 테이블의 모든 행을 결합하여 결과를 만듭니다. 일반적으로 크로스 조인은 매우 큰 결과를 생성할 수 있기 때문에 잘 사용되지 않습니다.
    List<Tuple> result = query.from(qMember)
                              .crossJoin(qTeam)
                              .fetch();
  4. 자기 조인(Self Join):

    • 같은 테이블을 조인하는 방식입니다. 예를 들어, 하나의 Employee 테이블에서 상사와 부하 직원 관계를 조회할 때 사용됩니다.
    QEmployee qEmployee1 = QEmployee.employee;
    QEmployee qEmployee2 = new QEmployee("manager");
    List<Employee> employees = query.from(qEmployee1)
                                     .leftJoin(qEmployee1.manager, qEmployee2)
                                     .fetch();

QueryDSL에서의 조인

QueryDSL에서는 join(), leftJoin(), innerJoin() 등을 사용하여 조인을 구현할 수 있습니다. 각 메서드는 각각의 조인 유형을 지정할 수 있으며, 조인에 대한 추가 조건도 설정할 수 있습니다.

  1. Inner Join:

    JPAQuery query = new JPAQuery(em);
    QMember qMember = QMember.member;
    QTeam qTeam = QTeam.team;
    List<Member> members = query.from(qMember)
                                 .innerJoin(qMember.team, qTeam)
                                 .fetch();
  2. Left Join:

    List<Member> members = query.from(qMember)
                                 .leftJoin(qMember.team, qTeam)
                                 .fetch();

조인 조건 추가

조인에서 추가적인 조건을 설정할 수 있습니다. 예를 들어, ON 절을 사용해 조인 조건을 명확히 할 수 있습니다.

List<Member> members = query.from(qMember)
                             .leftJoin(qMember.team, qTeam).on(qTeam.name.eq("Team A"))
                             .fetch();

이와 같은 방법으로 QueryDSL에서는 다양한 방식의 조인을 사용하여 데이터를 조회할 수 있습니다.

ON 절 (Join 조건)

ON 절은 조인의 조건을 정의할 때 사용됩니다. 이 절은 내부 조인(Inner Join), 외부 조인(Outer Join) 등에서 유용하게 사용됩니다. 특히, 외부 조인에서는 ON 절을 사용하여 조건을 명시적으로 설정할 수 있으며, 이는 WHERE 절을 사용할 때와 다른 결과를 초래할 수 있습니다.

  • 사용 예시:
    ON 절을 사용하면, 두 테이블 간의 조건을 보다 세밀하게 조정할 수 있습니다.

    List<Member> members = query.from(qMember)
                                 .leftJoin(qMember.team, qTeam).on(qTeam.name.eq("Team A"))
                                 .fetch();
  • 특징:

    • ON 절은 조인의 조건을 설정하는 데 사용되며, WHERE 절과 다르게 필터링을 하지 않습니다. WHERE 절은 결과를 필터링하지만, ON 절은 조인하는 테이블 간의 관계를 정의합니다.
    • 외부 조인의 경우, ON 절을 사용하여 조건을 설정하면 더 유연한 조인을 만들 수 있습니다. 예를 들어, ON 절에서 조건을 설정하면 외부 조인 결과로 NULL 값도 포함될 수 있습니다.

페치 조인 (Fetch Join)

페치 조인은 연관된 엔티티들을 즉시 로딩(Eager Loading) 방식으로 가져오는 기법입니다. 페치 조인을 사용하면 관련 엔티티들을 한 번의 쿼리로 가져오므로, 별도로 추가 쿼리를 실행하는 지연 로딩(Lazy Loading)을 방지할 수 있습니다.

  • 사용 예시:

    List<Member> members = query.from(qMember)
                                 .leftJoin(qMember.team, qTeam).fetch()
                                 .where(qMember.name.eq("John"))
                                 .fetch();

    여기서 fetch() 메서드는 Member 엔티티와 Team 엔티티를 한 번에 불러오는 데 사용됩니다. 이 방법은 연관된 엔티티들을 즉시 로딩하고, SQL 쿼리의 수를 줄여 성능을 향상시킬 수 있습니다.

  • 특징:

    • 지연 로딩 회피: 페치 조인은 연관된 엔티티들을 즉시 로딩하므로 추가적인 쿼리 실행 없이 한 번에 모든 데이터를 조회할 수 있습니다.
    • 성능 개선: 연관된 엔티티를 여러 번 조회하는 대신, 한 번의 쿼리로 모든 데이터를 불러오기 때문에 성능을 크게 개선할 수 있습니다.
    • 주의사항: 페치 조인을 사용할 때, 엔티티 간에 중복된 데이터가 발생할 수 있습니다. 예를 들어, Member 엔티티와 연관된 Team 엔티티를 페치 조인하면 Member가 여러 번 중복되어 결과에 포함될 수 있습니다. 이를 피하려면 distinct()나 다른 방법을 사용해야 할 수 있습니다.

ON 절과 페치 조인의 차이

  • ON: 주로 외부 조인에서 조인 조건을 세밀하게 제어할 수 있게 해줍니다. 예를 들어, 외부 조인에서 특정 조건을 만족하는 데이터만 연결하여 불러오고 싶을 때 사용합니다.
  • 페치 조인: 연관된 엔티티들을 즉시 로딩하여 별도의 추가 쿼리를 실행하지 않도록 해주는 기법으로, 성능을 최적화하는 데 유용합니다.

두 개념은 서로 다르지만, 함께 사용될 수도 있습니다. 예를 들어, 외부 조인과 페치 조인을 결합하여 관련 엔티티를 한 번에 로딩하면서, 조건을 ON 절을 사용하여 세밀하게 조정할 수 있습니다.

서브 쿼리

서브쿼리는 JPQL이나 QueryDSL에서 복잡한 쿼리를 작성할 때 다른 쿼리의 결과를 조건이나 값으로 사용하는 쿼리입니다. QueryDSL에서는 서브쿼리를 생성하기 위해 com.querydsl.jpa.JPAExpressions 클래스를 사용하며, 예전 버전에서 언급된 com.mysema.query.jpa.JPASubQuery는 더 이상 사용되지 않습니다.


QueryDSL에서 서브쿼리 생성

QueryDSL의 최신 버전에서는 JPAExpressions를 사용하여 서브쿼리를 작성합니다.

서브쿼리 예시

import com.querydsl.jpa.impl.JPAQuery;
import com.querydsl.jpa.JPAExpressions;

// 메인 엔티티와 서브쿼리용 엔티티
QMember member = QMember.member;
QMember subMember = new QMember("subMember");

// 서브쿼리 작성
JPAQuery<Member> query = new JPAQuery<>(entityManager);
List<Member> result = query.select(member)
    .from(member)
    .where(member.age.eq(
        JPAExpressions.select(subMember.age.max())
            .from(subMember)
    ))
    .fetch();

생성된 JPQL

SELECT m.*
FROM Member m
WHERE m.age = (
    SELECT MAX(sub_m.age)
    FROM Member sub_m
);

서브쿼리 주요 구성 요소

  1. JPAExpressions

    • 서브쿼리 생성에 필요한 빌더 클래스.
    • select, from, where 등을 사용하여 서브쿼리 작성.
  2. 독립된 Q 객체

    • 서브쿼리에서 사용하는 엔티티와 메인 쿼리의 엔티티가 동일한 경우, 다른 별칭을 가진 Q 객체를 만들어야 충돌을 방지할 수 있습니다.
    • 예: new QMember("subMember")

서브쿼리의 사용 목적

  • 조건 쿼리: 특정 값이 서브쿼리의 결과와 일치하는지 비교.
  • 집계 함수: MAX, MIN, AVG 같은 집계 결과를 사용.
  • 중첩 필터링: 서브쿼리에서 필터링된 결과를 메인 쿼리에 전달.

주의점

  1. SQL 제약: 서브쿼리는 SQL 실행 시 성능에 영향을 미칠 수 있습니다. 가능한 경우, JOIN을 고려하여 쿼리를 단순화하세요.
  2. QueryDSL 버전 차이: QueryDSL 3.x 이하에서는 JPASubQuery를 사용했지만, 현재는 JPAExpressions로 변경되었습니다.

더 알아보기

  • QueryDSL 서브쿼리 공식 문서: https://querydsl.com
  • JPA 표준 서브쿼리 관련 내용은 JPQL 문서에서 확인 가능합니다.

여러 컬럼 반환과 튜플

QueryDSL에서 여러 컬럼을 반환하거나 튜플 형태로 결과를 조회하는 방법은 com.querydsl.core.Tuple을 활용합니다. 이 방식은 특정 엔티티가 아닌 여러 컬럼 값이나 계산된 값을 동시에 조회할 때 유용합니다.


여러 컬럼 반환 예제

예제 코드

import com.querydsl.jpa.impl.JPAQuery;
import com.querydsl.core.Tuple;

// Q 클래스 생성
QMember member = QMember.member;

// QueryDSL 쿼리 작성
JPAQuery<Tuple> query = new JPAQuery<>(entityManager);
List<Tuple> results = query.select(member.id, member.username, member.age)
    .from(member)
    .where(member.age.gt(20))
    .fetch();

// 결과 처리
for (Tuple tuple : results) {
    Long id = tuple.get(member.id);
    String username = tuple.get(member.username);
    Integer age = tuple.get(member.age);

    System.out.println("ID: " + id + ", Username: " + username + ", Age: " + age);
}

생성되는 JPQL

SELECT member.id, member.username, member.age
FROM Member member
WHERE member.age > 20;

주요 클래스와 메서드

  1. com.querydsl.core.Tuple

    • 여러 컬럼을 반환할 때 사용하는 객체.
    • get() 메서드를 사용해 결과를 추출합니다.
    • 반환 타입은 컬럼의 데이터 타입에 따라 달라짐.
  2. select()

    • 반환할 컬럼을 나열하며, 복수 개의 필드를 선택할 수 있음.
  3. fetch()

    • 쿼리를 실행하고 결과를 리스트로 반환.

여러 컬럼 반환을 사용하는 이유

  1. 복수 필드 조회: 엔티티 전체 대신 필요한 필드만 조회하여 성능 최적화.
  2. 계산된 값: SQL에서 집계 함수나 연산 결과를 조회 가능.
  3. 엔티티와 비엔티티 데이터 혼합 조회: 엔티티 필드와 계산된 값을 함께 반환할 수 있음.

튜플을 활용할 때의 주의사항

  1. 직접적인 엔티티 반환이 아님: 튜플은 엔티티가 아닌 컬럼 값만 포함.
  2. 컬럼 접근 방식: Q 클래스의 필드를 기반으로 get() 메서드 사용.
  3. 타입 안정성: 컬럼의 데이터 타입에 따라 적절히 처리해야 함.

예제: 튜플과 집계 함수

List<Tuple> results = query.select(member.team.name, member.age.avg())
    .from(member)
    .groupBy(member.team.name)
    .fetch();

for (Tuple tuple : results) {
    String teamName = tuple.get(member.team.name);
    Double avgAge = tuple.get(member.age.avg());

    System.out.println("Team: " + teamName + ", Avg Age: " + avgAge);
}

생성되는 JPQL

SELECT team.name, AVG(member.age)
FROM Member member
GROUP BY team.name;

대안: DTO 반환

Tuple 대신 DTO를 사용하는 방식도 권장됩니다. QueryDSL의 Projections API를 활용하면 특정 DTO로 결과를 매핑할 수 있습니다.

List<MemberDTO> results = query.select(
        Projections.constructor(MemberDTO.class, member.id, member.username, member.age))
    .from(member)
    .fetch();

query.from() VS query.select()

query.from()이 아니라 query.select()을 사용하네? 버전 바뀌면서 바뀐부분이야?

QueryDSL의 메서드 호출 순서는 버전에 따라 약간의 차이가 있지만, query.select()query.from()의 순서는 기본적인 QueryDSL 사용 방식에 따라 달라지는 부분입니다.


1. QueryDSL 3.x 이전

과거 QueryDSL 3.x 버전에서는 from()을 먼저 호출하고 이후에 select() 없이 바로 결과를 처리하거나 필터링하는 방식이 일반적이었습니다. 예를 들어:

List<Member> members = query.from(qMember)
    .where(qMember.username.eq("회원1"))
    .orderBy(qMember.username.desc())
    .list(qMember);

이 방식에서는 list() 메서드가 결과를 조회하는 핵심이었고, select()를 별도로 지정하지 않아도 from()의 대상을 기본적으로 반환 대상으로 처리했습니다.


2. QueryDSL 4.x 이후

QueryDSL 4.x 이후 버전에서는 fetch()가 도입되고, select()를 명시적으로 호출하여 반환할 필드를 지정하는 방식으로 변경되었습니다. 최신 방식에서는 다음과 같이 작성합니다:

List<Member> members = query.select(qMember)
    .from(qMember)
    .where(qMember.username.eq("회원1"))
    .orderBy(qMember.username.desc())
    .fetch();

이 방식은 명시적으로 반환 대상을 설정하므로 코드 가독성과 의도를 명확히 하는 데 기여합니다.


변경 이유

  • 가독성 향상: 반환할 필드를 명확히 지정함으로써 쿼리 의도를 더 쉽게 파악 가능.
  • 표준화: 다양한 반환 대상(Tuple, DTO, 특정 필드 등)을 일관되게 처리할 수 있도록 통합.
  • 결과 조회 방식 변경: list(), uniqueResult() 등의 메서드가 사라지고, fetch(), fetchOne(), fetchFirst() 등의 새로운 결과 조회 API로 통일.

결론

select()fetch()를 사용하는 최신 방식은 QueryDSL 4.x 이후의 표준입니다. 따라서, from()만 사용하는 방식은 이전 버전의 스타일이며, 최신 버전을 사용하는 경우에는 select()를 명시적으로 사용해야 합니다.

참고 사항

  • QueryDSL 3.x의 경우 querydsl-jpaquerydsl-apt 설정 방식이 달라질 수 있습니다.
  • 최신 문법은 공식 QueryDSL 문서를 참고하세요.

빈 생성 Bean pupulation

쿼리 결과를 엔티티가 아닌 특정 객체로 받고 싶으면 빈 생성 기능을 사용한다.
(책에서는 com.mysema.query.types.Projections를 사용한다고 하는데 queryDSL 버전이 업 되면서 사용 안 한다고 함)

아래는 ItemDTO와 실제 Item 엔티티에서 name 필드를 username으로 매핑하여 사용하는 QueryDSL 예제입니다. 여기서는 as() 메서드를 활용해 필드 이름을 별칭으로 지정하여 DTO로 매핑합니다.

Item 엔티티, ItemDTO

@Entity
public class Item {

    @Id @GeneratedValue
    private Long id;

    private String name;
    private int price;

    // Getter, Setter, Constructor
    public Item() {}
    
    public Item(String name, int price) {
        this.name = name;
        this.price = price;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getPrice() {
        return price;
    }

    public void setPrice(int price) {
        this.price = price;
    }
}

//ItemDTO
public class ItemDTO {

    private String username;
    private int price;

    public ItemDTO() {}

    public ItemDTO(String username, int price) {
        this.username = username;
        this.price = price;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public int getPrice() {
        return price;
    }

    public void setPrice(int price) {
        this.price = price;
    }
}

QueryDSL 예제: DTO 매핑 및 별칭 사용

1. 프로퍼티 접근 (Projections.bean)

public void queryWithAlias(EntityManager em) {
    JPAQueryFactory queryFactory = new JPAQueryFactory(em);
    QItem qItem = QItem.item;

	// 프로퍼티 접근 (Setter)
    List<ItemDTO> results = queryFactory
        .select(Projections.bean(
            ItemDTO.class,
            qItem.name.as("username"), // name 필드를 username으로 매핑
            qItem.price
        ))
        .from(qItem)
        .where(qItem.price.gt(1000)) // 예시 조건: 가격이 1000 이상인 경우
        .fetch();

    for (ItemDTO dto : results) {
        System.out.println("Username: " + dto.getUsername() + ", Price: " + dto.getPrice());
    }
}

주요 포인트

  1. Projections.bean() 사용:

    • DTO 매핑 시 프로퍼티 기반 매핑(Setter 사용)을 위해 bean() 메서드를 사용했습니다.
    • DTO의 프로퍼티와 엔티티 필드 이름이 다를 경우 as("alias")를 이용해 별칭을 지정합니다.
  2. 별칭(as) 사용:

    • qItem.name.as("username")name 필드를 DTO의 username 프로퍼티에 매핑.
    • 이 방식은 DTO에서 엔티티 필드 이름과 다른 이름을 사용할 때 유용합니다.
  3. 조건:

    • where(qItem.price.gt(1000))로 조건을 추가. 예제에서는 가격이 1000 이상인 항목만 필터링.

실행 결과 (예시)

Item 엔티티에 다음 데이터가 있다고 가정:

IDnameprice
1"ItemA"1500
2"ItemB"900

출력:

Username: ItemA, Price: 1500

이 방식은 QueryDSL의 DTO 매핑 기능을 활용해 직관적이고 간결한 쿼리 작성을 가능하게 합니다.


2. 필드 직접 접근 (Projections.fields)

이 방식은 DTO 클래스에 Getter/Setter 없이 필드에 직접 값을 할당합니다.

public void queryWithFieldsProjection(EntityManager em) {
    JPAQueryFactory queryFactory = new JPAQueryFactory(em);
    QItem qItem = QItem.item;

    List<ItemDTO> results = queryFactory
        .select(Projections.fields(
            ItemDTO.class,
            qItem.name.as("username"), // 필드 이름 매핑
            qItem.price
        ))
        .from(qItem)
        .where(qItem.price.gt(1000)) // 가격 1000 이상 필터링
        .fetch();

    for (ItemDTO dto : results) {
        System.out.println("Username: " + dto.getUsername() + ", Price: " + dto.getPrice());
    }
}
  • 장점: DTO 클래스에서 Setter 메서드가 없어도 동작합니다.
  • 필드 이름 매핑: as("alias")를 사용해 DTO 필드 이름과 엔티티 필드 이름이 다를 경우 별칭을 설정합니다.

2. 생성자 기반 매핑 (Projections.constructor)

이 방식은 DTO의 생성자를 호출하여 값을 전달합니다. 생성자에 필요한 모든 필드를 명시적으로 지정해야 합니다.

public void queryWithConstructorProjection(EntityManager em) {
    JPAQueryFactory queryFactory = new JPAQueryFactory(em);
    QItem qItem = QItem.item;

    List<ItemDTO> results = queryFactory
        .select(Projections.constructor(
            ItemDTO.class,
            qItem.name,   // 생성자 첫 번째 파라미터
            qItem.price   // 생성자 두 번째 파라미터
        ))
        .from(qItem)
        .where(qItem.price.gt(1000)) // 가격 1000 이상 필터링
        .fetch();

    for (ItemDTO dto : results) {
        System.out.println("Username: " + dto.getUsername() + ", Price: " + dto.getPrice());
    }
}
  • 장점: DTO가 불변 객체(Immutable)여야 하거나 모든 값을 생성자에서 설정해야 할 경우 사용됩니다.
  • 주의사항: DTO에 정의된 생성자 파라미터 순서와 타입이 쿼리에서 지정한 순서와 정확히 일치해야 합니다.

3. 프로퍼티 접근 (Projections.bean)

이미 위에서 보여준 예제입니다. Setter 메서드를 통해 값을 할당하는 방식입니다.

public void queryWithBeanProjection(EntityManager em) {
    JPAQueryFactory queryFactory = new JPAQueryFactory(em);
    QItem qItem = QItem.item;

    List<ItemDTO> results = queryFactory
        .select(Projections.bean(
            ItemDTO.class,
            qItem.name.as("username"), // 프로퍼티 매핑
            qItem.price
        ))
        .from(qItem)
        .where(qItem.price.gt(1000))
        .fetch();

    for (ItemDTO dto : results) {
        System.out.println("Username: " + dto.getUsername() + ", Price: " + dto.getPrice());
    }
}

각 방식의 비교

방식특징사용 사례
Projections.beanSetter 메서드를 통해 값을 설정. 필드 이름 매핑 필요.DTO 필드 이름과 엔티티 필드 이름이 다른 경우. 일반적인 Setter 기반 클래스.
Projections.fields필드에 직접 값을 할당. Getter/Setter 필요 없음.DTO 필드에 바로 값을 설정하는 경우. Setter를 사용하지 않으려는 경우.
Projections.constructor생성자를 호출하여 값을 설정. 파라미터 순서와 타입이 일치해야 함.DTO가 불변 객체이거나 모든 값이 생성자에서만 설정되는 경우.

DISTINCT

distinct는 다음과 같이 사용한다.

query.distinct().from(item)...

QueryDSL에서 distinct()를 사용하는 방식은 현재 버전에서도 여전히 동일하게 동작합니다. QueryDSL의 API는 distinct() 메서드를 통해 JPQL의 DISTINCT 키워드를 추가할 수 있도록 설계되어 있습니다.

예를 들어:

List<String> result = queryFactory
    .select(item.name)
    .distinct() // DISTINCT 키워드 추가
    .from(item)
    .fetch();

위 코드는 SELECT DISTINCT 쿼리를 생성합니다.


변경점에 대한 설명

distinct() 메서드 사용 자체는 버전 3.x와 5.x 사이에서 변화하지 않았습니다. 다만, QueryDSL 버전 4.x 이상부터 fetch() 사용이 권장되며 더 이상 list() 등의 메서드를 사용하지 않는 점이 관련 문법 변화입니다.

QueryDSL 5.x에서의 사용

현재 최신 QueryDSL에서도 distinct()를 사용하는 방법은 위와 같으며, 아래처럼 페이징, 정렬, 서브쿼리 등과 함께 사용이 가능합니다.

List<ItemDTO> result = queryFactory
    .select(Projections.bean(ItemDTO.class, item.name, item.price))
    .distinct()
    .from(item)
    .where(item.price.gt(1000))
    .orderBy(item.name.asc())
    .fetch();

요약

  1. query.distinct()의 사용법은 현재 버전에서도 동일합니다.
  2. QueryDSL 4.x 이후부터는 fetch()를 사용해 데이터를 조회하는 것이 권장됩니다.
  3. QueryDSL의 다른 기능과 distinct()를 조합해 사용할 수 있습니다.

버전에 따라 기본적인 동작 방식은 유지되므로 최신 버전에서도 이와 같은 방법으로 활용 가능합니다.

수정, 삭제 배치 쿼리

QueryDSL에서 수정(Update)와 삭제(Delete) 배치 쿼리는 대량의 데이터 변경 작업을 효율적으로 처리하기 위한 방법으로, JPQL이나 SQL의 배치 쿼리와 유사한 방식으로 동작합니다.

QueryDSL에서는 수정 및 삭제 배치 작업을 수행할 때 com.querydsl.jpa.impl.JPAUpdateClauseJPADeleteClause 클래스를 사용합니다.


1. 수정(Update) 배치 쿼리

수정 배치 쿼리는 여러 행의 데이터를 한 번에 변경하는 작업입니다. QueryDSL에서 JPAUpdateClause를 사용하여 수행됩니다.

예제 코드:

long updatedCount = new JPAUpdateClause(entityManager, qItem)
    .where(qItem.price.lt(1000)) // 가격이 1000보다 작은 항목만
    .set(qItem.price, 2000)      // 가격을 2000으로 업데이트
    .execute();

생성되는 SQL:

UPDATE item
SET price = 2000
WHERE price < 1000;
  • set 메서드: 특정 필드 값을 설정.
  • where 조건: 어떤 데이터를 수정할지 결정.
  • execute 메서드: 배치 작업 실행, 영향을 받은 행 수를 반환.

2. 삭제(Delete) 배치 쿼리

삭제 배치 쿼리는 조건에 맞는 데이터를 한 번에 삭제합니다. QueryDSL에서 JPADeleteClause를 사용하여 수행됩니다.

예제 코드:

long deletedCount = new JPADeleteClause(entityManager, qItem)
    .where(qItem.price.lt(500)) // 가격이 500보다 작은 항목만 삭제
    .execute();

생성되는 SQL:

DELETE FROM item
WHERE price < 500;
  • where 조건: 어떤 데이터를 삭제할지 결정.
  • execute 메서드: 배치 작업 실행, 영향을 받은 행 수를 반환.

3. 주의사항

  • 영속성 컨텍스트와의 동기화:
    • 배치 쿼리는 영속성 컨텍스트를 무시하고 직접 데이터베이스를 변경합니다.
    • 따라서, 영속성 컨텍스트에 있는 데이터와 데이터베이스의 상태가 불일치할 수 있으므로, 배치 쿼리 실행 후에는 EntityManager.clear()를 호출하여 영속성 컨텍스트를 초기화하는 것이 권장됩니다.

예시:

entityManager.clear();
  • 트랜잭션: 배치 쿼리는 데이터베이스에 직접 반영되므로 트랜잭션 안에서 실행해야 데이터 무결성을 유지할 수 있습니다.

4. 실제 사용 예

수정 및 삭제 배치 쿼리는 대량의 데이터 처리, 데이터 정리, 조건 기반 삭제 작업 등에 자주 사용됩니다. 성능 향상을 위해 엔티티 객체를 개별적으로 변경하는 대신 한 번의 쿼리로 처리할 수 있다는 점이 주요 장점입니다.

동적 쿼리

QueryDSL에서 동적 쿼리(Dynamic Query)는 조건에 따라 쿼리를 유연하게 구성하는 방법입니다. 동적 쿼리는 주로 사용자가 선택하는 조건에 따라 쿼리 내용이 달라지거나, 조건이 있을 때만 쿼리를 추가하는 방식으로 처리됩니다.

예전에는 BooleanBuilder를 많이 사용하여 조건을 동적으로 구성했지만, 최신 버전의 QueryDSL에서는 이를 다른 방식으로 처리할 수 있습니다. BooleanBuilder는 여러 조건을 논리적으로 결합하는 데 유용한 도구였지만, 이제는 BooleanExpression이나 Predicate를 직접 사용하거나 where() 메서드를 체이닝하는 방식으로 동적 쿼리를 구성할 수 있습니다.

동적 쿼리 구성 방법:

  1. BooleanExpression 사용:
    BooleanExpression은 조건을 동적으로 추가할 때 유용한 QueryDSL의 기본 조건식입니다. 이를 통해 조건을 설정하고, 여러 조건을 조합할 수 있습니다.

    예시:

    QMember qMember = QMember.member;
    BooleanExpression usernameEq = qMember.username.eq("kim");
    BooleanExpression ageGoe = qMember.age.goe(30);
    
    // 동적 쿼리 구성
    JPAQuery query = new JPAQuery<>(entityManager);
    List<Member> members = query.select(qMember)
                                .from(qMember)
                                .where(usernameEq.and(ageGoe)) // 조건을 결합
                                .fetch();

goe는 QueryDSL에서 사용되는 메서드로, greaterThanEqual의 약어입니다. 이 메서드는 "이상" 조건을 나타내며, 주어진 값보다 크거나 같은 값인지 확인합니다. 예를 들어, qMember.age.goe(30)age 필드가 30 이상인 경우를 찾는 조건을 의미합니다.

즉, goegreater than or equal to를 표현하며, JPA에서 사용할 수 있는 범위 조건 중 하나입니다. 이와 유사한 다른 조건들도 존재합니다:

  • gt: greater than (초과)
  • lt: less than (미만)
  • loe: less than or equal to (이하)

이 메서드는 숫자뿐만 아니라 날짜와 문자열 비교에도 사용할 수 있습니다.

  1. 조건에 따라 where() 체이닝:
    조건이 있을 때만 where() 절을 추가하여 쿼리의 조건을 유동적으로 변경할 수 있습니다. 이 방식은 코드가 깔끔하고 이해하기 쉽습니다.

    예시:

    JPAQuery query = new JPAQuery<>(entityManager);
    QMember qMember = QMember.member;
    
    // 동적 조건
    BooleanBuilder builder = new BooleanBuilder();
    if (username != null) {
        builder.and(qMember.username.eq(username));
    }
    if (age != null) {
        builder.and(qMember.age.goe(age));
    }
    
    // 쿼리 실행
    List<Member> members = query.select(qMember)
                                .from(qMember)
                                .where(builder)  // 동적 조건 추가
                                .fetch();
  2. Predicate 사용:
    Predicate는 쿼리에서 조건을 표현할 때 사용되며, Java의 java.util.function.Predicate 인터페이스를 기반으로 동적 조건을 작성할 수 있습니다. PredicateBooleanExpression보다 좀 더 유연하게 쿼리 조건을 작성할 수 있습니다.

    예시:

    QMember qMember = QMember.member;
    
    Predicate predicate = qMember.username.eq("kim").and(qMember.age.goe(30));
    
    List<Member> members = new JPAQuery<>(entityManager)
                            .select(qMember)
                            .from(qMember)
                            .where(predicate)
                            .fetch();

동적 쿼리의 장점:

  • 유연성: 조건에 따라 쿼리 구조를 유동적으로 변경할 수 있어 다양한 상황에 대응할 수 있습니다.
  • 성능 향상: 조건을 동적으로 추가하므로 불필요한 쿼리 조건을 피할 수 있어 성능을 최적화할 수 있습니다.
  • 가독성: 코드에서 조건을 명확하게 분리하고, 조건을 체이닝하거나 빌더 패턴을 사용할 수 있어 코드가 더 직관적이고 유지보수가 쉬워집니다.

결론:

QueryDSL의 최신 버전에서는 BooleanBuilder 대신 BooleanExpression이나 Predicate를 사용하여 동적 쿼리를 구성합니다. 이를 통해 유연하고 확장 가능한 쿼리 생성이 가능하며, 코드가 간결하고 가독성이 좋습니다.

메소드 위임 Delegate methods

@QueryDelegate는 QueryDSL에서 특정 엔티티에 대한 커스텀 메서드를 생성하는 기능입니다. 이 어노테이션을 사용하면, 특정 엔티티 클래스에 메서드 위임을 정의할 수 있습니다. 이를 통해, 엔티티 클래스에서 쿼리 로직을 더 간결하고 재사용 가능한 형태로 만들 수 있습니다.

메서드 위임 예시:

예를 들어, Item 엔티티에 대한 isExpensive라는 조건을 쿼리에서 쉽게 사용할 수 있도록 메서드를 위임한다고 할 때, 다음과 같은 방식으로 사용할 수 있습니다.

@QueryDelegate(Item.class)
public static BooleanExpression isExpensive(QItem item, Integer price) {
    return item.price.gt(price); // 가격이 특정 값 이상인 항목을 찾는 메서드
}

위와 같은 방식으로 @QueryDelegate를 사용하면, QItem 클래스에서 isExpensive 메서드가 자동으로 생성됩니다. 이 메서드는 QItem 객체에서 조건을 직접 작성하는 것처럼 사용할 수 있습니다.

QItem 클래스에 생성된 메서드:

@QueryDelegate를 사용하면 QItem 클래스에 아래와 같은 메서드가 생성됩니다.

public class QItem extends EntityPathBase<Item> {
    // 기존 필드들
    
    public final NumberPath<Integer> price = createNumber("price", Integer.class);

    public QItem(String variable) {
        super(Item.class, forVariable(variable));
    }

    // QueryDelegate로 생성된 메서드
    public BooleanExpression isExpensive(Integer price) {
        return this.price.gt(price); // 가격이 특정 값 이상인 조건
    }
}

예전 버전의 QueryDSL에서는 com.mysema.query.types.expr.BooleanExpression을 명시적으로 사용해야 했습니다. 하지만 최신 버전에서는 기본적으로 BooleanExpression을 임포트할 수 있기 때문에, 패키지 이름을 생략할 수 있습니다.
(com.querydsl.core.types.dsl.BooleanExpression을 임포트 필요)

사용 방법:

이렇게 생성된 isExpensive 메서드는 쿼리에서 다음과 같이 사용될 수 있습니다.

QItem item = QItem.item;
JPAQuery<?> query = new JPAQuery<Void>(em);

List<Item> expensiveItems = query.from(item)
                                 .where(item.isExpensive(100))  // 100 이상인 가격 조건
                                 .fetch();

요약:

  • @QueryDelegate는 QueryDSL에서 엔티티에 대해 커스텀 쿼리 메서드를 정의할 수 있게 해줍니다.
  • QItem과 같은 Q타입 클래스에 자동으로 생성된 메서드를 사용하여 쿼리 로직을 더 깔끔하고 재사용 가능하게 만듭니다.
  • isExpensive와 같은 메서드는 QItem 클래스에서 조건을 직접 다룰 수 있게 하며, 쿼리 작성이 더 직관적이고 편리해집니다.

이 기능은 QueryDSL을 좀 더 효율적으로 사용할 수 있는 방법 중 하나입니다.

참조 : [자바 ORM 표준 JPA 프로그래밍]

profile
코드 위에서 춤추고 싶어요

0개의 댓글