Gradle を使った Querydsl SQL のコード生成

前々回前回 に続き、今回は Querydsl SQL のコード生成を Gradle で実施してみました。

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

なお、Querydsl 4.0 からパッケージ名等が変更になるようなのでご注意ください。(github の master ブランチでは com.querydsl となっています)

Gradle を使ったコード生成

Querydsl SQL の場合は Querydsl JPA・MongoDB と違って DB からメタデータを取得してコードを生成します。

querydsl-sql-codegen モジュールを使用し、アノテーションプロセッサではなく Ant 用のタスクや API を使ってコードを生成します。

(a) Ant 用のタスクを使用

まずは Ant 用のタスククラス com.mysema.query.sql.ant.AntMetaDataExporter を Gradle から使ってコード生成してみます。

基本的に、DB への接続が発生するため JDBC ドライバーが必要となります。 また、コード生成のログを出力するには slf4j を使います。(slf4j を使わなくても支障はありません)

今回は MySQL を使いました。

build1.gradle
apply plugin: 'java'
// DB 接続 URL
def qdslDbUrl = 'jdbc:mysql://localhost:3306/sample1?user=root'
// コード生成先ディレクトリ
def qdslDestDir = 'src/main/qdsl-generated'
// 生成するコードの所属パッケージ
def qdslDestPackage = 'sample.model'

repositories {
    jcenter()
}

configurations {
    querydsl
}

dependencies {
    // コード生成用の依存モジュール定義
    querydsl 'com.mysema.querydsl:querydsl-sql-codegen:3.6.3'
    querydsl 'mysql:mysql-connector-java:5.1.35'
    querydsl 'org.slf4j:slf4j-simple:1.7.12'

    compile 'com.mysema.querydsl:querydsl-sql:3.6.3'
}

task generate << {
    // Ant 用のタスク定義
    ant.taskdef(
        name: 'export', 
        classname: 'com.mysema.query.sql.ant.AntMetaDataExporter', 
        classpath: configurations.querydsl.asPath
    )

    // コード生成の実行
    ant.export(
        jdbcDriverClass: 'com.mysql.jdbc.Driver',
        dbUrl: qdslDbUrl,
        targetSourceFolder: qdslDestDir,
        targetPackage: qdslDestPackage
    )
}

compileJava {
    dependsOn generate
    sourceSets.main.java.srcDir qdslDestDir
}

clean {
    delete qdslDestDir
}

ビルド結果は以下の通りです。
事前に MySQL を起動し、テーブル等を作成しておきます。

ビルド例
> gradle -b build1.gradle build

:generate
[ant:export] [main] INFO com.mysema.query.sql.codegen.MetaDataExporter - Exported product successfully
[ant:export] [main] INFO com.mysema.query.sql.codegen.MetaDataExporter - Exported variation successfully
:compileJava
:processResources UP-TO-DATE
:classes
:jar
:assemble
:compileTestJava UP-TO-DATE
:processTestResources UP-TO-DATE
:testClasses UP-TO-DATE
:test UP-TO-DATE
:check UP-TO-DATE
:build

BUILD SUCCESSFUL

generate タスクの実行時に以下のようなソースコードが生成されました。

  • src/main/qdsl-generated/sample/model/QProduct.java
  • src/main/qdsl-generated/sample/model/QVariation.java

ちなみに、今回は以下のようなテーブルを用意しています。

DDL
CREATE TABLE `product` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(50) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

