Querydsl - 레퍼런스 문서

Timo Westkämper

Samppa Saarela

Vesa Marttila

Lassi Immonen

Ruben Dijkstra

John Tims

4.0.0

이 저작물은 Apache License, Version 2.0에 따라 누구나 사용, 편집, 복사, 재배포 가능합니다.


차례

서문
1. Introduction
1.1. Background
1.2. 원칙
2. 튜토리얼
2.1. JPA 쿼리
2.1.1. 메이븐 통합
2.1.2. Ant 통합
2.1.3. Roo에서 Querydsl JPA 사용하기
2.1.4. hbm.xml 파일에서 쿼리 모델 생성하기
2.1.5. 쿼리 타입 사용하기
2.1.6. 쿼리
2.1.7. 조인
2.1.8. 일반 용법
2.1.9. 정렬
2.1.10. 그룹핑
2.1.11. DeleteClause
2.1.12. UpdateClause
2.1.13. 서브쿼리
2.1.14. 원래의 JPA Query 구하기
2.1.15. JPA 쿼리에서 네이티브 SQL 사용하기
2.2. JDO 쿼리
2.2.1. 메이븐 통합
2.2.2. Ant 통합
2.2.3. 쿼리 타입 사용하기
2.2.4. 쿼리
2.2.5. 일반 용법
2.2.6. 정렬
2.2.7. 그룹핑
2.2.8. DeleteClause
2.2.9. 서브쿼리
2.2.10. 네티이브 SQL 사용하기
2.3. SQL 쿼리
2.3.1. 메이븐 통합
2.3.2. 메이븐을 통한 코드 생성
2.3.3. ANT를 통한 코드 생성
2.3.4. 쿼리 타입 만들기
2.3.5. 설정
2.3.6. 쿼리
2.3.7. 일반 용법
2.3.8. 조인
2.3.9. 정렬
2.3.10. 그룹핑
2.3.11. 서브쿼리
2.3.12. 리터럴 조회
2.3.13. 쿼리 확장 지원
2.3.14. 윈도우 함수
2.3.15. 다른 SQL 표현식
2.3.16. DML 명령 사용하기
2.3.16.1. 삽입
2.3.16.2. 수정
2.3.16.3. 삭제
2.3.17. DMLClause의 배치 지원
2.3.18. 빈 클래스 생성
2.3.19. SQL 쿼리와 바인딩 추출하기
2.3.20. 커스텀 타입
2.3.21. Query와 Clause 리스닝
2.4. 루신 쿼리
2.4.1. Maven integration
2.4.2. 쿼리 타입 생성
2.4.3. 쿼리
2.4.4. 일반 용법
2.4.5. 정렬
2.4.6. 결과 개수 제한
2.4.7. 오프셋
2.4.8. 퍼지(fuzzy) 검색
2.4.9. 루신 필터를 쿼리에 적용하기
2.5. Hibernate Search 쿼리
2.5.1. Querydsl 쿼리 타입 생성
2.5.2. 쿼리
2.5.3. 일반 용법
2.6. Mongodb 쿼리
2.6.1. 메이븐 통합
2.6.2. 쿼리
2.6.3. 일반 용법
2.6.4. 정렬
2.6.5. 결과 개수 제한
2.6.6. 오프셋
2.6.7. 공간(Geospatial) 쿼리
2.6.8. 관련 필드만 선택하기
2.7. 콜렉션 쿼리
2.7.1. 생성된 쿼리 타입 없이 사용하기
2.7.2. 생성된 쿼리 타입을 갖고 사용하기
2.7.3. 메이븐 통합
2.7.4. Ant 통합
2.7.5. Hamcrest matchers
2.8. Scala에서 쿼리하기
2.8.1. Scala를 위한 DSL 표현
2.8.2. 향상된 프로젝션
2.8.3. SQL을 이용한 쿼리
2.8.3.1. 컴팩트 쿼리
2.8.3.2. 코드 생성
2.8.4. 다른 백엔드에 대한 쿼리
3. 일반 사용법
3.1. 쿼리 생성
3.1.1. 복합 조건(complex predicates)
3.1.2. 동적 표현식
3.1.3. 동적 경로
3.1.4. Case 표현식
3.1.5. Casting 표현식
3.1.6. 리터럴 선택
3.2. 결과 처리
3.2.1. 다중 컬럼 리턴
3.2.2. 빈 생성(population)
3.2.3. 생성자 사용
3.2.4. 결과 집합(aggregation)
3.3. 코드 생성
3.3.1. 경로 초기화
3.3.2. 커스터마이징
3.3.3. 커스텀 타입 매핑
3.3.4. 위임 메서드(Delegate methods)
3.3.5. 애노테이션 비적용 타입
3.3.6. 클래스패스 기반 코드 생성
3.3.6.1. 메이븐 사용법
3.3.6.2. Scala 지원
3.4. 별칭 사용법
4. 문제해결
4.1. 불충분한 타입 인자
4.2. 멀티쓰레드 환경에서 Querydsl Q타입의 초기화
4.3. JDK5 사용

서문

Querydsl 정적 타입을 이용해서 SQL과 같은 쿼리를 생성할 수 있도록 해 주는 프레임워크다. 문자열로 작성하거나 XML 파일에 쿼리를 작성하는 대신, Querydsl이 제공하는 플루언트(Fluent) API를 이용해서 쿼리를 생성할 수 있다.

단순 문자열과 비교해서 Fluent API를 사용할 때의 장점은 다음과 같다.

  • IDE의 코드 자동 완성 기능 사용

  • 문법적으로 잘못된 쿼리를 허용하지 않음

  • 도메인 타입과 프로퍼티를 안전하게 참조할 수 있음

  • 도메인 타입의 리팩토링을 더 잘 할 수 있음

1장. Introduction

1.1. Background

Querydsl은 타입에 안전한 방식으로 HQL 쿼리를 실행하기 위한 목적으로 만들어졌다. HQL 쿼리를 작성하다보면 String 연결을 이용하게 되고, 이는 결과적으로 읽기 어려운 코드를 만드는 문제를 야기한다. String을 이용해서 도메인 타입과 프로퍼티를 참조하다보면 오타 등으로 잘못된 참조를 하게 될 수 있으며, 이는 String을 이용해서 HQL 작성할 때 발생하는 또 다른 문제다.

타입에 안전하도록 도메인 모델을 변경하면 소프트웨어 개발에서 큰 이득을 얻게 된다. 도메인의 변경이 직접적으로 쿼리에 반영되고, 쿼리 작성 과정에서 코드 자동완성 기능을 사용함으로써 쿼리를 더 빠르고 안전하게 만들 수 있게 된다.

Querydsl의 최초 쿼리 언어 대상은 Hibernate의 HQL이었으나, 현재는 JPA, JDO, JDBC, Lucene, Hibernate Search, MongoDB, 콜렉션 그리고 RDFBean을 지원한다.

1.2. 원칙

Querydsl의 핵심 원칙은 타입 안정성(Type safety)이다. 도메인 타입의 프로퍼티를 반영해서 생성한 쿼리 타입을 이용해서 쿼리를 작성하게 된다. 또한, 완전히 타입에 안전한 방법으로 함수/메서드 호출이 이루어진다.

또 다른 중요한 원칙은 일관성(consistency)이다. 기반 기술에 상관없이 쿼리 경로와 오퍼레이션은 모두 동일하며, Query 인터페이스는 공통의 상위 인터페이스를 갖는다.

모든 쿼리 인스턴스는 여러 차례 재사용 가능하다. 쿼리 실행 이후 페이징 데이터와 프로젝션 정의는 제거된다.

Javadoc에서 com.querydsl.core.Query, com.querydsl.core.Projectable 그리고 com.querydsl.query.types.Expression의 내용을 보면 Querydsl 쿼리와 표현 타입이 제공하는 표현력을 알게 될 것이다.

2장. 튜토리얼

일반적인 시작 안내 문서 대신 Querydsl이 지원하는 주요 백엔드 기술에 대한 안내 문서를 제공한다.

2.1. JPA 쿼리

Querydsl은 영속 도메인 모델에 대해 쿼리할 수 있는 범용적인 정적 타입 구문을 정의하고 있다. JDO와 JPA는 Querydsl이 지원하는 주요 기술이다. 이 안내 문서에서는 JPA와 함께 Querydsl을 사용하는 방법을 설명한다.

Querydsl은 JPQL과 Criteria 쿼리를 모두 대체할 수 있다. Querydsl은 Criteria 쿼리의 동적인 특징과 JPQL의 표현력을 타입에 안전한 방법으로 제공한다.

2.1.1. 메이븐 통합

메이븐 프로젝트의 의존 설정에 다음을 추가한다.

<dependency>
  <groupId>com.querydsl</groupId>
  <artifactId>querydsl-apt</artifactId>
  <version>${querydsl.version}</version>
  <scope>provided</scope>
</dependency>

<dependency>
  <groupId>com.querydsl</groupId>
  <artifactId>querydsl-jpa</artifactId>
  <version>${querydsl.version}</version>
</dependency>

<dependency>
  <groupId>org.slf4j</groupId>
  <artifactId>slf4j-log4j12</artifactId>
  <version>1.6.1</version>
</dependency>

다음으로 메이븐 APT 플러그인을 설정한다.

<project>
  <build>
  <plugins>
    ...
    <plugin>
      <groupId>com.mysema.maven</groupId>
      <artifactId>apt-maven-plugin</artifactId>
      <version>1.1.3</version>
      <executions>
        <execution>
          <goals>
            <goal>process</goal>
          </goals>
          <configuration>
            <outputDirectory>target/generated-sources/java</outputDirectory>
            <processor>com.querydsl.apt.jpa.JPAAnnotationProcessor</processor>
          </configuration>
        </execution>
      </executions>
    </plugin>
    ...
  </plugins>
  </build>
</project>

JPAAnnotationProcessor는 javax.persistence.Entity 애노테이션을 가진 도메인 타입을 찾아서 쿼리 타입을 생성한다.

도메인 타입으로 Hibernate 애노테이션을 사용하면, APT 프로세서로 com.querydsl.apt.hibernate.HibernateAnnotationProcessor를 사용해야 한다.

mvn clean install 을 실행하면, target/generated-sources/java 디렉토리에 Query 타입이 생성된다.

이클립스를 사용할 경우, mvn eclipse:eclipse 를 실행하면 target/generated-sources/java 디렉토리가 소스 폴더에 추가된다.

생성된 Query 타입을 이용하면 JPA 쿼리 인스턴스와 쿼리 도메인 모델 인스턴스를 생성할 수 있다.

2.1.2. Ant 통합

클래스패스에 full-deps에 포함된 jar 파일들을 위치시키고, 다음 태스크를 이용해서 Querydsl 코드를 생성한다.

    <!-- APT based code generation -->
    <javac srcdir="${src}" classpathref="cp">
      <compilerarg value="-proc:only"/>
      <compilerarg value="-processor"/>
      <compilerarg value="com.querydsl.apt.jpa.JPAAnnotationProcessor"/>
      <compilerarg value="-s"/>
      <compilerarg value="${generated}"/>
    </javac>

    <!-- compilation -->
    <javac classpathref="cp" destdir="${build}">
      <src path="${src}"/>
      <src path="${generated}"/>
    </javac>

src를 메인 소스 폴더로 변경하고, generated를 생성된 소스를 위한 폴더로 변경하고, build를 클래스 생성 폴더로 변경한다.

2.1.3. Roo에서 Querydsl JPA 사용하기

스프링 Roo에서 Querydsl JPA를 사용한다면, com.querydsl.apt.jpa.JPAAnnotationProcessor 대신 com.querydsl.apt.roo.RooAnnotationProcessor를 사용할 수 있다. RooAnnotationProcessor는 @Entity가 적용된 클래스 대신 @RooJpaEntity@RooJpaActiveRecord 애노테이션이 적용된 클래스를 처리한다.

APT 기반의 코드 생성 기능은 AspectJ IDT에는 잘 동작하지 않는다.

2.1.4. hbm.xml 파일에서 쿼리 모델 생성하기

하이버네이트에서 XML 기반 설정을 사용하고 있다면, Querydsl 모델을 생성하기 위해 XML 메타정보를 사용할 수 있다.

com.querydsl.jpa.codegen.HibernateDomainExporter가 이 기능을 제공한다.

HibernateDomainExporter exporter = new HibernateDomainExporter(
  "Q",                     // name prefix
  new File("target/gen3"), // target folder
  configuration);          // instance of org.hibernate.cfg.Configuration

exporter.export();

HibernateDomainExporter는 리플렉션을 이용해서 도메인의 프로퍼티 타입을 확인하기 때문에, HibernateDomainExporter를 실행하려면 클래스패스에 도메인 타입이 위치해야 한다.

모든 JPA 어노테이션은 무시되지만, @QueryInit이나 @QueryType과 같은 Querydsl 애노테이션은 처리한다.

2.1.5. 쿼리 타입 사용하기

Querydsl을 이용해서 쿼리를 작성하려면, 변수와 Query 구현체를 생성해야 한다. 먼저 변수부터 시작해보자.

다음과 같은 도메인 타입이 있다고 가정하다.

@Entity
public class Customer {
    private String firstName;
    private String lastName;

    public String getFirstName(){
        return firstName;
    }

    public String getLastName(){
        return lastName;
    }

    public void setFirstName(String fn){
        firstName = fn;
    }

    public void setLastName(String ln) {
        lastName = ln;
    }
}

Querydsl은 Customer와 동일한 패키지에 QCustomer라는 이름을 가진 쿼리 타입을 생성한다. Querydsl 쿼리에서 Customer 타입을 위한 정적 타입 변수로 QCustomer를 사용한다.

QCustomer는 기본 인스턴스 변수를 갖고 있으며, 다음과 같이 정적 필드로 접근할 수 있다.

QCustomer customer = QCustomer.customer;

다음처럼 Customer 변수를 직접 정의할 수도 있다.

QCustomer customer = new QCustomer("myCustomer");

2.1.6. 쿼리

Querdsl JPA 모듈은 JPA와 Hibernate API를 모두 지원한다.

JPA API를 사용하려면 다음과 같이 JPAQuery 인스턴스를 사용하면 된다.

// where entityManager is a JPA EntityManager
JPAQuery query = new JPAQuery(entityManager);

Hibernate를 사용한다면, HibernateQuery를 사용하면 된다.

// where session is a Hibernate session
HibernateQuery query = new HibernateQuery(session);

JPAQuery와 HibernateQuery는 둘 다 JPQLQuery 인터페이스를 구현하고 있다.

firstName 프로퍼티가 Bob인 Customer를 조회하고 싶다면 다음의 쿼리를 사용하면 된다.

