2017/12/15追記:gradle buildからJOOQのコード生成を除外
サーバサイドの開発をKotlinで行うために、ここのところ調査・検討を色々していました。
概ね問題なさそうという結論が出たので、それ以外の技術選定をしていたのですが、 O/R Mapperに採用することにしたJOOQをGradleと組み合わせたときに、悩むことが多かったので、 現時点でのやり方と考えを残しておこうと思います。
JOOQ
JOOQ(juke:ジュークと読むらしい)はJava用のO/R Mapperなのですが、KotlinのDSLもあり、Kotlinからでも(私としては)違和感なく使えるライブラリになっています。
JOOQ選定理由の大きなところは以下になります。
- Kotlin対応が考慮されている
- DSLがSQLの文法とほぼ同じで、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スキーマを一つに絞るのは、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マイグレーションと合わせてタスク化するなど、別の方法も取れると認識していますし、 そちらの方がいいこともあると思っています。
なのでこんな方法で運用しているよーとかあれば、コメントなどで頂けると嬉しいです。