CREATE TABLE `variation` (
  `product_id` int(11) NOT NULL,
  `size` varchar(10) NOT NULL,
  `color` varchar(10) NOT NULL,
  PRIMARY KEY (`product_id`,`size`,`color`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

(b) API を使用

次は、コード生成の APIcom.mysema.query.sql.codegen.MetaDataExporter クラス)を Gradle から直接使います。

MetaDataExporter クラスへ出力パッケージや出力ディレクトリなどを設定した後、export メソッドDatabaseMetaData を渡せばコードを生成します。

なお、今回は使っていませんが、com.mysema.query.sql.Configuration で特定のカラムの型 (コード生成の Java の型) を変更するような事も可能です。

build2.gradle
apply plugin: 'java'

def qdslDbUrl = 'jdbc:mysql://localhost:3306/sample1?user=root'
def qdslDestDir = 'src/main/qdsl-generated'
def qdslDestPackage = 'sample.model'

buildscript {
    repositories {
        jcenter()
    }

    dependencies {
        // コード生成用の依存モジュール定義
        classpath 'com.mysema.querydsl:querydsl-sql-codegen:3.6.3'
        classpath 'mysql:mysql-connector-java:5.1.35'
    }
}

repositories {
    jcenter()
}

dependencies {
    compile 'com.mysema.querydsl:querydsl-sql:3.6.3'
}

task generate << {
    // DB 接続
    def con = new com.mysql.jdbc.Driver().connect(qdslDbUrl, null)

    def exporter = new com.mysema.query.sql.codegen.MetaDataExporter()

    exporter.targetFolder = new File(qdslDestDir)
    exporter.packageName = qdslDestPackage
    // コード生成の実行
    exporter.export(con.metaData)

    con.close()
}

compileJava {
    dependsOn generate
    sourceSets.main.java.srcDir qdslDestDir
}

clean {
    delete qdslDestDir
}

ビルド結果は以下の通りです。

ビルド例
> gradle -b build2.gradle build

:generate
:compileJava
:processResources UP-TO-DATE
:classes
:jar
:assemble
:compileTestJava UP-TO-DATE
:processTestResources UP-TO-DATE
:testClasses UP-TO-DATE
:test UP-TO-DATE
:check UP-TO-DATE
:build

BUILD SUCCESSFUL

成果物は (a) のケースと同じです。

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

最後に、簡単なサンプルアプリケーションを実装し実行してみます。

ビルド定義

(b) のビルド定義をベースに以下のようなビルド定義を用意しました。

build.gradle
apply plugin: 'application'

def enc = 'UTF-8'
tasks.withType(AbstractCompile)*.options*.encoding = enc

def qdslDbUrl = 'jdbc:mysql://localhost:3306/sample1?user=root'
def qdslDestDir = 'src/main/qdsl-generated'
def qdslDestPackage = 'sample.model'

// 実行クラス
mainClassName = 'sample.App'

buildscript {
    repositories {
        jcenter()
    }

    dependencies {
        classpath 'com.mysema.querydsl:querydsl-sql-codegen:3.6.3'
        classpath 'mysql:mysql-connector-java:5.1.35'
    }
}

repositories {
    jcenter()
}

dependencies {
    compile 'com.mysema.querydsl:querydsl-sql:3.6.3'
    // 実行用の依存モジュール定義
    runtime 'mysql:mysql-connector-java:5.1.35'
    runtime 'org.slf4j:slf4j-nop:1.7.12'
}

task generate << {
    def con = new com.mysql.jdbc.Driver().connect(qdslDbUrl, null)

    def exporter = new com.mysema.query.sql.codegen.MetaDataExporter()

    exporter.targetFolder = new File(qdslDestDir)
    exporter.packageName = qdslDestPackage

    exporter.export(con.metaData)

    con.close()
}

compileJava {
    dependsOn generate
    sourceSets.main.java.srcDir qdslDestDir
}

clean {
    delete qdslDestDir
}

アプリケーションクラス

product・variation テーブルへそれぞれデータを insert して select するだけの単純な処理を実装しました。

insert は set メソッドを使う方法と columnsvalues メソッドを使う方法を用いてみました。 (他にも Bean を populate する方法があります)

src/main/sample/App.java
package sample;

import com.mysema.query.sql.Configuration;
import com.mysema.query.sql.MySQLTemplates;
import com.mysema.query.sql.SQLQuery;
import com.mysema.query.sql.dml.SQLInsertClause;

import sample.model.QProduct;
import sample.model.QVariation;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;

public class App {
    public static void main(String... args) throws SQLException {
        String dbUrl = "jdbc:mysql://localhost:3306/sample1?user=root";

        Configuration conf = new Configuration(new MySQLTemplates());

        QProduct p = QProduct.product;
        QVariation v = QVariation.variation;

        try (Connection con = DriverManager.getConnection(dbUrl)) {
            con.setAutoCommit(false);

            // product テーブルへレコードを insert
            Integer id = new SQLInsertClause(con, conf, p)
                    .set(p.name, "test" + System.currentTimeMillis())
                    .executeWithKey(p.id);

            // variation テーブルへレコードを insert
            new SQLInsertClause(con, conf, v)
                    .columns(v.productId, v.size, v.color)
                    .values(id, "L", "white")
                    .execute();

            // variation テーブルへレコードを insert
            new SQLInsertClause(con, conf, v)
                    .columns(v.productId, v.size, v.color)
                    .values(id, "S", "blue")
                    .execute();

            con.commit();

            // product と variation を join して select
            new SQLQuery(con, conf)
                    .from(p)
                    .join(v).on(v.productId.eq(p.id))
                    .where(p.name.startsWith("test"))
                    .list(p.id, p.name, v.color, v.size)
                    .forEach(System.out::println);
        }
    }
}

実行

実行結果は以下の通りです。

実行結果
> gradle run

・・・
:run
[1, test1428773665876, white, L]
[1, test1428773665876, blue, S]

BUILD SUCCESSFUL