QCustomer customer = QCustomer.customer;
JPAQuery query = new JPAQuery(entityManager);
Customer bob = query.from(customer)
  .where(customer.firstName.eq("Bob"))
  .uniqueResult(customer);

from 메서드는 쿼리 대상(소스)을 지정하고, where 부분은 필터를 정의하고, uniqueResult는 프로젝션을 정의하고, 1개 결과만 리턴하라고 지시한다.

여러 소스로부터 쿼리를 만들고 싶다면 다음처럼 쿼리를 사용한다.

QCustomer customer = QCustomer.customer;
QCompany company = QCompany.company;
query.from(customer, company);

여러 필터를 사용하는 방법은 다음과 같다.

query.from(customer)
    .where(customer.firstName.eq("Bob"), customer.lastName.eq("Wilson"));

또는, 다음과 같이 해도 된다.

query.from(customer)
    .where(customer.firstName.eq("Bob").and(customer.lastName.eq("Wilson")));

위 코드를 JPQL 쿼리로 작성하면 다음과 같을 것이다.

from Customer as customer
    where customer.firstName = "Bob" and customer.lastName = "Wilson"

필터 조건을 or로 조합하고 싶다면 다음 패턴을 사용한다.

query.from(customer)
    .where(customer.firstName.eq("Bob").or(customer.lastName.eq("Wilson")));

2.1.7. 조인

Querydsl은 JPQL의 이너 조인, 조인, 레프트 조인, 풀조인을 지원한다. 조인 역시 타입에 안전하며 다음 패턴에 따라 작성한다.

QCat cat = QCat.cat;
QCat mate = new QCat("mate");
QCate kitten = new QCat("kitten");
query.from(cat)
    .innerJoin(cat.mate, mate)
    .leftJoin(cat.kittens, kitten)
    .list(cat);

위 쿼리를 JPQL로 작성하면 다음과 같다.

from Cat as cat
    inner join cat.mate as mate
    left outer join cat.kittens as kitten

다음은 조인을 사용하는 또 다른 예다.

query.from(cat)
    .leftJoin(cat.kittens, kitten)
    .on(kitten.bodyWeight.gt(10.0))
    .list(cat);

위 코드의 JPQL 버전은 다음과 같다.

from Cat as cat
    left join cat.kittens as kitten
    on kitten.bodyWeight > 10.0

2.1.8. 일반 용법

JPQLQuery 인터페이스의 cascading 메서드는 다음과 같다.

from: 쿼리 소스를 추가한다.

innerJoin, join, leftJoin, rightJoin, on: 조인 부분을 추가한다. 조인 메서드에서 첫 번째 인자는 조인 소스이고, 두 번재 인자는 대상(별칭)이다.

where: 쿼리 필터를 추가한다. 가변인자나 and/or 메서드를 이용해서 필터를 추가한다.

groupBy: 가변인자 형식의 인자를 기준으로 그룹을 추가한다.

having: Predicate 표현식을 이용해서 "group by" 그룹핑의 필터를 추가한다.

orderBy: 정렬 표현식을 이용해서 정렬 순서를 지정한다. 숫자나 문자열에 대해서는 asc()나 desc()를 사용하고, OrderSpecifier에 접근하기 위해 다른 비교 표현식을 사용한다.

limit, offset, restrict: 결과의 페이징을 설정한다. limit은 최대 결과 개수, offset은 결과의 시작 행, restrict는 limit과 offset을 함께 정의한다.

2.1.9. 정렬

정렬을 위한 구문은 다음과 같다.

QCustomer customer = QCustomer.customer;
query.from(customer)
    .orderBy(customer.lastName.asc(), customer.firstName.desc())
    .list(customer);

위 코드는 다음의 JPQL과 동일하다.

from Customer as customer
    order by customer.lastName asc, customer.firstName desc

2.1.10. 그룹핑

그룹핑은 다음과 같은 코드로 처리한다.

query.from(customer)
    .groupBy(customer.lastName)
    .list(customer.lastName);

동등한 JPQL은 다음고 같다.

select customer.lastName
    from Customer as customer
    group by customer.lastName

2.1.11. DeleteClause

Querydsl JPA에서 DeleteClause는 간단한 delete-where-execute 형태를 취한다. 다음은 몇 가지 예다.

QCustomer customer = QCustomer.customer;
// delete all customers
new JPADeleteClause(entityManager, customer).execute();
// delete all customers with a level less than 3
new JPADeleteClause(entityManager, customer).where(customer.level.lt(3)).execute();

JPADeleteClause 생성자의 두 번째 파라미터는 삭제할 엔티티 대상이다. where는 필요에 따라 추가할 수 있으며, execute를 실행하면 삭제를 수행하고 삭제된 엔티티의 개수를 리턴한다.

Hibernate 이용시, HibernateDeleteClause를 사용하면 된다.

JPA의 DML 절은 JPA 레벨의 영속성 전파 규칙을 따르지 않고, 2차 레벨 캐시와의 연동되지 않는다.

2.1.12. UpdateClause

Querydsl JPA의 UpdateClause은 간단한 update-set/where-execute 형태를 취한다. 다음은 몇 가지 예다.

QCustomer customer = QCustomer.customer;
// rename customers named Bob to Bobby
new JPAUpdateClause(session, customer).where(customer.name.eq("Bob"))
    .set(customer.name, "Bobby")
    .execute();

JPAUpdateClause 생성자의 두 번째 파라미터는 수정할 엔티티 대상이다. set은 SQL의 update 스타일로 프로퍼티 수정을 정의하고, execute를 실행하면 수정을 실행하고 수정된 엔티티의 개수를 리턴한다.

Hibernate 이용시, HibernateUpdateClause를 사용한다.

JPA에서 DML 절은 JPA 레벨의 영속성 전파 규칙을 따르지 않고, 2차 레벨 캐시와 연동되지 않는다.

2.1.13. 서브쿼리

서브쿼리를 만들려면 JPASubQuery를 사용하면 된다. 서브쿼리를 만들기 위해 from 메서드로 쿼리 파라미터를 정의하고, unique나 list를 이용한다. unique는 단일 결과를 위해 사용하고 list는 리스트 결과를 위해 사용한다. 서브쿼리도 쿼리처럼 타입에 안전한 Querydsl 표현식이다.

QDepartment department = QDepartment.department;
QDepartment d = new QDepartment("d");
query.from(department)
    .where(department.employees.size().eq(
        new JPASubQuery().from(d).unique(d.employees.size().max())
     )).list(department);

다른 예제

QEmployee employee = QEmployee.employee;
QEmployee e = new QEmployee("e");
query.from(employee)
    .where(employee.weeklyhours.gt(
        new JPASubQuery().from(employee.department.employees, e)
        .where(e.manager.eq(employee.manager))
        .unique(e.weeklyhours.avg())
    )).list(employee);

Hibernate를 사용할 경우, HibernateSubQuery를 사용하면 된다.

2.1.14. 원래의 JPA Query 구하기

만약 쿼리를 실행하기 전에 JPA Query를 구하고 싶다면, 다음 코드를 사용한다.

JPAQuery query = new JPAQuery(entityManager);
Query jpaQuery = query.from(employee).createQuery(employee);
// ...
List results = jpaQuery.getResultList();

2.1.15. JPA 쿼리에서 네이티브 SQL 사용하기

JPASQLQuery 클래스를 사용하면 JPA의 네이티브 SQL을 Querydsl에서 사용할 수 있다.

이걸 사용하려면 SQL 스키마를 위한 Querydsl 쿼라 티입을 생성해야 한다. 다음은 이를 위한 Maven 설정 예를 보여주고 있다.

<project>
  <build>
    <plugins>
      ...
      <plugin>
        <groupId>com.querydsl</groupId>
        <artifactId>querydsl-maven-plugin</artifactId>
        <version>${querydsl.version}</version>
        <executions>
          <execution>
            <goals>
              <goal>export</goal>
            </goals>
          </execution>
        </executions>
        <configuration>
          <jdbcDriver>org.apache.derby.jdbc.EmbeddedDriver</jdbcDriver>
          <jdbcUrl>jdbc:derby:target/demoDB;create=true</jdbcUrl>
          <packageName>com.mycompany.mydomain</packageName>
          <targetFolder>${project.basedir}/target/generated-sources/java</targetFolder>
        </configuration>
        <dependencies>
          <dependency>
            <groupId>org.apache.derby</groupId>
            <artifactId>derby</artifactId>
            <version>${derby.version}</version>
          </dependency>
        </dependencies>
      </plugin>
      ...
    </plugins>
  </build>
</project>

지정한 위치에 쿼리 타입을 성공적으로 생성했다면, 쿼리에서 그 타입을 사용할 수 있다.

한 개 컬럼 쿼리:

// serialization templates
SQLTemplates templates = new DerbyTemplates();
// query types (S* for SQL, Q* for domain types)
SAnimal cat = new SAnimal("cat");
SAnimal mate = new SAnimal("mate");
QCat catEntity = QCat.cat;

JPASQLQuery query = new JPASQLQuery(entityManager, templates);
List<String> names = query.from(cat).list(cat.name);

한 쿼리에서 엔티티(예, QCat)와 테이블(예, SAnimal)에 대한 참조를 섞어 쓰고 싶다면, 같은 변수명을 갖도록 해야 한다. SAnimal.animal은 "animal"이란 변수명을 가지므로 새로운 인스턴스 (new SAnimal("cat"))을 대신 사용했다.

다음과 같이 할 수도 있다.

QCat catEntity = QCat.cat;
SAnimal cat = new SAnimal(catEntity.getMetadata().getName());

다중 컬럼 쿼리:

query = new JPASQLQuery(entityManager, templates);
List<Object[]> rows = query.from(cat).list(cat.id, cat.name);

모든 컬럼 쿼리:

List<Object[]> rows = query.from(cat).list(cat.all());
 

SQL로 쿼리를 하고, 결과는 엔티티로 구하기:

query = new JPASQLQuery(entityManager, templates);
List<Cat> cats = query.from(cat).orderBy(cat.name.asc()).list(catEntity);

조인을 이용한 쿼리:

query = new JPASQLQuery(entityManager, templates);
cats = query.from(cat)
    .innerJoin(mate).on(cat.mateId.eq(mate.id))
    .where(cat.dtype.eq("Cat"), mate.dtype.eq("Cat"))
    .list(catEntity);

쿼리 결과를 DTO로 구하기:

query = new JPASQLQuery(entityManager, templates);
List<CatDTO> catDTOs = query.from(cat)
    .orderBy(cat.name.asc())
    .list(Projections.constructor(CatDTO.class, cat.id, cat.name));

JPA API 대신 하이버네이트 API를 사용한다면, HibernateSQLQuery를 사용한다.

2.2. JDO 쿼리

Querydsl은 영속 도메인 모델에 대해 쿼리할 수 있는 범용적인 정적 타입 구문을 정의하고 있다. JDO와 JPA는 Querydsl이 지원하는 주요 기술이다. 이 안내 문서에서는 JDO와 함께 Querydsl을 사용하는 방법을 설명한다.

2.2.1. 메이븐 통합

메이븐 프로젝트의 의존 설정에 다음을 추가한다.

<dependency>
  <groupId>com.querydsl</groupId>
  <artifactId>querydsl-apt</artifactId>
  <version>${querydsl.version}</version>
  <scope>provided</scope>
</dependency>

<dependency>
  <groupId>com.querydsl</groupId>
  <artifactId>querydsl-jdo</artifactId>
  <version>${querydsl.version}</version>
</dependency>

<dependency>
  <groupId>org.slf4j</groupId>
  <artifactId>slf4j-log4j12</artifactId>
  <version>1.6.1</version>
</dependency>

다음으로 Querydsl에서 쿼리 타입을 생성하기 위해 사용하는 메이븐 APT 플러그인을 설정한다.

<project>
  <build>
    <plugins>
      ...
      <plugin>
        <groupId>com.mysema.maven</groupId>
        <artifactId>apt-maven-plugin</artifactId>
        <version>1.1.3</version>
        <executions>
          <execution>
            <goals>
              <goal>process</goal>
            </goals>
            <configuration>
              <outputDirectory>target/generated-sources/java</outputDirectory>
              <processor>com.querydsl.apt.jdo.JDOAnnotationProcessor</processor>
            </configuration>
          </execution>
        </executions>
      </plugin>
    ...
    </plugins>
  </build>
</project>

JDOAnnotationProcessor는 javax.jdo.annotations.PersistenceCapable 애노테이션이 적용된 타입을 찾아서 그 타입을 위한 쿼리 타입을 생성한다.

`mvn clean install`을 실행하면, target/generated-sources/java 디렉토리에 Query 타입이 생성된다.

이클립스를 사용할 경우, `mvn eclipse:eclipse`을 실행하면 target/generated-sources/java 디렉토리가 소스 폴더에 추가된다.

생성된 Query 타입을 이용하면 JDO 쿼리 인스턴스와 쿼리 도메인 모델 인스턴스를 생성할 수 있다.

2.2.2. Ant 통합

클래스패스에 full-deps에 포함된 jar 파일들을 위치시키고, 다음 태스크를 이용해서 Querydsl 코드를 생성한다.

    <!-- APT based code generation -->
    <javac srcdir="${src}" classpathref="cp">
      <compilerarg value="-proc:only"/>
      <compilerarg value="-processor"/>
      <compilerarg value="com.querydsl.apt.jdo.JDOAnnotationProcessor"/>
      <compilerarg value="-s"/>
      <compilerarg value="${generated}"/>
    </javac>

    <!-- compilation -->
    <javac classpathref="cp" destdir="${build}">
      <src path="${src}"/>
      <src path="${generated}"/>
    </javac>

src를 메인 소스 폴더로 변경하고, generated를 생성된 소스를 위한 폴더로 변경하고, build를 클래스 생성 폴더로 변경한다.

2.2.3. 쿼리 타입 사용하기

Querydsl을 이용해서 쿼리를 작성하려면, 변수와 Query 구현체를 생성해야 한다. 먼저 변수부터 시작해보자.

다음과 같은 도메인 타입이 있다고 가정하다.

@PersistenceCapable
public class Customer {
  private String firstName;
  private String lastName;

  public String getFirstName() {
    return firstName;
  }

  public String getLastName() {
    return lastName;
  }

  public void setFirstName(String fn) {
    firstName = fn;
  }

  public void setLastName(String ln) {
    lastName = ln;
  }
}

Querydsl은 Customer와 동일한 패키지에 QCustomer라는 이름을 가진 쿼리 타입을 생성한다. Querydsl 쿼리에서 Customer 타입을 위한 정적 타입 변수로 QCustomer를 사용한다.

QCustomer는 기본 인스턴스 변수를 갖고 있으며, 다음과 같이 정적 필드로 접근할 수 있다.

