Gradle と Querydsl Scala を使った Querydsl JPA のコード生成

Gradle と Querydsl Scala を使って Querydsl JPAScala 用コード生成を試してみました。

ソースは http://github.com/fits/try_samples/tree/master/blog/20150727/

はじめに

Gradle を使った Querydsl JPA のコード生成」 ではアノテーションプロセッサを使って Querydsl JPA のコードを生成しましたが、Scala の場合は com.querydsl.codegen.GenericExporter クラスを使うようです。

GenericExporter でコード生成するには JPA のエンティティクラスをロードできなければなりません。 (つまり、エンティティクラスを事前にコンパイルしておく必要あり)

Gradle ビルド定義

エンティティクラスを Querydsl のコード生成前にコンパイルするため、今回は以下のようにエンティティクラスだけをコンパイルするタスク modelCompile と Querydsl 用のコードを生成するタスク generate を追加しました。

番号 概要 タスク名
(1) エンティティクラスをコンパイル modelCompile
(2) (1) のエンティティクラスを使って Querydsl JPAScala 用コードを生成 generate
(3) (2) で生成したソースをビルド compileScala

(1) では src/main/scala-model へ配置したエンティティクラスのソース (Scala) をビルドして build/classes/main へ出力します。

(2) では com.querydsl.codegen.GenericExporter を使って Scala 用の Querydsl JPA コードを src/main/qdsl-generated へ生成します。

(3) で (2) の生成したソースをビルドできるように sourceSets.main.scala.srcDirsrc/main/qdsl-generated を追加しています。

なお、(2) で (1) のクラスをロードできるように buildscriptclasspathbuild/classes/main を追加しているのですが、これが原因で初回実行時や clean 直後は (1) と (2) を別々に実行する必要があります。

これは、build/classes/main へクラスファイルが配置されていない状態 ((1) の実施前) で Gradle を実行すると given scan urls are empty. set urls in the configuration とメッセージが出力され、以降のタスクで build/classes/main をクラスパスとして認識しない事が原因です。

build.gradle
apply plugin: 'scala'

// スキャン対象の JPA エンティティクラスのパッケージ名
ext.modelPackage = 'sample.model'
// JPA エンティティクラスのソースディレクトリ
ext.modelSourceDir = 'src/main/scala-model'
// Querydsl のソース生成先ディレクトリ
ext.qdslDestDir = 'src/main/qdsl-generated'

buildscript {
    // JPA エンティティクラスのビルド結果の出力先ディレクトリ
    // buildscript の classpath へ設定する必要があるため、ここで定義している
    ext.destDir = "$buildDir/classes/main"

    repositories {
        jcenter()
    }

    dependencies {
        classpath 'com.querydsl:querydsl-codegen:4.0.2'
        classpath 'com.querydsl:querydsl-scala:4.0.2'

        classpath 'org.scala-lang:scala-library:2.11.7'

        classpath 'javax:javaee-api:7.0'
        // コード生成時に JPA エンティティクラスをロードさせるための設定
        classpath files(destDir)
    }
}

repositories {
    jcenter()
}

dependencies {
    compile 'com.querydsl:querydsl-jpa:4.0.2'
    compile 'com.querydsl:querydsl-scala:4.0.2'

    compile 'org.scala-lang:scala-library:2.11.7'

    compile 'org.apache.commons:commons-dbcp2:2.1'
    compile 'javax:javaee-api:7.0'
}

// (1) JPA エンティティクラスをコンパイル
task modelCompile(type: ScalaCompile) {
    // ソースディレクトリ
    source = modelSourceDir
    // クラスパスの設定 (buildscript のクラスパスを設定)
    classpath = buildscript.configurations.classpath
    // クラスファイルの出力先
    destinationDir = file(destDir)

    // 以下が必須 (ファイル名やパスは何でも良さそう)
    scalaCompileOptions.incrementalOptions.analysisFile = file("${buildDir}/tmp/scala/compilerAnalysis/compileCustomScala.analysis")
}

// (2) Querydsl JPA の Scala 用コードを生成
task generate(dependsOn: 'modelCompile') {
    def exporter = new com.querydsl.codegen.GenericExporter()
    // コード生成先ディレクトリの設定
    exporter.targetFolder = file(qdslDestDir)

    exporter.serializerClass = com.querydsl.scala.ScalaEntitySerializer
    exporter.typeMappingsClass = com.querydsl.scala.ScalaTypeMappings

    exporter.entityAnnotation = javax.persistence.Entity
    exporter.embeddableAnnotation = javax.persistence.Embeddable
    exporter.embeddedAnnotation = javax.persistence.Embedded
    exporter.skipAnnotation = javax.persistence.Transient
    exporter.supertypeAnnotation = javax.persistence.MappedSuperclass
    // Scala ソースの出力
    exporter.createScalaSources = true
    // コード生成の実施
    exporter.export(modelPackage)
}

// (3) ソースをビルド
compileScala {
    // generate タスクとの依存設定
    dependsOn generate
    // Querydsl のコード生成先ディレクトリを追加
    sourceSets.main.scala.srcDir qdslDestDir
}

clean {
    delete qdslDestDir
}

サンプルアプリケーション

それでは簡単なサンプルアプリケーションを作成し実行してみます。

ビルド定義

