White Box技術部

WEB開発のあれこれ(と何か)

JOOQをGradleプロジェクトで使う

2017/12/15追記:gradle buildからJOOQのコード生成を除外

サーバサイドの開発をKotlinで行うために、ここのところ調査・検討を色々していました。

概ね問題なさそうという結論が出たので、それ以外の技術選定をしていたのですが、 O/R Mapperに採用することにしたJOOQをGradleと組み合わせたときに、悩むことが多かったので、 現時点でのやり方と考えを残しておこうと思います。

JOOQ

JOOQ(juke:ジュークと読むらしい)はJava用のO/R Mapperなのですが、KotlinのDSLもあり、Kotlinからでも(私としては)違和感なく使えるライブラリになっています。

JOOQ選定理由の大きなところは以下になります。

  • Kotlin対応が考慮されている
  • DSLSQLの文法とほぼ同じで、JOIN句が書きやすく、直感的になっている
  • ライブラリを利用するための事前準備が、ソースコードと独立している(アノテーション利用ではないこと)
  • 必要であればSQLがそのまま実行できる
  • 公式ドキュメントでそれなりに疑問が解消できる

利用よりも運用が悩ましい

使う分には違和感がなかったJOOQでも、運用方法についてはまだしっくりきていません。

今のところは「Gradleプロジェクトで利用するのであれば、Gradleで完結させよう」という方針で使っています。

Gradleとの組み合わせ

GradleでJOOQを利用するのには、gradle-jooq-pluginを使いました。

以下はkotlin-api-sampleで最初のあたりに書いていた、build.gradleのJOOQ設定部分です。

jooq {
    version = '3.10.1'
    edition = 'OSS'
    tables(sourceSets.main) {
        jdbc {
            driver = 'org.h2.Driver'
            url = 'jdbc:h2:./test;AUTO_SERVER=TRUE'
            user = 'sa'
            password = ''
        }
        generator {
            name = 'org.jooq.util.DefaultGenerator'
            database {
                name = 'org.jooq.util.h2.H2Database'
                includes = '.*'
                excludes = ''
            }
            target {
                packageName = 'box.white.seriwb.api.jooq'
                directory = 'src/main/java'
            }
        }
    }
}

ちなみにdependenciesに記載する、JOOQに関係するruntimeとjooqRuntimeには 以下のようにバージョン番号まで指定しないとgenerateコマンドがエラーとなり、動作しませんでした。

runtime('com.h2database:h2:1.4.196')
jooqRuntime('com.h2database:h2:1.4.196')

この設定を記載した際は以下のことを考えていました。

  • Spring starterで入るJOOQとコード生成に使うJOOQのバージョンを合わせるため、バージョンを指定しよう
  • テーブルに対しての処理だから、タスク名部分はtablesがいいだろう
  • 生成ファイルは直接src/main/javaに出力すれば、管理が楽だろう
  • 生成先のパッケージ名の最後はわかりやすくjooqにしよう
  • 本番環境でコード生成する必要はない(DBマイグレーション管理しているため、開発環境での生成でOKだろう)

実際、これで動作するようには作れるのですが、次第に以下が気になってきます。

  • 実装に利用しないDBスキーマも自動生成対象にしてしまっている
  • gradle cleanすると、src/main/java配下の生成したファイルが消えてしまう

自動生成対象の選定

対象のDBスキーマを一つに絞るのは、inputSchemaを指定することで簡単にできます。
例えば、target_db_nameというMySQLのDBスキーマを対象にした場合は、以下のようになります。

jooq {
    version = '3.10.1'
    edition = 'OSS'
    tables(sourceSets.main) {
        jdbc {
            driver = 'com.mysql.cj.jdbc.Driver'
            url = 'jdbc:mysql://localhost:3306'
            user = 'admin'
            password = 'admin'
        }
        generator {
            name = 'org.jooq.util.DefaultGenerator'
            database {
                name = 'org.jooq.util.mysql.MySQLDatabase'
                inputSchema = 'target_db_name'
                includes = '.*'
                excludes = ''
            }
            target {
                packageName = 'box.white.seriwb.api.jooq'
                directory = 'src/main/java'
            }
        }
    }
}

一つの場合はこれでいいのですが、複数ある場合はこれだと対応ができません。