QCustomer customer = QCustomer.customer;

다음처럼 Customer 변수를 직접 정의할 수도 있다.

QCustomer customer = new QCustomer("myCustomer");

QCustomer는 원래 Customer 타입의 모든 프로퍼티를 public 필드로 반영한다. firstName 필다는 다음과 같이 접근할 수 있다.

customer.firstName;

2.2.4. 쿼리

JDOQuery가 JDO 모듈을 위한 Query 구현체이며, 다음과 같이 인스턴스를 생성한다.

PersistenceManager pm = ...;
JDOQuery query = new JDOQuery (pm);

firstName 프로퍼티가 Bob인 Customer를 조회하고 싶다면 다음의 쿼리를 사용하면 된다.

QCustomer customer = QCustomer.customer;
JDOQuery query = new JDOQuery (pm);
Customer bob = query.from(customer)
  .where(customer.firstName.eq("Bob"))
  .uniqueResult(customer);
query.close();

from 메서드는 쿼리 대상(소스)을 지정하고, where 부분은 필터를 정의하고, uniqueResult는 프로젝션을 정의해서 1개 결과만 리턴하라고 지시한다.

여러 소스로부터 쿼리를 만들고 싶다면 다음처럼 쿼리를 사용한다.

QCustomer customer = QCustomer.customer;
QCompany company = QCompany.company;
query.from(customer, company);

여러 필터를 사용하는 방법은 다음과 같다.

query.from(customer)
    .where(customer.firstName.eq("Bob"), customer.lastName.eq("Wilson"));

또는, 다음과 같이 해도 된다.

query.from(customer)
    .where(customer.firstName.eq("Bob").and(customer.lastName.eq("Wilson")));

필터 조건을 or로 조합하고 싶다면 다음 패턴을 사용한다.

query.from(customer)
    .where(customer.firstName.eq("Bob").or(customer.lastName.eq("Wilson")));

2.2.5. 일반 용법

JDOQuery 클래스의 cascading 메서드는 다음과 같다.

from: 쿼리 소스를 추가한다. 첫 번째 인자는 메인 소스가 되고, 나머지는 변수로 취급한다.

where: 쿼리 필터를 추가한다. 가변인자나 and/or 메서드를 이용해서 필터를 추가한다.

groupBy: 가변인자 형식의 인자를 기준으로 그룹을 추가한다.

having: Predicate 표현식을 이용해서 "group by" 그룹핑의 필터를 추가한다.

orderBy: 정렬 표현식을 이용해서 정렬 순서를 지정한다. 숫자나 문자열에 대해서는 asc()나 desc()를 사용하고, OrderSpecifier에 접근하기 위해 다른 비교 표현식을 사용한다.

limit, offset, restrict: 결과의 페이징을 설정한다. limit은 최대 결과 개수, offset은 결과의 시작 행, restrict는 limit과 offset을 함께 정의한다.

2.2.6. 정렬

정렬을 위한 구문은 다음과 같다.

QCustomer customer = QCustomer.customer;
query.from(customer)
    .orderBy(customer.lastName.asc(), customer.firstName.desc())
    .list(customer);

2.2.7. 그룹핑

그룹핑은 다음과 같은 코드로 처리한다.

query.from(customer)
    .groupBy(customer.lastName)
    .list(customer.lastName);

2.2.8. DeleteClause

Querydsl JDO에서 DeleteClause는 간단한 delete-where-execute 형태를 취한다. 다음은 몇 가지 예다.

QCustomer customer = QCustomer.customer;
// delete all customers
new JDODeleteClause(pm, customer).execute();
// delete all customers with a level less than 3
new JDODeleteClause(pm, customer).where(customer.level.lt(3)).execute();

JDODeleteClause 생성자의 두 번째 파라미터는 삭제할 엔티티 대상이다. where는 필요에 따라 추가할 수 있으며, execute를 실행하면 삭제를 수행하고 삭제된 엔티티의 개수를 리턴한다.

2.2.9. 서브쿼리

서브쿼리를 만들려면 JDOSubQuery를 사용하면 된다. 서브쿼리를 만들기 위해 from 메서드로 쿼리 파라미터를 정의하고, unique나 list를 이용한다. unique는 단일 결과를 위해 사용하고 list는 리스트 결과를 위해 사용한다. 서브쿼리도 쿼리처럼 타입에 안전한 Querydsl 표현식이다.

QDepartment department = QDepartment.department;
QDepartment d = new QDepartment("d");
query.from(department)
    .where(department.employees.size().eq(
        new JDOSubQuery().from(d).unique(AggregationFunctions.max(d.employees.size()))
     )).list(department);

위 코드는 다음의 네이티브 JDO 쿼리를 표현한다.

      SELECT this FROM com.querydsl.jdo.models.company.Department
      WHERE this.employees.size() ==
      (SELECT max(d.employees.size()) FROM com.querydsl.jdo.models.company.Department d)
    

다른 예제

QEmployee employee = QEmployee.employee;
QEmployee e = new QEmployee("e");
query.from(employee)
    .where(employee.weeklyhours.gt(
        new JDOSubQuery().from(employee.department.employees, e)
        .where(e.manager.eq(employee.manager))
        .unique(AggregationFunctions.avg(e.weeklyhours))
    )).list(employee);

위 코드는 다음의 네이티브 JDO 쿼리를 표현한다.

      SELECT this FROM com.querydsl.jdo.models.company.Employee
      WHERE this.weeklyhours >
      (SELECT avg(e.weeklyhours) FROM this.department.employees e WHERE e.manager == this.manager)
    

2.2.10. 네티이브 SQL 사용하기

JDOSQLQuery 클래스를 사용하면 JDO의 네이티브 SQL을 Querydsl에서 사용할 수 있다.

이걸 사용하려면 SQL 스키마를 위한 Querydsl 쿼라 티입을 생성해야 한다. 다음은 이를 위한 Maven 설정 예를 보여주고 있다.

<project>
  <build>
    <plugins>
      ...
      <plugin>
        <groupId>com.querydsl</groupId>
        <artifactId>querydsl-maven-plugin</artifactId>
        <version>${querydsl.version}</version>
        <executions>
          <execution>
            <goals>
              <goal>export</goal>
            </goals>
          </execution>
        </executions>
        <configuration>
          <jdbcDriver>org.apache.derby.jdbc.EmbeddedDriver</jdbcDriver>
          <jdbcUrl>jdbc:derby:target/demoDB;create=true</jdbcUrl>
          <packageName>com.mycompany.mydomain</packageName>
          <targetFolder>${project.basedir}/target/generated-sources/java</targetFolder>
        </configuration>
        <dependencies>
          <dependency>
            <groupId>org.apache.derby</groupId>
            <artifactId>derby</artifactId>
            <version>${derby.version}</version>
          </dependency>
        </dependencies>
      </plugin>
      ...
    </plugins>
  </build>
</project>

지정한 위치에 쿼리 타입을 성공적으로 생성했다면, 쿼리에서 그 타입을 사용할 수 있다.

한 개 컬럼 쿼리:

// serialization templates
SQLTemplates templates = new DerbyTemplates();
// query types (S* for SQL, Q* for domain types)
SAnimal cat = new SAnimal("cat");
SAnimal mate = new SAnimal("mate");

JDOSQLQuery query = new JDOSQLQuery(pm, templates);
List<String> names = query.from(cat).list(cat.name);

다중 컬럼 쿼리:

query = new JDOSQLQuery(pm, templates);
List<Object[]> rows = query.from(cat).list(cat.id, cat.name);

모든 컬럼 쿼리:

List<Object[]> rows = query.from(cat).list(cat.all());
 

조인을 이용한 쿼리:

query = new JDOSQLQuery(pm, templates);
cats = query.from(cat)
    .innerJoin(mate).on(cat.mateId.eq(mate.id))
    .where(cat.dtype.eq("Cat"), mate.dtype.eq("Cat"))
    .list(catEntity);

쿼리 결과를 DTO로 구하기:

query = new JDOSQLQuery(pm, templates);
List<CatDTO> catDTOs = query.from(cat)
    .orderBy(cat.name.asc())
    .list(Projections.constructor(CatDTO.class, cat.id, cat.name));

2.3. SQL 쿼리

본 절에서는 SQL 모듈의 쿼라 타입 생성과 쿼리 기능을 설명한다.

2.3.1. 메이븐 통합

메이븐 프로젝트에 다음의 의존을 추가한다.

<dependency>
  <groupId>com.querydsl</groupId>
  <artifactId>querydsl-sql</artifactId>
  <version>${querydsl.version}</version>
</dependency>

<dependency>
  <groupId>com.querydsl</groupId>
  <artifactId>querydsl-sql-codegen</artifactId>
  <version>${querydsl.version}</version>
  <scope>provided</scope>
</dependency>

<dependency>
  <groupId>org.slf4j</groupId>
  <artifactId>slf4j-log4j12</artifactId>
  <version>1.6.1</version>
</dependency>

코드 생성을 메이븐이나 Ant에서 할 경우 querydsl-sql-codegen 의존은 생략할 수 있다.

2.3.2. 메이븐을 통한 코드 생성

코드 생성은 주로 메이븐 플러그인을 통해서 수행한다. 다음은 설정 예다.

<project>
  <build>
    <plugins>
      ...
      <plugin>
        <groupId>com.querydsl</groupId>
        <artifactId>querydsl-maven-plugin</artifactId>
        <version>${querydsl.version}</version>
        <executions>
          <execution>
            <goals>
              <goal>export</goal>
            </goals>
          </execution>
        </executions>
        <configuration>
          <jdbcDriver>org.apache.derby.jdbc.EmbeddedDriver</jdbcDriver>
          <jdbcUrl>jdbc:derby:target/demoDB;create=true</jdbcUrl>
          <packageName>com.myproject.domain</packageName>
          <targetFolder>${project.basedir}/target/generated-sources/java</targetFolder>
        </configuration>
        <dependencies>
          <dependency>
            <groupId>org.apache.derby</groupId>
            <artifactId>derby</artifactId>
            <version>${derby.version}</version>
          </dependency>
        </dependencies>
      </plugin>
      ...
    </plugins>
  </build>
</project>

컴파일 소스 루트 대신에 테스트 컴파일 소스 루트로 targetFolder를 추가하려면 test-export 골을 사용하면 된다.

표 2.1. 파라미터

이름설명
jdbcDriverJDBC 드라이버 클래스 이름
jdbcUrlJDBC URL
jdbcUserJDBC 사용자
jdbcPasswordJDBC 암호
namePrefix생성될 쿼리 클래스의 접두어 (기본: Q)
nameSuffix생성될 쿼리 클래스의 접미사 (기본: )
beanPrefix생성될 빈Bean 클래스의 접두어
beanSuffix생성될 빈 클래스의 접미사
packageName생성될 소스 파일이 위치할 패키지
beanPackageName빈 파일이 생성될 패키지 이름 (기본: packageName)
beanInterfaces빈 클래스에 추가할 인터페이스 목록 (기본: 없음)
beanAddToStringtrue로 지정하면 기본 toString() 구현을 생성 (기본: false)
beanAddFullConstructortrue로 지정하면 기본 생성자 외에 완전한 생성자를 생성 (기본: false)
beanPrintSupertypetrue로 지정하면 상위 타입을 출력 (기본: false)
schemaPattern스키마 이름 패턴. 반드시 데이터베이스에 존재하는 스키마 이름과 일치해야 한다. (기본: null)
tableNamePattern테이블 이름 패턴. 반드시 데이터베이스에 존재하는 테이블 이름과 일치해야 하며, 콤마로 구분해서 두 개 이상 지정할 수 있다. (기본: null)
targetFolder소스 파일을 생성할 폴더를 지정
namingStrategyClassNamingStrategy로 사용할 클래스 이름을 입력 (기본: DefaultNamingStrategy)
beanSerializerClassBeanSerializer로 사용할 클래스 이름 (기본: BeanSerializer)
serializerClassSerializer로 사용할 클래스 이름 (기본: MetaDataSerializer)
exportBeanstrue로 지정하면 빈을 함께 생성. 2.14.13 참고. (기본: false)
innerClassesForKeystrue로 지정하면 키를 내부 클래스로 생성 (기본: false)
validationAnnotationstrue로 지정하면 Validation 어노테이션의 직렬화를 가능하게 함 (기본: false)
columnAnnotationstrue로 지정하면 컬럼 어노테이션을 추출함 (기본: false)
createScalaSourcestrue로 지정하면 자바 소스 대신 Scala 소스로 추찰함 (기본: false)
schemaToPackagetrue로 지정하면 스키마 이름을 패키지에 붙임 (기본: false)
lowerCasetrue로 지정하면 이름을 소문자로 변환 (기본: false)
exportTablestrue로 지정하면 테이블을 추출 (기본: true)
exportViewstrue로 지정한 뷰를 추출 (기본: true)
exportPrimaryKeystrue로 지정하면 PK를 추출 (기본: true)
exportForeignKeystrue로 지정하면 외부키를 추출 (기본: true)
customTypes커스텀 사용자 타입 (기본: 없음)
typeMappings테이블.컬럼에서 자바 타입으로 매핑 (기본: 없음)
numericMappings크기/숫자에서 자바 타입으로 매핑 (기본: 없음)
imports생성된 쿼리 클래스에 추가할 자바 import 목록: 패키지의 경우 (.* 없이) 패키지 이름만(예, com.bar), 클래스의 경우 완전한 클래스 이름 (예, com.bar.Foo) 사용. (기본: 없음)

추가로 타입 구현을 등록하고 싶을 때 customTypes을 사용한다.

<customTypes>
  <customType>com.querydsl.sql.types.InputStreamType</customType>
</customTypes>

테이블.컬럼을 위한 자바 타입을 등록하고 싶을 때 typeMappings를 사용한다.

<typeMappings>
  <typeMapping>
    <table>IMAGE</table>
    <column>CONTENTS</column>
    <type>java.io.InputStream</type>
  </typeMapping>
</typeMappings>

숫자 매핑을 위한 기본 타입은 다음과 같다.

표 2.2. 숫자 매핑

크기자리(Digits)타입
> 180BigInteger
> 90Long
> 40Integer
> 20Short
> 00Byte
> 16> 0BigDecimal
> 0> 0Double

특정 크기/자리에 대한 커스텀 타입은 다음과 같이 설정한다.

<numericMappings>
  <numericMapping>
    <size>1</size>
    <digits>0</digits>
    <javaType>java.lang.Byte</javaType>
  </numericMapping>
</numericMappings>

Import를 사용하면 크로스 스키마 외래키 지원을 추가할 수 있다.

APT 기반 코드 생성과 비교할 때 특정 기능은 사용할 수 없다. (예, QueryDelegate 애노테이션 처리)

