3.3. 코드 생성

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

3.3.1. 경로 초기화

기본적으로 Querydsl은 처음 2레벨의 레퍼런스 프로퍼티만 초기화한다. 더 깊은 경로로 초기화해야 한다면, com.mysema.query.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.mysema.query.domain.rel;

import com.mysema.query.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.mysema.query.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 com.mysema.query.domain.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.mysema.query.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.mysema.query.apt.QuerydslAnnotationProcessor</processor>
          </configuration>
        </execution>
      </executions>
    </plugin>
    ...
  </plugins>
  </build>
</project>

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

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

GenericExporter를 사용하려면 querydsl-codegen 모듈을 의존에 추가해주어야 한다. (더 정확하게는 com.mysema.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.mysema.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.mysema.querydsl</groupId>
        <artifactId>querydsl-maven-plugin</artifactId>
        <version>${querydsl.version}</version>
        <dependencies>
          <dependency>
            <groupId>com.mysema.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>