なので複数の場合はinputSchemaを指定するのではなく、 以下のように不要なものを除く書き方をすることで、必要な分だけをコードにすることができます。

database {
    name = 'org.jooq.util.mysql.MySQLDatabase'
    includes = '.*'
    excludes = '(?i:(information_schema|sys|mysql|performance_schema)\\..*)'
}

試してはいませんが、反対に必要なものだけをincludesに記載するやり方でも同じ結果になるはずです。

gradle cleanから自動生成したコードをガード

これは自動生成コードの出力先をデフォルトに戻し、別タスクでsrc/main/java配下にコピーすることで対応しました。

import org.gradle.util.GFileUtils

中略

sourceSets {
    initJooq
}

jooq {
    version = '3.10.1'
    edition = 'OSS'
    tables(sourceSets.initJooq) {
        jdbc {
            driver = 'org.h2.Driver'
            url = 'jdbc:h2:./test;AUTO_SERVER=TRUE'
            user = 'sa'
            password = ''
        }
        generator {
            name = 'org.jooq.util.DefaultGenerator'
            database {
                name = 'org.jooq.util.h2.H2Database'
                inputSchema = ''
                includes = '.*'
                excludes = ''
            }
            target {
                packageName = 'box.white.seriwb.api.jooq'
            }
        }
    }
}

// cleanタスクで生成されたコードが削除されないようにするため、1つ段階を踏む
task copyJooq(dependsOn: [clean, generateTablesJooqSchemaSource]) {
    doLast {
        File targetDir = new File("src/main/java/box/white/seriwb/api/jooq")
        targetDir.deleteDir()

        File jooqDir = new File("build/generated-src/jooq/tables")
        GFileUtils.copyDirectory(jooqDir, new File("src/main/java"))
    }
}

コピーするタスクを追加するだけでは、自動生成のコードがsourceSets.mainになるため、 クラス重複エラーがコンパイル時に発生するようになるため、sourceSetsに新しくinitJooqを追加し、 生成したコードはこちらのsourceSetになるようにして、クラス重複を回避しています(裏技みたいですね・・・)。

こうして、src/main/javaへの反映をプラグインの仕組みから切り離すことで、 cleanで自動生成ファイルが削除されることと、不意のgenerateTablesJooqSchemaSourceタスク実行によるコード変更はなくなりますが、 コード反映する場合はgradle copyJooqを手動で実行しなければいけないという制約もできます。

ちなみに、この問題の解決策として、『Gradle実行時に生成されることを期待して、自動生成ファイルをコード管理に登録しない』という方法もあると思いますが、 通常はDBスキーマ情報が同一のリポジトリで管理されていないはずですので、1リポジトリ内でコンパイルを成功させるためにも、生成したコードもリポジトリに登録するのが良いと思います。

import文の記載やdoLastの記載は、gradle実行時の警告回避によるものです。 Gradle 5.0.0以降ではtasks定義時の<<演算子などは削除される予定という旨のメッセージが出力されます。

gradle buildからJOOQのコード生成を除外

copyJooqタスクでコード生成を行うため、buildタスクで実行されるgenerateTablesJooqSchemaSourceは意味がなくなりました。 なのでbuild時にgenerateTablesJooqSchemaSourceが実行されないようにしておきましょう。

タスクの除外は、buildコマンドを実行する際にxオプションでgenerateTablesJooqSchemaSourceを指定してもいいのですが、 以下のように『generateTablesJooqSchemaSourceはbuildタスク以外のときに実行する』とbuild.gradleに記載しておくのがいいでしょう。

jooq {
    省略
}
generateTablesJooqSchemaSource.onlyIf { name != 'build' }

以下にこれらの内容が反映されたプロジェクトがありますので、詳細な内容はこちらからご確認ください。

やっぱり

利用までに一手間が入るライブラリは、実際に利用するよりも、運用のほうが難しく感じます。

今回は前提に「Gradleから利用」を置いていたのでこのような結論になったのですが、 もしその前提を外すならば、DBマイグレーションと合わせてタスク化するなど、別の方法も取れると認識していますし、 そちらの方がいいこともあると思っています。

なのでこんな方法で運用しているよーとかあれば、コメントなどで頂けると嬉しいです。