先程の build.gradle へ少しだけ手を加え、EclipseLink と MySQL を使ったサンプルアプリケーション sample.SampleApp を実行するようにしました。

build.gradle
apply plugin: 'scala'
apply plugin: 'application'

ext.modelPackage = 'sample.model'
ext.modelSourceDir = 'src/main/scala-model'
ext.qdslDestDir = 'src/main/qdsl-generated'
// 実行クラス
mainClassName = 'sample.SampleApp'

buildscript {
    ・・・
}
・・・
dependencies {
    compile 'com.querydsl:querydsl-scala:4.0.2'
    compile 'com.querydsl:querydsl-jpa:4.0.2'
    compile 'org.scala-lang:scala-library:2.11.7'
    compile 'org.apache.commons:commons-dbcp2:2.1'
    compile 'javax:javaee-api:7.0'

    // 実行用の依存ライブラリ
    runtime 'org.eclipse.persistence:eclipselink:2.6.1-RC1'
    runtime 'mysql:mysql-connector-java:5.1.36'
    runtime 'org.slf4j:slf4j-nop:1.7.12'
}
・・・

JPA エンティティクラス

JPA における一対多のリレーションシップ - EclipseLink」 で使った JPA エンティティクラスを Scala で実装し直しました。

src/main/scala-model/sample/model/Product.scala
package sample.model

import javax.persistence._

import java.util.ArrayList
import java.util.List
import java.math.BigDecimal

@Entity
class Product {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    var id: Long = _
    var name: String = _
    var price: BigDecimal = _

    @OneToMany(fetch = FetchType.EAGER, cascade= Array(CascadeType.ALL))
    @JoinColumn(name = "product_id")
    val variationList: List[ProductVariation] = new ArrayList()

    override def toString = s"Product(id: ${id}, name: ${name}, price: ${price}, variationList: ${variationList})"
}
src/main/scala-model/sample/model/ProductVariation.scala
package sample.model

import javax.persistence._

@Entity
@Table(name = "product_variation")
class ProductVariation {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    var id: Long = _
    var color: String = _
    var size: String = _

    override def toString = s"ProductVariation(id: ${id}, color: ${color}, size: ${size})"
}

実行クラス

Querydsl JPA を使った単純な検索処理を行います。

src/main/scala/sample/SampleApp.scala
package sample

import sample.model.Product
import sample.model.ProductVariation
import sample.model.QProduct

import com.querydsl.jpa.impl.JPAQuery

import javax.persistence.Persistence
import java.math.BigDecimal

import scala.collection.JavaConversions._

object SampleApp extends App{
    def product(name: String, price: BigDecimal, variationList: ProductVariation*) = {
        val res = new Product()
        res.name = name
        res.price = price
        variationList.foreach(res.variationList.add)
        res
    }

    def variation(color: String, size: String) = {
        val res = new ProductVariation()
        res.color = color
        res.size = size
        res
    }

    val emf = Persistence.createEntityManagerFactory("jpa")
    val em = emf.createEntityManager()

    val tx = em.getTransaction()
    tx.begin()

    val p1 = product(
        "sample" + System.currentTimeMillis(), 
        new BigDecimal(1250),
        variation("White", "L"),
        variation("Black", "M")
    )

    em.persist(p1)

    tx.commit()

    val p = QProduct as "p"

    val query = new JPAQuery[Product](em)

    // Querydsl JPA による検索
    val res = query.from(p).where(p.name.startsWith("sample")).fetch()
    // 結果の出力
    res.foreach(println)

    em.close()
}

実行

JPA における一対多のリレーションシップ - EclipseLink」 で使った DB や JPA 設定ファイルを使って実行します。

src/main/resources/META-INF/persistence.xml
<persistence
    xmlns="http://java.sun.com/xml/ns/persistence"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd"
    version="2.0">

    <persistence-unit name="jpa">
        <class>sample.model.Product</class>
        <class>sample.model.ProductVariation</class>
        <properties>
            <property name="javax.persistence.jdbc.driver" value="com.mysql.jdbc.Driver" />
            <property name="javax.persistence.jdbc.url" value="jdbc:mysql://localhost:3306/jpa_sample" />
            <property name="javax.persistence.jdbc.user" value="root" />

            <property name="eclipselink.logging.level" value="FINE" />
        </properties>
    </persistence-unit>
</persistence>

初回実行時や clean 直後は、modelCompilegenerate 以降のタスクを分けて実行する必要があります。 (上の方でも書きましたが buildscriptclasspathbuild/classes/main を設定している事が原因です)

エンティティクラスのコンパイル (modelCompile タスクの実行)
> gradle modelCompile

given scan urls are empty. set urls in the configuration
:modelCompile

以下のファイルが生成されます。

  • src/main/qdsl-generated/sample/model/QProduct.scala
  • src/main/qdsl-generated/sample/model/QProductVariation.scala
実行結果 (run タスクの実行)
> gradle run

:compileJava UP-TO-DATE
:modelCompile
:generate
:compileScala
:processResources
:classes
:run
・・・
Product(id: 3, name: sample1, price: 100, variationList: [ProductVariation(id: 4, color: Black, size: M), ProductVariation(id: 5, color: White, size: L)])
Product(id: 4, name: sample1437821487341, price: 1250, variationList: [ProductVariation(id: 6, color: White, size: L), ProductVariation(id: 7, color: Black, size: M)])