2.3.3. ANT를 통한 코드 생성

Querydsl-sql 모듈이 제공하는 com.querydsl.sql.codegen.ant.AntMetaDataExporter ANT 태스크는 ANT 태스크(어떤 ANT 태스크?)와 같은 기능을 제공한다. 태스크의 설정 파라미터느는 메이븐 플러그인과 동일하다.

2.3.4. 쿼리 타입 만들기

DB 스키마를 Querydsl의 쿼리 타입으로 만들려면 다음과 같이 하면 된다.

java.sql.Connection conn = ...;
MetaDataExporter exporter = new MetaDataExporter();
exporter.setPackageName("com.myproject.mydomain");
exporter.setTargetFolder(new File("target/generated-sources/java"));
exporter.export(conn.getMetaData());

위 코드를 실행하면 데이터베이스 스키마로부터 생성한 쿼리 타입 소스 코드(com.myproject.mydomain 패키지에 속함)를 target/generated-sources/java 디렉토리에 만든다.

생성된 타입의 클래스 이름은 변형된 테이블 이름이 되며, 쿼리 티입 프로퍼티 경로의 이름을 변형된 컬럼 이름이 된다.

추가로, 간략한 조인 설정을 위해 PK와 FK를 위한 필드가 추가된다.

2.3.5. 설정

com.querydsl.sql.Configuration 클래스를 이용해서 설정하며, Configuration 클래스는 생성자 인자로 Querydsl SQL Dialect를 취한다. 예를 들어, H2 DB 사용시 다음과 같이 생성한다.

SQLTemplates templates = new H2Templates();
Configuration configuration = new Configuration(templates);

Querydsl은 서로 다른 RDBMS를 위한 SQL 직렬화를 커스터마이징하기 위해 SQL Dialect를 사용한다. 사용가능한 Dialect는 다음과 같다.

  • CUBRIDTemplates (tested with CUBRID 8.4)

  • DB2Templates (tested with DB2 10.1.2)

  • DerbyTemplates (tested with Derby 10.8.2.2)

  • FirebirdTemplates (tested with Firebird 2.5)

  • HSQLDBTemplates (tested with HSQLDB 2.2.4)

  • H2Templates (tested with H2 1.3.164)

  • MySQLTemplates (tested with MySQL 5.5)

  • OracleTemplates (test with Oracle 10 and 11)

  • PostgreSQLTemplates (tested with PostgreSQL 9.1)

  • SQLiteTemplates (tested with xerial JDBC 3.7.2)

  • SQLServerTemplates (tested with SQL Server)

  • SQLServer2005Templates (for SQL Server 2005)

  • SQLServer2008Templates (for SQL Server 2008)

  • SQLServer2012Templates (for SQL Server 2012 and later)

  • TeradataTemplates (tested with Teradata 14)

SQLTemplate 객체의 설정을 변경하려면 다음과 같이 빌더 패턴을 사용할 수 있다.

  H2Templates.builder()
     .printSchema() // to include the schema in the output
     .quote()       // to quote names
     .newLineToSingleSpace() // to replace new lines with single space in the output
     .escape(ch)    // to set the escape char
     .build();      // to get the customized SQLTemplates instance

Configuration 클래스를 이용하면 setUseLiterals(true)를 통한 리터럴의 직접 직렬화 활성, 스키마와 테이블 재정의, 커스텀 타입을 등록할 수 있다. 완전한 내용은 javadoc의 Configuration를 참고한다.

2.3.6. 쿼리

Querydsl SQL을 이용해서 쿼리하는 방법은 다음처럼 간단하다.

QCustomer customer = new QCustomer("c");

SQLQuery query = new SQLQuery(connection, configuration);
List<String> lastNames = query.from(customer)
    .where(customer.firstName.eq("Bob"))
    .list(customer.lastName);

위 코드는 다음의 SQL로 변환되어 실행된다. (테이블 이름은 customer, 컬럼 이름은 first_name, last_name이라고 가정)

SELECT c.last_name
FROM customer c
WHERE c.first_name = 'Bob'

2.3.7. 일반 용법

SQLQuery 클래스의 cascading 메서드는 다음과 같다.

from: 쿼리 소스를 추가한다.

innerJoin, join, leftJoin, rightJoin, fullJoin, on: 조인 부분을 추가한다. 조인 메서드에서 첫 번째 인자는 조인 소스이고, 두 번재 인자는 대상(별칭)이다.

where: 쿼리 필터를 추가한다. 가변인자나 and/or 메서드를 이용해서 필터를 추가한다.

groupBy: 가변인자 형식의 인자를 기준으로 그룹을 추가한다.

having: Predicate 표현식을 이용해서 "group by" 그룹핑의 필터를 추가한다.

orderBy: 정렬 표현식을 이용해서 정렬 순서를 지정한다. 숫자나 문자열에 대해서는 asc()나 desc()를 사용하고, OrderSpecifier에 접근하기 위해 다른 비교 표현식을 사용한다.

limit, offset, restrict: 결과의 페이징을 설정한다. limit은 최대 결과 개수, offset은 결과의 시작 행, restrict는 limit과 offset을 함께 정의한다.

2.3.8. 조인

다음 구문을 이용해서 조인을 한다.

QCustomer customer = QCustomer.customer;
QCompany company = QCompany.company;
query.from(customer)
    .innerJoin(customer.company, company)
    .list(customer.firstName, customer.lastName, company.name);

레프트 조인은 다음과 같다.

query.from(customer)
    .leftJoin(customer.company, company)
    .list(customer.firstName, customer.lastName, company.name);

조인 조건을 쓸 수도 있다.

query.from(customer)
    .leftJoin(company).on(customer.company.eq(company.id))
    .list(customer.firstName, customer.lastName, company.name);

2.3.9. 정렬

다음은 정렬 구문이다.

query.from(customer)
    .orderBy(customer.lastName.asc(), customer.firstName.asc())
    .list(customer.firstName, customer.lastName);

위 코드는 아래 SQL 쿼리와 동등하다.

SELECT c.first_name, c.last_name
FROM customer c
ORDER BY c.last_name ASC, c.first_name ASC

2.3.10. 그룹핑

다음 형식을 이용해서 그룹핑을 한다.

query.from(customer)
    .groupBy(customer.lastName)
    .list(customer.lastName);

다음은 위 코드에 해당하는 SQL 쿼리다.

SELECT c.last_name
FROM customer c
GROUP BY c.last_name

2.3.11. 서브쿼리

서브쿼리를 만들려면 SQLSubQuery를 사용하면 된다. 서브쿼리를 만들기 위해 from 메서드로 쿼리 파라미터를 정의하고, unique나 list를 이용한다. unique는 단일 결과를 위해 사용하고 list는 리스트 결과를 위해 사용한다. 서브쿼리도 쿼리처럼 타입에 안전한 Querydsl 표현식이다.

QCustomer customer = QCustomer.customer;
QCustomer customer2 = new QCustomer("customer2");
query.from(customer).where(
  customer.status.eq(new SQLSubQuery().from(customer2).unique(customer2.status.max()))
  .list(customer.all())

다른 예제

QStatus status = QStatus.status;
query.from(customer).where(
  customer.status.in(new SQLSubQuery().from(status).where(status.level.lt(3)).list(status.id))
  .list(customer.all())

2.3.12. 리터럴 조회

리터럴을 조회하려면, 다음과 같이 constant 인스턴스를 생성해주면 된다.

query.list(Expressions.constant(1),
           Expressions.constant("abc"));

com.querydsl.core.types.dsl.Expressions 클래스는 프로젝션, 오퍼레이션, 템플릿 생성을 위한 유용한 정적 메서드도 제공한다.

2.3.13. 쿼리 확장 지원

엔진에 특화된 구문을 사용하려면 커스텀 쿼리 확장을 사용한다. 커스텀 쿼리 확장은 AbstractSQLQuery를 상속받아 구현할 수 있다. 다음은 MySQLQuery 클래스에서 플래그를 추가하는 예를 보여주고 있다.

public class MySQLQuery extends AbstractSQLQuery<MySQLQuery> {

    public MySQLQuery(Connection conn) {
        this(conn, new MySQLTemplates(), new DefaultQueryMetadata());
    }

    public MySQLQuery(Connection conn, SQLTemplates templates) {
        this(conn, templates, new DefaultQueryMetadata());
    }

    protected MySQLQuery(Connection conn, SQLTemplates templates, QueryMetadata metadata) {
        super(conn, new Configuration(templates), metadata);
    }

    public MySQLQuery bigResult() {
        return addFlag(Position.AFTER_SELECT, "SQL_BIG_RESULT ");
    }

    public MySQLQuery bufferResult() {
        return addFlag(Position.AFTER_SELECT, "SQL_BUFFER_RESULT ");
    }


    // ...
}

플래그는 직렬화 과정에서 특정 위치에 삽입될 수 있는 커스텀 SQL 부분 코드다. com.querydsl.core.QueryFlag.Position 열거 타입에 지원되는 위치가 정의되어 있다.

2.3.14. 윈도우 함수

Querydsl은 SQLExpressions 클래스를 통해서 윈도우 함수를 지원한다.

다음은 사용 예다.

query.from(employee)
    .list(SQLExpressions.rowNumber()
        .over()
        .partitionBy(employee.name)
        .orderBy(employee.id));

2.3.15. 다른 SQL 표현식

SQLExpressions 클래스의 정적 메서드를 이용해서 다른 SQL 표현식을 사용할 수 있다.

2.3.16. DML 명령 사용하기

Querydsl SQL 모듈의 모든 DMLClause 구현체는 Connection, 쿼리에 사용될 SQLTemplate, DMLClause와 엮일 메인 엔티티의 세 개 파라미터를 필요로 한다.

2.3.16.1. 삽입

컬럼 지정

QSurvey survey = QSurvey.survey;

new SQLInsertClause(conn, configuration, survey)
    .columns(survey.id, survey.name)
    .values(3, "Hello").execute();

컬럼 없이

new SQLInsertClause(conn, configuration, survey)
    .values(4, "Hello").execute();

서브쿼리 이용

new SQLInsertClause(conn, configuration, survey)
    .columns(survey.id, survey.name)
    .select(new SQLSubQuery().from(survey2).list(survey2.id.add(1), survey2.name))
    .execute();

서브쿼리 이용, 컬럼 없이

new SQLInsertClause(conn, configuration, survey)
    .select(new SQLSubQuery().from(survey2).list(survey2.id.add(10), survey2.name))
    .execute();

columns/values를 사용하는 대신, set 메서드를 이용

QSurvey survey = QSurvey.survey;

new SQLInsertClause(conn, configuration, survey)
    .set(survey.id, 3)
    .set(survey.name, "Hello").execute();

위 코드는 첫 번째 예제와 동일하다. set 메서드를 사용하면 내부적으로 columns/values가 사용된다.

아래 형식의 코드에서는 컬럼과 쿼리 결과 집합을 매핑하는 것에 주의하자.

columns(...).select(...)

변경된 행 개수 대신 생성된 키를 구하고 싶다면 executeWithKey/s 메서드를 사용한다.

set(...)

위 코드는 한 개 컬럼을 매핑한다. 서브 쿼리 결과가 없으면 null을 사용한다.

빈의 데이터에 기반해서 clause 인스턴스를 생성하려면 다음의 코드를 사용한다.

new SQLInsertClause(conn, configuration, survey)
    .populate(surveyBean).execute();

위 코드는 빈의 데이터 중 null은 제외한다. null도 포함시키고 싶다면 아래 코드를 사용한다.

new SQLInsertClause(conn, configuration, survey)
    .populate(surveyBean, DefaultMapper.WITH_NULL_BINDINGS).execute();

2.3.16.2. 수정

where 절 포함

QSurvey survey = QSurvey.survey;

new SQLUpdateClause(conn, configuration, survey)
    .where(survey.name.eq("XXX"))
    .set(survey.name, "S")
    .execute();

where 절 없이

new SQLUpdateClause(conn, configuration, survey)
    .set(survey.name, "S")
    .execute();

빈을 이용

new SQLUpdateClause(conn, configuration, survey)
    .populate(surveyBean)
    .execute();

2.3.16.3. 삭제

where 절 포함

QSurvey survey = QSurvey.survey;

new SQLDelecteClause(conn, configuration, survey)
    .where(survey.name.eq("XXX"))
    .execute();

where 없이

new SQLDelecteClause(conn, configuration, survey)
    .execute()

2.3.17. DMLClause의 배치 지원

Querydsl SQL은 DML API를 통해서 JDBC 배치 업데이터를 지원한다. 같은 구조를 갖는 DML을 연속해서 실행할 경우, addBatch() 메서드를 이용해서 한 DMLClause로 묶을 수 있다. UPDATE, DELETE, INSERT에 대해 어떻게 동작하는지 예제를 살펴보자.

수정:

QSurvey survey = QSurvey.survey;

insert(survey).values(2, "A").execute();
insert(survey).values(3, "B").execute();

SQLUpdateClause update = update(survey);
update.set(survey.name, "AA").where(survey.name.eq("A")).addBatch();
update.set(survey.name, "BB").where(survey.name.eq("B")).addBatch();

삭제:

insert(survey).values(2, "A").execute();
insert(survey).values(3, "B").execute();

SQLDeleteClause delete = delete(survey);
delete.where(survey.name.eq("A")).addBatch();
delete.where(survey.name.eq("B")).addBatch();
assertEquals(2, delete.execute());

삽입:

SQLInsertClause insert = insert(survey);
insert.set(survey.id, 5).set(survey.name, "5").addBatch();
insert.set(survey.id, 6).set(survey.name, "6").addBatch();
assertEquals(2, insert.execute());

2.3.18. 빈 클래스 생성

MetaDataExporter를 이용해서 테이블에 대한 자바빈 DTO 타입을 생성한다.

java.sql.Connection conn = ...;
MetaDataExporter exporter = new MetaDataExporter();
exporter.setPackageName("com.myproject.mydomain");
exporter.setTargetFolder(new File("src/main/java"));
exporter.setBeanSerializer(new BeanSerializer());
exporter.export(conn.getMetaData());

DMLClause의 populate 메서드의 인자로 빈 타입을 사용할 수 있으며, 쿼리에서 빈 타입을 직접 선택할 수 있다. 다음은 JUnit으로 작성한 간단한 예이다.

QEmployee e = new QEmployee("e");

// Insert
Employee employee = new Employee();
employee.setFirstname("John");
Integer id = insert(e).populate(employee).executeWithKey(e.id);
employee.setId(id);

// Update
employee.setLastname("Smith");
assertEquals(1l, update(e).populate(employee).where(e.id.eq(employee.getId())).execute());

// Query
Employee smith = query().from(e).where(e.lastname.eq("Smith")).uniqueResult(e);
assertEquals("John", smith.getFirstname());

// Delete
assertEquals(1l, delete(e).where(e.id.eq(employee.getId())).execute());

앞서 예제에서 사용한 팩토리 메서드는 다음과 같다.

protected SQLUpdateClause update(RelationalPath<?> e) {
    return new SQLUpdateClause(Connections.getConnection(), templates, e);
}

protected SQLInsertClause insert(RelationalPath<?> e) {
    return new SQLInsertClause(Connections.getConnection(), templates, e);
}

protected SQLDeleteClause delete(RelationalPath<?> e) {
    return new SQLDeleteClause(Connections.getConnection(), templates, e);
}

protected SQLMergeClause merge(RelationalPath<?> e) {
    return new SQLMergeClause(Connections.getConnection(), templates, e);
}

protected SQLQuery query() {
    return new SQLQuery(Connections.getConnection(), templates);
}

2.3.19. SQL 쿼리와 바인딩 추출하기

getSQL 메서드를 통해서 SQL 쿼리와 바인딩 값을 구할 수 있다.

SQLBindings bindings = query.getSQL(customer.id, customer.firstname, customer.lastname);
System.out.println(bindings.getSQL());

SQL 문자열에 포함된 모든 리터럴이 필요하다면, setUseLiterals(true)를 이용해서 쿼리의 리터럴 직렬화를 활성화하면 된다.

2.3.20. 커스텀 타입

Querydsl SQL은 ResultSet/Statement에서 커스텀 타입 매핑을 지원한다. com.querydsl.sql.Configuration 객체를 이용해서 커스텀 카입 매핑을 등록한다. Configuration 객체는 실제 쿼리의 생성자 인자로 제공된다.

Configuration configuration = new Configuration(new H2Templates());
// overrides the mapping for Types.DATE
configuration.register(new UtilDateType());

특정 테이블 컬럼을 위한 커스텀 타입 매핑 등록

Configuration configuration = new Configuration(new H2Templates());
// declares a maping for the gender column in the person table
configuration.register("person", "gender",  new EnumByNameType<Gender>(Gender.class));

숫자에 대한 커스텀 타입 매핑을 등록하려면 registerNumeric 메서드를 사용한다.

configuration.registerNumeric(5,2,Float.class);

이는 Float 타입을 NUMERIC(5,2) 타입으로 매핑한다.

2.3.21. Query와 Clause 리스닝

SQLListener는 쿼리와 DMLClause를 리스닝 할 때 사용되는 리스너 인터페이스이다. Configuration이나 Query, Clause의 addListener 메서드를 통해서 SQLListener 객체를 등록할 수 있다.

리스너의 적용 예로는 데이터 동기화, 로깅, 캐싱, 검증이 있다.

2.4. 루신 쿼리

이 절에서는 루신 모듈의 쿼리 기능을 설명한다.

2.4.1. Maven integration

Querydsl 루신을 사용하려면 루신 3은 querydsl-lucene3 모듈을 그리고 루신 4는 querydsl-lucene4 모듈을 사용한다.

루신 3:

<dependency>
  <groupId>com.querydsl</groupId>
  <artifactId>querydsl-lucene3</artifactId>
  <version>${querydsl.version}</version>
</dependency>

<dependency>
  <groupId>org.slf4j</groupId>
  <artifactId>slf4j-log4j12</artifactId>
  <version>1.6.1</version>
</dependency>

루신 4:

<dependency>
  <groupId>com.querydsl</groupId>
  <artifactId>querydsl-lucene4</artifactId>
  <version>${querydsl.version}</version>
</dependency>

<dependency>
  <groupId>org.slf4j</groupId>
  <artifactId>slf4j-log4j12</artifactId>
  <version>1.6.1</version>
</dependency>

2.4.2. 쿼리 타입 생성

다음과 같은 방식으로 year와 title 필드를 가진 쿼리 타입을 직접 작성할 수 있다.

public class QDocument extends EntityPathBase<Document> {
    private static final long serialVersionUID = -4872833626508344081L;

    public QDocument(String var) {
        super(Document.class, PathMetadataFactory.forVariable(var));
    }

    public final StringPath year = createString("year");

    public final StringPath title = createString("title");
}

QDocument는 year와 title 필드를 가진 루신 Document를 표현한다.

루신의 경우 스키마 데이터를 사용할 수 없기 때문에 코드 생성 기능을 사용할 수 없다.

2.4.3. 쿼리

Querydsl 루신으로 쿼리하는 것은 간단하다.

QDocument doc = new QDocument("doc");

IndexSearcher searcher = new IndexSearcher(index);
LuceneQuery query = new LuceneQuery(true, searcher);
List<Document> documents = query
    .where(doc.year.between("1800", "2000").and(doc.title.startsWith("Huckle"))
    .list();

위 코드는 다음의 루신 쿼리로 바뀐다.

+year:[1800 TO 2000] +title:huckle*

2.4.4. 일반 용법

LuceneQuery 클래스의 cascading 메서드는 다음과 같다.

where: 쿼리 필터를 추가한다. 가변인자나 and/or 메서드를 이용해서 필터를 추가한다. PStrings에 수행되는 오퍼레이션을 지원한다. (matches, indexOf, charAt은 제외). 현재 in은 지원되지 않으며, 향후 지원할 예정이다.

orderBy: 정렬 표현식을 이용해서 정렬 순서를 지정한다. 숫자나 문자열에 대해서는 asc()나 desc()를 사용하고, OrderSpecifier에 접근하기 위해 다른 비교 표현식을 사용한다.

limit, offset, restrict: 결과의 페이징을 설정한다. limit은 최대 결과 개수, offset은 결과의 시작 행, restrict는 limit과 offset을 함께 정의한다.

2.4.5. 정렬

정렬 구문은 다음과 같다.

query
    .where(doc.title.like("*"))
    .orderBy(doc.title.asc(), doc.year.desc())
    .list();

위 코드는 다음 루신 쿼리와 동일하다.

title:*

title과 year의 오름차순으로 결과를 정렬한다.

sort 메서드와 Sort 인스턴스를 사용해서 정렬을 지정할 수 있다.

Sort sort = ...;
query
    .where(doc.title.like("*"))
    .sort(sort)
    .list();

2.4.6. 결과 개수 제한

결과 개수 제한은 다음과 같이 한다.

query
    .where(doc.title.like("*"))
    .limit(10)
    .list();

2.4.7. 오프셋

오프셋은 다음과 같이 지정한다.

query
    .where(doc.title.like("*"))
    .offset(3)
    .list();

2.4.8. 퍼지(fuzzy) 검색

com.querydsl.lucene3.LuceneExpressions 클래스에서 정의된 fuzzyLike 메서드를 이용해서 퍼지 검색을 할 수 있다.

query
    .where(LuceneExpressions.fuzzyLike(doc.title, "Hello"))
    .list();

2.4.9. 루신 필터를 쿼리에 적용하기

단일 루신 필터를 쿼리에 적용할 수 있다.

query
    .where(doc.title.like("*"))
    .filter(filter)
    .list();

distinct 필터링을 위한 distinct(Path) 메서드를 제공한다.

query
    .where(doc.title.like("*"))
    .distinct(doc.title)
    .list();

2.5. Hibernate Search 쿼리

Hibernate Search 모듈의 쿼리 기능을 설명한다.

2.5.1. Querydsl 쿼리 타입 생성

쿼리 타입을 생성하는 방법은 본 문서의 JPA 쿼리 부분을 참고한다.

2.5.2. 쿼리

Querydsl Hibernate Search를 이용한 쿼리는 다음과 같이 간단하다.

QUser user = QUser.user;
SearchQuery<User> query = new SearchQuery<User>(session, user);
List<User> list = query
    .where(user.firstName.eq("Bob"))
    .list();

2.5.3. 일반 용법

일반 용법은 Querying Lucene의 일반 용법을 참고한다.

쿼리 직렬화 과정에서 Querydsl Lucene module과의 유일한 차이점은 경로를 다르게 처리한다는 것이다. org.hibernate.search.annotations.Field 애노테이션이 적용된 프로퍼티인 경우, 필드 이름에 대한 대체 방법으로 name 속성의 값을 사용한다.

2.6. Mongodb 쿼리

Mongodb 모듈의 조회 기능을 설명한다.

2.6.1. 메이븐 통합

메이븐 프로젝트에 다음 의존을 추가한다.

<dependency>
  <groupId>com.querydsl</groupId>
  <artifactId>querydsl-apt</artifactId>
  <version>${querydsl.version}</version>
  <scope>provided</scope>
</dependency>

<dependency>
  <groupId>com.querydsl</groupId>
  <artifactId>querydsl-mongodb</artifactId>
  <version>${querydsl.version}</version>
</dependency>

<dependency>
  <groupId>org.slf4j</groupId>
  <artifactId>slf4j-log4j12</artifactId>
  <version>1.6.1</version>
</dependency>

Querydsl을 이용해서 쿼리 타입을 생성하기 위해 메이븐 APT 플러그인을 설정한다.

<project>
  <build>
    <plugins>
      ...
      <plugin>
        <groupId>com.mysema.maven</groupId>
        <artifactId>apt-maven-plugin</artifactId>
        <version>1.1.3</version>
        <executions>
          <execution>
            <goals>
              <goal>process</goal>
            </goals>
            <configuration>
              <outputDirectory>target/generated-sources/java</outputDirectory>
              <processor>com.querydsl.apt.morphia.MorphiaAnnotationProcessor</processor>
            </configuration>
          </execution>
        </executions>
      </plugin>
    ...
    </plugins>
  </build>
</project>

MorphiaAnnotationProcessor는 com.google.code.morphia.annotations.Entity 애노테이션이 적용된 도메인 타입을 위한 Querydsl 쿼리 타입을 생성한다.

mvn clean install 을 실행하면 target/generated-sources/java 폴더에 쿼라 타입 코드가 생성된다.

이클립스를 사용중이면, mvn eclipse:eclipse 을 실행해서 target/generated-sources/java을 이클립스 프로젝트의 소스 폴더에 포함시킨다.

이제 쿼리 도메인 모델을 이용해서 Mongodb를 조회할 수 있다.

2.6.2. 쿼리

Querydsl Mongodb를 이용하면 다음과 같이 간단하게 쿼리할 수 있다.

Morphia morphia;
Datastore datastore;
// ...
QUser user = new QUser("user");
MorphiaQuery<User> query = new MorphiaQuery<User>(morphia, datastore, user);
List<User> list = query
    .where(user.firstName.eq("Bob"))
    .list();

2.6.3. 일반 용법

MongodbQuery 클래스의 cascading 메서드는 다음과 같다.

where: 쿼리 필터를 추가한다. 가변인자나 and/or 메서드를 이용해서 필터를 추가한다. PStrings에 수행되는 오퍼레이션을 지원한다. (matches, indexOf, charAt은 제외). 현재 in은 지원되지 않으며, 향후 지원할 예정이다.

orderBy: 정렬 표현식을 이용해서 정렬 순서를 지정한다. 숫자나 문자열에 대해서는 asc()나 desc()를 사용하고, OrderSpecifier에 접근하기 위해 다른 비교 표현식을 사용한다.

limit, offset, restrict: 결과의 페이징을 설정한다. limit은 최대 결과 개수, offset은 결과의 시작 행, restrict는 limit과 offset을 함께 정의한다.

2.6.4. 정렬

정렬 구문은 다음과 같다.

query
    .where(doc.title.like("*"))
    .orderBy(doc.title.asc(), doc.year.desc())
    .list();

title과 year의 오름차순으로 결과를 정렬한다.

2.6.5. 결과 개수 제한

다음과 같이 결과 개수를 제한한다.

query
    .where(doc.title.like("*"))
    .limit(10)
    .list();

2.6.6. 오프셋

다음과 같이 오프셋을 지정한다.

query
    .where(doc.title.like("*"))
    .offset(3)
    .list();

2.6.7. 공간(Geospatial) 쿼리

near(Double[]) 메서드를 이용해서 공간 검색을 할 수 있다.

query
    .where(geoEntity.location.near(50.0, 50.0))
    .list();

2.6.8. 관련 필드만 선택하기

관련 필드만 선택하고 싶다면, 선택 대상 목록을 갖는 list, iterate, uniqueResult, singleResult 메서드를 사용하면 된다.

query
    .where(doc.title.like("*"))
    .list(doc.title, doc.path);

이 쿼리는 문서의 title과 path 필드만 조회한다.

2.7. 콜렉션 쿼리

생성된 쿼리 타입을 이용하거나 또는 쿼리 타입 없이 querydsl-collections 모듈을 사용할 수 있다. 첫 번째 절에서는 생성된 쿼리 타입 없이 사용하는 방법을 설명한다.

2.7.1. 생성된 쿼리 타입 없이 사용하기

생성된 쿼리 타입 없이 querydsl-collections 모듈을 사용하려면, Querydsl 별칭 기능을 사용해야 한다. 다음은 몇 가지 예다.

먼저 다음의 정적 임포트를 추가한다.

// needed for access of the Querydsl Collections API
import static com.querydsl.collections.CollQueryFactory.*;
// needed, if you use the $-invocations
import static com.querydsl.core.alias.Alias.*;

이제 Cat 클래스에 대한 별칭 인스턴스를 만들어보자. 기본 생성자를 가진 non-final 클래스에 대해서만 Alias 인스턴스를 만들수 있다.

$ 메서드로 감싸는 방법으로 Cat 타입의 별칭 인스턴스 및 그것의 getter 메서드 호출을 경로로 바꾼다. 예를 들어, c.getKittens()에 대한 호출은 $ 메서드를 통해 c.kittends 경로로 바뀐다.

Cat c = alias(Cat.class, "cat");
for (String name : from($(c),cats)
  .where($(c.getKittens()).size().gt(0))
  .list($(c.getName()))) {
    System.out.println(name);
}

다음은 앞 예제를 다르게 작성해본 것이다. 아래 코드는 List의 size() 메서드를 $ 메서드로 감쌌다.

Cat c = alias(Cat.class, "cat");
for (String name : from($(c),cats)
  .where($(c.getKittens().size()).gt(0))
  .list($(c.getName()))) {
    System.out.println(name);
}

별칭의 모든 비-기본타입과 non-final 타입 프로퍼티는 별칭 그 자체이다. 따라서, $ 메서드 범위안에서 기본 타입이나 non-final 타입 (예, java.lang.String)을 만날 때 까지 연속된 메서드 호출이 가능하다.

예를 들어,

$(c.getMate().getName())

이 코드는 c.mate.name으로 바뀐다. 하지만, 아래 코드는 올바르게 바뀌지 않는다.

$(c.getMate().getName().toLowerCase())

그 이유는 toLowerCase() 호출은 추적되지 않기 때문이다.

별칭 타입에 대해 getter, size(), contains(Object), get(int) 만 호출할 수 있다. 나머지 다른 메서드에 대한 호출은 익셉션을 발생시킨다.

2.7.2. 생성된 쿼리 타입을 갖고 사용하기

앞서 예제를 생성된 쿼리 타입을 이용하면 다음과 같이 표현할 수 있다.

QCat cat = new QCat("cat");
for (String name : from(cat,cats)
  .where(cat.kittens.size().gt(0))
  .list(cat.name)) {
    System.out.println(name);
}

생성된 쿼리 타입을 사용하면, 별칭 인스턴스 대신 표현식을 생성할 수 있고 $ 메서드를 사용할 필요 없이 프로퍼티 경로를 바로 사용할 수 있다.

2.7.3. 메이븐 통합

다음 의존을 메이븐 프로젝트에 추가한다.

<dependency>
  <groupId>com.querydsl</groupId>
  <artifactId>querydsl-apt</artifactId>
  <version>${querydsl.version}</version>
  <scope>provided</scope>
</dependency>

<dependency>
  <groupId>com.querydsl</groupId>
  <artifactId>querydsl-collections</artifactId>
  <version>${querydsl.version}</version>
</dependency>

<dependency>
  <groupId>org.slf4j</groupId>
  <artifactId>slf4j-log4j12</artifactId>
  <version>1.6.1</version>
</dependency>

JPA나 JDO를 사용하지 않는다면, 도메인 타입에 com.querydsl.core.annotations.QueryEntity 애노테이션을 적용하고 메이븐 설정(pom.xml)에 다음 플러그인 설정을 추가함으로써 표현식 타입을 생성할 수 있다.

<project>
  <build>
  <plugins>
    ...
    <plugin>
      <groupId>com.mysema.maven</groupId>
      <artifactId>apt-maven-plugin</artifactId>
      <version>1.1.3</version>
      <executions>
        <execution>
          <goals>
            <goal>process</goal>
          </goals>
          <configuration>
            <outputDirectory>target/generated-sources/java</outputDirectory>
            <processor>com.querydsl.apt.QuerydslAnnotationProcessor</processor>
          </configuration>
        </execution>
      </executions>
    </plugin>
    ...
  </plugins>
  </build>
</project>

2.7.4. Ant 통합

클래스패스에 full-deps에 포함된 jar 파일들을 위치시키고, 다음 태스크를 이용해서 Querydsl 코드를 생성한다.

    <!-- APT based code generation -->
    <javac srcdir="${src}" classpathref="cp">
      <compilerarg value="-proc:only"/>
      <compilerarg value="-processor"/>
      <compilerarg value="com.querydsl.apt.QuerydslAnnotationProcessor"/>
      <compilerarg value="-s"/>
      <compilerarg value="${generated}"/>
    </javac>

    <!-- compilation -->
    <javac classpathref="cp" destdir="${build}">
      <src path="${src}"/>
      <src path="${generated}"/>
    </javac>

src를 메인 소스 폴더로 변경하고, generated를 생성된 소스를 위한 폴더로 변경하고, build를 클래스 생성 폴더로 변경한다.

2.7.5. Hamcrest matchers

Querydsl Collections 모듈은 Hamcrest matchers를 제공한다. 다음의 import를 통해 사용하면 된다.

import static org.hamcrest.core.IsEqual.equalTo;
import static com.querydsl.collections.PathMatcher.hasValue;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThat;

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

Car car = new Car();
car.setHorsePower(123);

assertThat(car, hasValue($.horsePower));
assertThat(car, hasValue($.horsePower, equalTo(123)));

Jeroen van Schagen가 Hamcrest matchers를 기여했다.

2.8. Scala에서 쿼리하기

querydsl-scala 모듈을 통해 Scala에서 Querydsl을 사용할 수 있다. 이 모듈을 사용하려면 메이븐 빌드에 다음의 코드를 추가한다.

<dependency>
  <groupId>com.querydsl</groupId>
  <artifactId>querydsl-scala</artifactId>
  <version>${querydsl.version}</version>
</dependency>

2.8.1. Scala를 위한 DSL 표현

Scala 용 Querydsl은 표현식 생성을 위한 별도 DSL을 제공한다. Scala DSL은 가독성과 간결함을 향상시키기 위해 연산자 오버로딩, 함수 포인터, 임의 임포트 등 언어의 특징을 활용한다.

주요 대체 DSL은 다음과 같다.

//Standard              Alternative

expr isNotNull          expr is not(null)
expr isNull             expr is null
expr eq "Ben"           expr === "Ben"
expr ne "Ben"           expr !== "Ben"
expr append "X"         expr + "X"
expr isEmpty            expr is empty
expr isNotEmpty         expr not empty

// boolean
left and right          left && right
left or right           left || right
expr not                !expr

// comparison
expr lt 5               expr < 5
expr loe 5              expr <= 5
expr gt 5               expr > 5
expr goe 5              expr >= 5
expr notBetween(2,6)    expr not between (2,6)
expr negate             -expr

// numeric
expr add 3              expr + 3
expr subtract 3         expr - 3
expr divide 3           expr / 3
expr multiply 3         expr * 3
expr mod 5              expr % 5

// collection
list.get(0)             list(0)
map.get("X")            map("X")

2.8.2. 향상된 프로젝션

Querydsl Scala 모듈은 Querydsl의 쿼리 프로젝션을 Scala에 더욱 알맞게 만들기 위해 몇몇 임의 변환을 제공한다.

Querydsl 쿼리에서 Scala 프로젝션을 활성화하려면 RichProjectableRichSimpleProjectable 래퍼를 사용해야 한다. com.querydsl.scala.Helpers를 임포트 함으로써 필요한 임의 변환이 가능해진다.

예를 들어, 표준 API를 이용한 다음 쿼리는 Object[] 타입의 java.util.List를 리턴한다.

query.from(person).list(person.firstName, person.lastName, person.age)

임의 변환을 추가함으로써, list대신에 select를 이용해서 Scala List 타입 결과를 사용할 수 있다. 또한, uniqueResult나 singleResult 대신에 unique와 single를 이용해서 Option 타입으로 결과를 사용할 수 있다.

임의 변환을 사용하면 앞서 쿼리를 다음과 같이 작성할 수 있다.

import com.querydsl.scala.Helpers._

query.from(person).select(person.firstName, person.lastName, person.age)

이 경우 결과 타입은 List[(String,String,Integer)] 즉, Tuple3[String,String,Integer]의 List가 된다.

2.8.3. SQL을 이용한 쿼리

자바를 위한 Querydsl SQL과 마찬가지로, 쿼리를 만들려면 쿼리 타입을 생성해야 한다. 다음은 쿼리 타입 생성을 위한 코드 예제다.

빈 타입 없이 생성하기:

val directory = new java.io.File("target/jdbcgen1")
val namingStrategy = new DefaultNamingStrategy()
val exporter = new MetaDataExporter()
exporter.setNamePrefix("Q")
exporter.setPackageName("com.querydsl")
exporter.setSchemaPattern("PUBLIC")
exporter.setTargetFolder(directory)
exporter.setSerializerClass(classOf[ScalaMetaDataSerializer])
exporter.setCreateScalaSources(true)
exporter.setTypeMappings(ScalaTypeMappings.create)
exporter.export(connection.getMetaData)

빈 타입을 이용해서 생성하기:

val directory = new java.io.File("target/jdbcgen2")
val namingStrategy = new DefaultNamingStrategy()
val exporter = new MetaDataExporter()
exporter.setNamePrefix("Q")
exporter.setPackageName("com.querydsl")
exporter.setSchemaPattern("PUBLIC")
exporter.setTargetFolder(directory)
exporter.setSerializerClass(classOf[ScalaMetaDataSerializer])
exporter.setBeanSerializerClass(classOf[ScalaBeanSerializer])
exporter.setCreateScalaSources(true)
exporter.setTypeMappings(ScalaTypeMappings.create)
exporter.export(connection.getMetaData)

2.8.3.1. 컴팩트 쿼리

Querydsl Scala은 Querydls SQL을 위한 컴팩트 쿼리를 제공한다. 이 구문은 Rogue 프레임워크의 도메인 지향 쿼리 구문에서 영감을 얻었다.

RelationalPath 인스턴스를 쿼리로 임의 변환해서 도메인 지향 쿼리를 구현한다. 서비스나 DAO 클래스가 com.querydsl.scala.sql.SQLHelpers 트레잇을 상속하면 이 기능을 사용할 수 있다.

컴팩트 쿼리를 사용하면, 쿼리의 시작 지점으로 메타 모델 클래스를 사용할 수 있다.

다음의 일반 쿼리를 사용하는 대신에

query().from(employee).select(employee.firstName, employee.lastName)

다음과 같이 Employee 또는 QEmployee의 companion 객체를 사용할 수 있다.

Employee.select(_.firstName, _.lastName)

표현식에 orderBy, where, select, single, unique를 사용하는 대신, 쿼리의 루트 표현식을 파라미터로 받고 다른 표현식을 리턴하는 함수를 사용할 수 있다. 다음은 앞 예제를 확장한 형식이다.

Employee.select({ e => e.firstName }, { e => e.lastName })

자세한 내용은 com.querydsl.scala.sql.RichSimpleQuery의 시그너처를 참고하기 바란다.

2.8.3.2. 코드 생성

querydsl-maven-plugin을 이용해서 SQL 메타타입과 프로젝션을 위한 Scala 소스를 생성한다. 다음은 설정 예다.

<project>
  <build>
    <plugins>
      ...
      <plugin>
        <groupId>com.querydsl</groupId>
        <artifactId>querydsl-maven-plugin</artifactId>
        <version>${querydsl.version}</version>
        <configuration>
          <jdbcDriver>com.mysql.jdbc.Driver</jdbcDriver>
          <jdbcUrl>jdbc:mysql://localhost:3306/test</jdbcUrl>
          <jdbcUser>matko</jdbcUser>
          <jdbcPassword>matko</jdbcPassword>
          <packageName>com.example.schema</packageName>
          <targetFolder>${project.basedir}/src/main/scala</targetFolder>
          <exportBeans>true</exportBeans>
          <createScalaSources>true</createScalaSources>
        </configuration>
        <dependencies>
          <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.16</version>
          </dependency>
          <dependency>
            <groupId>com.querydsl</groupId>
            <artifactId>querydsl-scala</artifactId>
            <version>${querydsl.version}</version>
          </dependency>
          <dependency>
            <groupId>org.scala-lang</groupId>
            <artifactId>scala-library</artifactId>
            <version>${scala.version}</version>
          </dependency>
        </dependencies>
      </plugin>
      ...
    </plugins>
  </build>
</project>

querydsl:export 메이븐 골을 실행한다.

2.8.4. 다른 백엔드에 대한 쿼리

다른 백엔드에 대해 쿼리를 하려면, Expression 모델을 수동으로 만들거나 별칭 함수를 사용해야 한다.

다음은 JPA를 이용할 때의 예제다.

@Entity
class User {
  @BeanProperty
  @Id
  var id: Integer = _;
  @BeanProperty
  var userName: String = _;
  @BeanProperty
  @ManyToOne
  var department: Department = _;
}

@Entity
class Department {
  @BeanProperty
  @Id
  var id: Integer = _;
  @BeanProperty
  var name: String = _;
}

다음은 몇 가지 쿼리 예제다.

List

val person = Person as "person"

query.from(person).where(person.firstName like "Rob%").list(person)

Unique result

query.from(person).where(person.firstName like "Rob%").unique(person)

Long where

query.from(person)
  .where(person.firstName like "Rob%", person.lastName like "An%")
  .list(person)

Order

query.from(person).orderBy(person.firstName asc).list(person)

Not null

query.from(person)
  .where(person.firstName isEmpty, person.lastName isNotNull)
  .list(person)

쿼리 생성을 위한 팩토리 메서드

def query() = new JPAQuery(entityManager)

위 쿼리를 실행하려면 다음과 같이 변수를 생성해야 한다.

val person = Person as "person"

하이버네이트에서 XML 기반 설정을 사용할 경우, 아직 Scala 모듈을 사용할 수 없다. HibernateDomainExporter는 현재 자바 소스 파일 생성만 지원한다.

3장. 일반 사용법

본 장은 레퍼런스 튜토리얼에서 다루지 않은 내용을 설명한다.

3.1. 쿼리 생성

Querydsl에서 Query를 생성하려면 표현식 인자를 이용해서 query 메서드를 호출한다. query 메서드는 모듈에 따라 다르고 이미 튜토리얼에서 설명했으므로, 본 절에서는 표현식에 초점을 맞출 것이다.

표현식을 생성할 때에는 도메인 모듈에서 생성한 표현식 타입의 필드와 메서드를 이용한다. 코드 생성할 수 없는 경우, 표현식을 생성하기 위한 범용 방법을 사용하면 된다.

3.1.1. 복합 조건(complex predicates)

복합 불리언 표현식을 작성하려면 com.querydsl.core.BooleanBuilder 클래스를 사용한다. 이 클래스는 Predicate을 구현하고 있고 메서드 체인 형식으로 사용할 수 있다.

public List<Customer> getCustomer(String... names) {
    QCustomer customer = QCustomer.customer;
    JPAQuery query = new JPAQuery(entityManager).from(customer);
    BooleanBuilder builder = new BooleanBuilder();
    for (String name : names) {
        builder.or(customer.name.eq(name));
    }
    query.where(builder); // customer.name eq name1 OR customer.name eq name2 OR ...
    return query.list(customer);
}

BooleanBuilder는 상태변경이 되며(mutable) 초기에는 null을, 각 and 또는 or 호출 뒤에는 오퍼레이션의 결과를 표현한다.

3.1.2. 동적 표현식

com.querydsl.core.types.dsl.Expressions 클래스는 동적인 표현식 생성을 위한 정적 팩토리 클래스다. 팩토리 메서드는 리턴 타입에 따라 이름을 지었으므로 쉽게 유추할 수 있다.

일반적으로 동적 경로, 커스텀 구문이나 커스텀 오퍼레이션과 같이 Fluent DSL 형식을 사용할 수 없는 경우에 한해 Expressions 클래스를 사용한다.

다음 표현식을 보자.

QPerson person = QPerson.person;
person.firstName.startsWith("P");

만약 Q타입 생성이 가능하지 않으면 다음과 같이 위와 동일한 표현식을 만들 수 있다.

Path<Person> person = Expressions.path(Person.class, "person");
Path<String> personFirstName = Expressions.path(String.class, person, "firstName");
Constant<String> constant = Expressions.constant("P");
Expressions.predicate(Ops.STARTS_WITH, personFirstName, constant);

Path 인스턴스는 변수와 프로퍼티를 의미하고, Constant는 상수를, Operation은 오퍼레이션을 표현하며, TemplateExpression 인스턴스를 사용해서 String 템플릿으로 표현식을 표현할 수 있다.

3.1.3. 동적 경로

Expressions 기반의 표현식 생성 외에 Querydsl은 동적 경로 생성을 위한 더 표현력이 좋은 API를 제공한다.

동적 경로 생성을 위해 com.querydsl.core.types.dsl.PathBuilder 클래스를 사용할 수 있다. 이 클래스는 EntityPathBase 클래스를 확장하고 있고 경로 생성을 위해 클래스 생성과 별칭 사용 대신에 사용가능하다.

Expressions API와 비교하면 PathBuilder 커스텀 구문이나 unknown 오퍼레이션을 직접 지원하진 않지만, 구문이 일반 DSL에 더 가깝다.

Strign 프로퍼티:

PathBuilder<User> entityPath = new
PathBuilder<User>(User.class, "entity");
// fully generic access
entityPath.get("userName");
// .. or with supplied type
entityPath.get("userName", String.class);
// .. and correct signature
entityPath.getString("userName").lower();

컴포넌트 타입을 가진 List 프로퍼티:

entityPath.getList("list", String.class).get(0);

복합 표현식 타입 사용:

entityPath.getList("list", String.class, StringPath.class).get(0).lower();

키와 값 타입을 갖는 맵 프로퍼티:

entityPath.getMap("map", String.class, String.class).get("key");

복합 표현식 타입 사용:

entityPath.getMap("map", String.class, String.class, StringPath.class).get("key").lower();

3.1.4. Case 표현식

case-when-then-else 표현식을 만들 땐, 다음과 같이 CaseBuilder 클래스를 사용한다.

QCustomer customer = QCustomer.customer;
Expression<String> cases = new CaseBuilder()
    .when(customer.annualSpending.gt(10000)).then("Premier")
    .when(customer.annualSpending.gt(5000)).then("Gold")
    .when(customer.annualSpending.gt(2000)).then("Silver")
    .otherwise("Bronze");
// The cases expression can now be used in a projection or condition

equals-operations을 가진 case 표현식은 다음과 같이 단순한 형태를 사용하면 된다.

QCustomer customer = QCustomer.customer;
Expression<String> cases = customer.annualSpending
    .when(10000).then("Premier")
    .when(5000).then("Gold")
    .when(2000).then("Silver")
    .otherwise("Bronze");
// The cases expression can now be used in a projection or condition

JDOQL에서는 아직 Case 표현식을 지원하지 않는다.

3.1.5. Casting 표현식

표현식 타입에서 지네릭 시그너처를 피하기 위해, 타입 계층을 단순화시킨다. 그 결과로 모든 생성된 쿼리 타입은 com.querydsl.core.types.dsl.EntityPathBasecom.querydsl.core.types.dsl.BeanPath를 직접 상속받으며, 논리적인 상위 타입으로 타입 변환할 수 없다.

자바 타입 변환을 직접 사용하는 대신, _super 필드를 통해서 상위 타입에 대한 레퍼런스에 접근할 수 있다. 생성된 쿼리 타입이 한 개 상위 타입을 가질 경우, _super 필드를 사용할 수 있다.

// from Account
QAccount extends EntityPathBase<Account> {
    // ...
}

// from BankAccount extends Account
QBankAccount extends EntityPathBase<BankAccount> {

    public final QAccount _super = new QAccount(this);

    // ...
}

상위 타입에서 하위 타입으로 변환하려면 EntityPathBase 클래스의 as 메서드를 사용하면 된다.

QAccount account = new QAccount("account");
QBankAccount bankAccount = account.as(QBankAccount.class);

3.1.6. 리터럴 선택

Constant 표현식을 통해 리터럴을 선택할 수 있다. 다음은 간단한 예다.

query.list(Expressions.constant(1),
           Expressions.constant("abc"));

서브쿼리에서 Constant 표현식을 종종 사용한다.

3.2. 결과 처리

Querydsl은 결과 처리를 커스터마이징 하기 위해 행 기반 변환을 위한 FactoryExpressions과 집합을 위한 ResultTransformer를 제공하고 있다.

com.querydsl.core.types.FactoryExpression 인터페이스는 빈 생성, 생성자 호출 그리고 더 복잡한 객체를 생성하기 위해 사용된다. com.querydsl.core.types.Projections 클래스를 이용해서 FactoryExpression 구현체 기능에 접근할 수 있다.

com.querydsl.core.ResultTransformer 인터페이스의 주요 구현체는 GroupBy 클래스이다.

3.2.1. 다중 컬럼 리턴

Querydsl 3.0 부터 다중 컬럼 결과를 위한 기본 타입은 com.querydsl.core.Tuple 이다. Tuple은 타입에 안전한 Map을 제공하고, 이를 통해 Tuple 행 객체로부터 컬럼 데이터에 접근할 수 있다.

List<Tuple> result = query.from(employee).list(employee.firstName, employee.lastName);
for (Tuple row : result) {
     System.out.println("firstName " + row.get(employee.firstName));
     System.out.println("lastName " + row.get(employee.lastName));
}}

위 예제를 QTuple 클래스를 이용하면 다음과 같이 작성할 수 있다.

List<Tuple> result = query.from(employee).list(new QTuple(employee.firstName, employee.lastName));
for (Tuple row : result) {
     System.out.println("firstName " + row.get(employee.firstName));
     System.out.println("lastName " + row.get(employee.lastName));
}}

3.2.2. 빈 생성(population)

쿼리 결과로부터 빈을 생성하고 싶다면, Bean 프로젝션을 사용하면 된다.

List<UserDTO> dtos = query.list(
    Projections.bean(UserDTO.class, user.firstName, user.lastName));

setter 메서드 대신 필드에 직접 접근해야 한다면 다음 코드를 사용하면 된다.

List<UserDTO> dtos = query.list(
    Projections.fields(UserDTO.class, user.firstName, user.lastName));

3.2.3. 생성자 사용

생성자 기반의 행 변환을 하는 방법은 다음과 같다.

List<UserDTO> dtos = query.list(
    Projections.bean(UserDTO.class, user.firstName, user.lastName));

지네릭 생성자 표현식을 사용하는 대신, QueryProjection 어노테이션을 적용한 생성자를 사용할 수도 있다.

class CustomerDTO {

  @QueryProjection
  public CustomerDTO(long id, String name) {
     ...
  }

}

그리고, 이 클래스를 다음과 같이 쿼리에서 사용 가능하다.

QCustomer customer = QCustomer.customer;
JPQLQuery query = new HibernateQuery(session);
List<CustomerDTO> dtos = query.from(customer).list(new QCustomerDTO(customer.id, customer.name));

이 예제가 Hibernate용 코드이긴 하지만, 다른 모든 모듈에서도 이 기능을 사용할 수 있다.

만약 QueryProjection 어노테이션이 적용된 타입이 엔티티(@Entity) 타입이 아니라면, 예제처럼 생성자 프로젝션을 사용할 수 있다. 하지만, 어노테이션이 적용된 타입이 엔티티(@Entity) 타입이라면 쿼리 타입의 정적 create 메서드를 실행해서 생성자 프로젝션을 만들 필요가 있다.

@Entity
class Customer {

  @QueryProjection
  public Customer(long id, String name) {
     ...
  }

}
QCustomer customer = QCustomer.customer;
JPQLQuery query = new HibernateQuery(session);
List<Customer> dtos = query.from(customer).list(QCustomer.create(customer.id, customer.name));

코드 생성을 할 수 없다면, 다음과 같이 생성자 프로젝션을 생성할 수 있다.

List<Customer> dtos = query.from(customer)
    .list(Projections.constructor(Customer.class, customer.id, customer.name));

3.2.4. 결과 집합(aggregation)

com.querydsl.core.group.GroupBy 클래스는 메모리에서 쿼리 결과에 대한 집합 연산을 수행하는 집합 함수를 제공한다. 다음은 사용 예이다.

부모 자식 관계에 대한 집합 연산

import static com.querydsl.core.group.GroupBy.*;

Map<Integer, List<Comment>> results = query.from(post, comment)
    .where(comment.post.id.eq(post.id))
    .transform(groupBy(post.id).as(list(comment)));

이 코드는 post id와 관련된 커멘트를 매핑한다.

다중 결과 컬럼

Map<Integer, Group> results = query.from(post, comment)
    .where(comment.post.id.eq(post.id))
    .transform(groupBy(post.id).as(post.name, set(comment.id)));

이 코드는 post id와 Group을 매핑한다. Group은 post name과 comment id를 갖는다.

Group은 GroupBy에 대해 Tuple 인터페이스와 같은 역할을 한다.

더 많은 예제는 여기를 참고한다.

3.3. 코드 생성

Querydsl은 JPA, JDO, Mongodb 모듈에서 코드 생성을 위해 자바6의 APT 어노테이션 처리 기능을 사용한다. 이 절에서는 코드 생성을 위한 다양한 설정 옵션과 APT에 대한 대안을 설명한다.

3.3.1. 경로 초기화

기본적으로 Querydsl은 처음 2레벨의 레퍼런스 프로퍼티만 초기화한다. 더 깊은 경로로 초기화해야 한다면, com.querydsl.core.annotations.QueryInit 애노테이션을 도메인 타입에 적용해야 한다. 더 깊은 레벨로 초기화가 필요한 프로퍼티에 QueryInit 어노테이션을 적용한다. 다음은 적용 예를 보여주고 있다.

@Entity
class Event {
    @QueryInit("customer.address")
    Account account;
}

@Entity
class Account {
    Customer customer;
}

@Entity
class Customer {
    String name;
    Address address;
    // ...
}

이 예제는 Event 경로가 루트 경로인 /로 초기화될 때, account.customer 경로의 초기화를 실행한다. 경로 초기화 포맷은 와일드카드 문자(customer.* 또는 그냥 * 등)를 지원한다.

자동 경로 초기화는 수동 초기화를 대신하며, 엔티티 필드가 final이어선 안 된다. 선언적 포맷은 쿼리 타입의 모든 최상위 레벨 인스턴스에 적용할 수 있고 final 엔티티 필드의 사용을 가능하게 해주는 이점이 있다.

선호하는 초기화 방식은 자동 경로 초기화지만, 다음에 설명할 Config 어노테이션을 이용해서 수동 초기화를 활성화시킬 수 있다.

3.3.2. 커스터마이징

패키지나 타입에 Config 어노테이션을 사용해서 Querydsl의 직렬화를 커스터마이징할 수 있다. Querydsl은 어노테이션이 적용된 패키지와 타입의 직렬화 방식을 변경한다.

직렬화 옵션은 다음과 같다.

표 3.1. Config 옵션

이름설명
entityAccessorspublic final 필드 대신 엔티티 경로로 사용할 접근 메서드 (기본값: false)
listAccessorslistProperty(int index) 형식의 메서드 (기본값: false)
mapAccessorsmapProperty(Key key) 형식의 접근 메서드 (기본값: false)
createDefaultVariable기본 변수 생성 (기본값: true)
defaultVariableName기본 변수의 이름

다음은 몇 가지 예이다.

엔티티 타입 직렬화 커스터마이징::

@Config(entityAccessors=true)
@Entity
public class User {
    //...
}

엔티티 타입 직렬화 커스터마이징::

@Config(listAccessors=true)
package com.querydsl.core.domain.rel;

import com.querydsl.core.annotations.Config;

만약 직렬화 설정을 글로벌하게 변경하고 싶다면, 다음의 APT 옵셥을 사용하면 된다.

표 3.2. APT 옵션

이름설명
querydsl.entityAccessors레퍼런스 필드 접근 활성화
querydsl.listAccessors인덱스 이용한 리스트 직접 겁근 활성화
querydsl.mapAccessors키 기반 맵 직접 접근 활성화
querydsl.prefix쿼리 타입을 위한 접두어 (기본값: Q)
querydsl.suffix쿼리 타입을 위한 접미사
querydsl.packageSuffix쿼리 타입 패키지를 위한 접미사
querydsl.createDefaultVariable기본 변수 만들지 여부
querydsl.unknownAsEmbeddable애노테이션 비적용 클래스를 embeddable로 처리할지 여부 (기본값: false)
querydsl.includedPackages코드 생성에 포함될 패키지 목록 (콤마로 구분) (default: all)
querydsl.includedClasses코드 생성에 포함될 클래스 이름 목록 (콤마로 구분) (default: all)
querydsl.excludedPackages코드 생성에서 제외할 패키지 이름 (콤마로 구분) (default: none)
querydsl.excludedClasses코드 생성에서 제외할 클래스 이름 (콤마로 구분) (default: none)

다음은 메이븐 APT 플러그인 옵션 사용 예다.

<project>
  <build>
  <plugins>
    ...
    <plugin>
      <groupId>com.mysema.maven</groupId>
      <artifactId>apt-maven-plugin</artifactId>
      <version>1.1.3</version>
      <executions>
        <execution>
          <goals>
            <goal>process</goal>
          </goals>
          <configuration>
            <outputDirectory>target/generated-sources/java</outputDirectory>
            <processor>com.querydsl.apt.jpa.JPAAnnotationProcessor</processor>
            <options>
              <querydsl.entityAccessors>true</querydsl.entityAccessors>
            </options>
          </configuration>
        </execution>
      </executions>
    </plugin>
    ...
  </plugins>
  </build>
</project>

3.3.3. 커스텀 타입 매핑

경로 타입을 바꾸고 싶으면 커스텀 타입 매핑을 사용하면 된다. 특정 String 경로에 대해 비교나 String 연산을 막아야 하거나 커스텀 타입을 위해 Date/Time 지원이 추가되어야 하는 경우에 커스텀 타입 매핑을 유용하게 사용할 수 있다. Joda time API와 JDK(java.util.Date, Calendar 그리고 하위 타입)의 시간 타입은 기본으로 지원하며, 다른 API가 필요할 경우 이 기능을 사용하면 된다.

다음은 예시다.

@Entity
public class MyEntity {
    @QueryType(PropertyType.SIMPLE)
    public String stringAsSimple;

    @QueryType(PropertyType.COMPARABLE)
    public String stringAsComparable;

    @QueryType(PropertyType.NONE)
    public String stringNotInQuerydsl;
}

PropertyType.NONE은 쿼리 타입 생성시 프로퍼티를 생략할 때 사용된다. @Transient나 @QueryTransient 어노테이션이 적용된 프로퍼티가 영속 대상에서 빠지는 것과 차이가 난다. PropertyType.NONE은 단지 Querydsl 쿼리 타입에서 해당 프로퍼티를 제외한다.

3.3.4. 위임 메서드(Delegate methods)

정적 메서드를 위임 메서드로 선언하려면, 해당하는 도메인 타입을 값으로 갖는 QueryDelegate 어노테이션을 정적 메서드에 적용하고, 정적 메서드의 첫 번째 파라미터로 해당하는 Querydsl 쿼리 타입을 제공한다.

다음은 예제다.

@QueryEntity
public static class User {

    String name;

    User manager;

}
@QueryDelegate(User.class)
public static BooleanPath isManagedBy(QUser user, User other) {
    return user.manager.eq(other);
}

QUser 쿼리 타입의 생성된 메서드는 다음과 같다.

public BooleanPath isManagedBy(QUser other) {
    return DelegateTest.isManagedBy(this, other);
}

내장 타입을 확장하는데 위임 메서드를 사용할 수도 있다. 다음은 몇 가지 예제다.

public class QueryExtensions {

    @QueryDelegate(Date.class)
    public static BooleanExpression inPeriod(DatePath<Date> date, Pair<Date,Date> period) {
        return date.goe(period.getFirst()).and(date.loe(period.getSecond()));
    }

    @QueryDelegate(Timestamp.class)
    public static BooleanExpression inDatePeriod(DateTimePath<Timestamp> timestamp, Pair<Date,Date> period) {
        Timestamp first = new Timestamp(DateUtils.truncate(period.getFirst(), Calendar.DAY_OF_MONTH).getTime());
        Calendar second = Calendar.getInstance();
        second.setTime(DateUtils.truncate(period.getSecond(), Calendar.DAY_OF_MONTH));
        second.add(1, Calendar.DAY_OF_MONTH);
        return timestamp.goe(first).and(timestamp.lt(new Timestamp(second.getTimeInMillis())));
    }

}

내장 타입을 위한 위임 메서드를 선언하면, 위임 메서드를 알맞게 사용하는 하위 클래스가 만들어진다.

public class QDate extends DatePath<java.sql.Date> {

    public QDate(BeanPath<? extends java.sql.Date> entity) {
        super(entity.getType(), entity.getMetadata());
    }

    public QDate(PathMetadata<?> metadata) {
        super(java.sql.Date.class, metadata);
    }

    public BooleanExpression inPeriod(com.mysema.commons.lang.Pair<java.sql.Date, java.sql.Date> period) {
        return QueryExtensions.inPeriod(this, period);
    }

}

public class QTimestamp extends DateTimePath<java.sql.Timestamp> {

    public QTimestamp(BeanPath<? extends java.sql.Timestamp> entity) {
        super(entity.getType(), entity.getMetadata());
    }

    public QTimestamp(PathMetadata<?> metadata) {
        super(java.sql.Timestamp.class, metadata);
    }

    public BooleanExpression inDatePeriod(com.mysema.commons.lang.Pair<java.sql.Date, java.sql.Date> period) {
        return QueryExtensions.inDatePeriod(this, period);
    }

}

3.3.5. 애노테이션 비적용 타입

@QueryEntities 애노테이션을 만들면, 애노테이션이 적용되지 않은 타입에 대해서도 Querydsl 쿼리 타입을 생성하는 것이 가능하다. QueryEntities 애노테이션을 선택한 패키지에 넣고, value 속성에 복제할 클래스를 값으로 지정한다.

실제로 타입을 생성하려면 com.querydsl.apt.QuerydslAnnotationProcessor를 사용한다. 메이븐 설정 방법은 다음과 같다.

<project>
  <build>
  <plugins>
    ...
    <plugin>
      <groupId>com.mysema.maven</groupId>
      <artifactId>apt-maven-plugin</artifactId>
      <version>1.1.3</version>
      <executions>
        <execution>
          <goals>
            <goal>process</goal>
          </goals>
          <configuration>
            <outputDirectory>target/generated-sources/java</outputDirectory>
            <processor>com.querydsl.apt.QuerydslAnnotationProcessor</processor>
          </configuration>
        </execution>
      </executions>
    </plugin>
    ...
  </plugins>
  </build>
</project>

3.3.6. 클래스패스 기반 코드 생성

어노테이션이 적용된 자바 소스를 사용할 수 없는 경우(예를 들어, Scala나 Groovy와 같은 다른 JVM 언어를 사용했거나, 바이트코드 조작을 이용해서 어노테이션을 추가한 경우 등), GenericExporter 클래스를 사용해서 클래스패스에서 어노테이션이 적용된 클래스를 스캔하고 검색된 클래스를 위한 쿼리 타입을 생성할 수 있다.

GenericExporter를 사용하려면 querydsl-codegen 모듈을 의존에 추가해주어야 한다. (더 정확하게는 com.querydsl:querydsl-codegen:${querydsl.version} 모듈)

다음은 JPA를 위한 예제다.

GenericExporter exporter = new GenericExporter();
exporter.setKeywords(Keywords.JPA);
exporter.setEntityAnnotation(Entity.class);
exporter.setEmbeddableAnnotation(Embeddable.class);
exporter.setEmbeddedAnnotation(Embedded.class);
exporter.setSupertypeAnnotation(MappedSuperclass.class);
exporter.setSkipAnnotation(Transient.class);
exporter.setTargetFolder(new File("target/generated-sources/java"));
exporter.export(DomainClass.class.getPackage());

이 코드는 DomainClass의 패키지 및 그 하위패키지에 위치한 모든 JPA 애노테이션 적용 클래스를 찾아 target/generated-sources/java 디렉토리에 쿼리 타입을 생성한다.

3.3.6.1. 메이븐 사용법

querydsl-maven-plugin의 generic-export, jpa-exportㅡjdo-export 골을 통해 GenericExporter를 사용할 수 있다.

각 골은 Querydsl, JPA, JDO 어노테이션에 매핑된다.

설정 엘리먼트는 다음과 같다.

표 3.3. 메이븐 설정

타입엘리먼트설명
FiletargetFolder생성된 소스가 위치할 대상 폴더
booleanscalaScala 소스를 생성하려면 true (기본값: false)
String[]packages엔티티 클래스를 검색할 패키지
booleanhandleFields필드를 프로퍼티로 처리할 경우 true (기본값: true)
booleanhandleMethodsgetter를 프로퍼티로 처리할 경우 true (기본값: true)
StringsourceEncoding생성할 소스 파일의 캐릭터 인코딩
booleantestClasspath테스트 클래스패스를 사용하려면 true

다음은 JPA 어노테이션이 적용된 클래스를 위한 예다.

<project>
  <build>
    <plugins>
      ...
      <plugin>
        <groupId>com.querydsl</groupId>
        <artifactId>querydsl-maven-plugin</artifactId>
        <version>${querydsl.version}</version>
        <executions>
          <execution>
            <phase>process-classes</phase>
            <goals>
              <goal>jpa-export</goal>
            </goals>
            <configuration>
              <targetFolder>target/generated-sources/java</targetFolder>
              <packages>
                <package>com.example.domain</package>
              </packages>
            </configuration>
          </execution>
        </executions>
      </plugin>
      ...
    </plugins>
  </build>
</project>

위 메이븐 설정은 com.example.domain 및 그 하위 패키지의 JPA 애노테이션 적용 클래스를 찾아 target/generated-sources/java 디렉토리에 코드를 생성한다.

생성 후에, 직접 생성된 소스를 컴파일하려면 그 소스 폴더를 위한 compile 골을 사용하면 된다.

<execution>
  <goals>
    <goal>compile</goal>
  </goals>
  <configuration>
    <sourceFolder>target/generated-sources/scala</targetFolder>
  </configuration>
</execution>

compile 골은 다음 설정 엘리먼트를 갖는다.

표 3.4. 메이븐 설정

타입엘리먼트설명
FilesourceFolder소스를 생성할 소스 폴더
StringsourceEncoding소스의 캐릭터 인코딩
Stringsource컴파일러의 -source 옵션
Stringtarget컴파일러의 -target 옵션
booleantestClasspath테스트 클래스패스를 사용할 경우 true
MapcompilerOptions컴파일러 옵션

sourceFolder를 제외한 모든 옵션은 선택사항이다.

3.3.6.2. Scala 지원

Scala 출력을 원하면, 다음 설정을 사용하자.

<project>
  <build>
    <plugins>
      ...
      <plugin>
        <groupId>com.querydsl</groupId>
        <artifactId>querydsl-maven-plugin</artifactId>
        <version>${querydsl.version}</version>
        <dependencies>
          <dependency>
            <groupId>com.querydsl</groupId>
            <artifactId>querydsl-scala</artifactId>
            <version>${querydsl.version}</version>
          </dependency>
          <dependency>
            <groupId>org.scala-lang</groupId>
            <artifactId>scala-library</artifactId>
            <version>${scala.version}</version>
          </dependency>
        </dependencies>
        <executions>
          <execution>
            <goals>
              <goal>jpa-export</goal>
            </goals>
            <configuration>
              <targetFolder>target/generated-sources/scala</targetFolder>
              <scala>true</scala>
              <packages>
                <package>com.example.domain</package>
              </packages>
            </configuration>
          </execution>
        </executions>
      </plugin>
      ...
    </plugins>
  </build>
</project>

3.4. 별칭 사용법

코드 생성을 할 수 없는 경우, 표현식 생성을 위한 경로 참조 목적으로 별칭 객체를 사용할 수 있다. 자바빈 객체의 프록시를 생성하고 프록시 객체의 getter 메서드를 경로로 사용할 수 있다.

다음 예제는 생성된 타입을 이용한 표현식 생성을 대신해서 어떻게 별칭 객체를 사용할 수 있는 보여준다.

먼저 다음 예제는 APT가 생성된 도메인 타입을 이용해서 쿼리를 실행하는 코드를 보자.

QCat cat = new QCat("cat");
for (String name : query.from(cat,cats)
    .where(cat.kittens.size().gt(0))
    .list(cat.name)) {
    System.out.println(name);
}

그리고, 다음은 Cal 클래스를 이용해서 별칭 인스턴스를 사용하는 예이다. $ 메서드 파라미터인 c.getKittens()는 내부적으로 프로퍼티 경로 c.kittens로 변환된다.

Cat c = alias(Cat.class, "cat");
for (String name : query.from($(c),cats)
    .where($(c.getKittens()).size().gt(0))
    .list($(c.getName()))) {
    System.out.println(name);
}

별칭 기능을 사용하려면 다음의 두 import를 추가해야 한다.

import static com.querydsl.core.alias.Alias.$;
import static com.querydsl.core.alias.Alias.alias;

다음 예는 앞 예제의 변형이다. $ 메서드 안에서 리스트의 size()에 접근하고 있다.

Cat c = alias(Cat.class, "cat");
for (String name : query.from($(c),cats)
    .where($(c.getKittens().size()).gt(0))
    .list($(c.getName()))) {
    System.out.println(name);
}

기본 데이터 타입이 아니고 final이 아닌 모든 별칭 프로퍼티는 별칭 자체이다. 따라서, $ 메서드 내에서 기본 데이터 타입이나 final 타입이 나오기 전까지 메서드를 이어서 호출할 수 있다. 다음은 예이다.

$(c.getMate().getName())

is transformed into *c.mate.name* internally, but

$(c.getMate().getName().toLowerCase())

이 코드는 내부적으로 toLowerCase()으로 변환된다.

별칭 타입에서는 getters, size(), contains(Object), get(int)만 호출할 수 있음에 유의하자. 모든 다른 메서드 호출은 익셉션을 발생시킨다.

4장. 문제해결

4.1. 불충분한 타입 인자

Querydsl이 코드 생성을 하려면 List, Set, Collection, Map 프로퍼티가 올바르게 인코딩 되어 있어야 한다.

올바르게 인코딩되지 않은 필드나 getter를 사용할 경우, 다음과 같은 에러가 발생한다.

java.lang.RuntimeException: Caught exception for field com.querydsl.jdo.testdomain.Store#products
  at com.querydsl.apt.Processor$2.visitType(Processor.java:117)
  at com.querydsl.apt.Processor$2.visitType(Processor.java:80)
  at com.sun.tools.javac.code.Symbol$ClassSymbol.accept(Symbol.java:827)
  at com.querydsl.apt.Processor.getClassModel(Processor.java:154)
  at com.querydsl.apt.Processor.process(Processor.java:191)
  ...
Caused by: java.lang.IllegalArgumentException: Insufficient type arguments for List
  at com.querydsl.apt.APTTypeModel.visitDeclared(APTTypeModel.java:112
  at com.querydsl.apt.APTTypeModel.visitDeclared(APTTypeModel.java:40)
  at com.sun.tools.javac.code.Type$ClassType.accept(Type.java:696)
  at com.querydsl.apt.APTTypeModel.<init>(APTTypeModel.java
  at com.querydsl.apt.APTTypeModel.get(APTTypeModel:48)
  at com.querydsl.apt.Processor$2.visitType(Processor.java:114)
  ... 35 more

다음은 문제가 되는 필드 선언과 올바른 선언의 예를 보여주고 있다.

    private Collection names; // WRONG

    private Collection<String> names; // RIGHT

    private Map employeesByName; // WRONG

    private Map<String,Employee> employeesByName; // RIGHT

4.2. 멀티쓰레드 환경에서 Querydsl Q타입의 초기화

Q타입이 순환 의존을 가질 경우, 멀티 쓰레드 환경에서 Q타입을 초기화하면 데드락이 발생할 수 있다.

이에 대한 가장 쉬운 해결책은 멀티쓰레드 환경에서 사용되기 전에 단일 쓰레드에서 클래스를 초기화하는 것이다.

이런 목적으로 com.querydsl.codegen.ClassPathUtils 클래스를 사용할 수 있다.

    ClassPathUtils.scanPackage(Thread.currentThread().getContextClassLoader(), packageToLoad);

packageToLoad를 실제 초기화하길 원하는 클래스의 패키지로 바꾸면 된다.

4.3. JDK5 사용

JDK 5로 프로젝트를 컴파일할 때, 다음과 같이 컴파일에 실패할 수 있다.

[INFO] ------------------------------------------------------------------------
[ERROR] BUILD FAILURE
[INFO] ------------------------------------------------------------------------
[INFO] Compilation failure
...
class file has wrong version 50.0, should be 49.0

자바6에서 사용되는 클래스 파일 버전은 50.0이고 자바5는 49.0이다.

JDK 6.0 이후 버전에서 사용가능한 APT를 광범위하게 사용하고 있기 때문에, Querydsl을 JDK 6.0에서만 테스트 했다.

만약 JDK 5.0에서 Querydsl을 사용하길 원한다면 Querydsl 자체를 직접 컴파일해야 한다.