この記事は過去記事Java版-jOOQで自動生成したJavaコードの管理方法&ついでにFlywayのサンプルをKotlinで書きなおしたものです。文章はほとんどそのままですが、少し加筆というか、加えられた工夫もあります。
2018年12月のJJUG-CCC(日本Javaユーザーズグループクロスコミュニティカンファレンス)でツール比較しながら語る O/RマッパーとDBマイグレーションの実際のところというタイトルで登壇させていただきました。
数あるO/RマッパーやDBマイグレーションツールの中でも、jOOQ推し、Flyway推しという内容でした。一方で巷のブログや人づてに聞く話ではありますが、それらの使い方を若干間違えているケースが多々あるようです。 特にまずいのが、jOOQが自動生成したJavaソースコードをgitのコミット対象としてバージョン管理しているケースがそこそこ観測されることです。それはやめたほうがいいです。 複数の開発者がうっかり同時期に同じテーブルに対してカラム追加等をすると、jOOQで自動生成した大量のJavaコード群は必ずコンフリクトを起こしてしまうからです。
jOOQは、指定したDBスキーマの構造を正確無比に読み取って、O/RマッピングをラクにするJavaコードを自動生成するのがキモです。 ということは、正確無比にバージョン管理しておくべきなのはDBスキーマ定義つまりCREATE TABLE, ALTER TABLEなどいわゆるDDL文のほうです。そこを実現するのがFlywayです。
jOOQが自動生成するのはJavaコードですので、つい src/main/java の配下を出力先として指定したくなりますが、そこが落とし穴です。gradleを使っている場合は build/の配下をjOOQの自動生成コードの出力先として指定したうえで、ソースコードの在り処も指定することで、コンパイルは通ります。IntelliJ IDEA上でも問題はありません。ただしEclipseという古代兵器のことは存じません。
同様に、jOOQが自動生成するのはJavaコードなので、 Kotlinのアプリケーションプロジェクトでそれをうまく使えるか?開発作業ができるか? といった点も心配になりますが、プロジェクト構成を適切に整理すればなんの問題もなくkotlinから呼び出すことが可能です。
git, JDK8以上, dockerが入ったPCを用意して、下記のコマンドを実行するだけです。
git clone git@github.com:nabedge/jooq-flyway-spboot-sample-kt.git
cd jooq-flyway-spboot-sample-kt
brew install postgresql (psqlコマンドが必要なだけです)
sh setup.sh
sh ./gradlew bootRun -p pj-web
または java -jar ./pj-web/build/libs/pj-web-1.0-SNAPSHOT.jar
ブラウザで http://localhost:8080 を開く
このあたりから下だけでももう少し解説しましょう。
@Test
fun generate() {
val sourceDirectory = "../pj-db/build/jooq-gen" // pj-db-genプロジェクトではなく pj-dbプロジェクトのbuildディレクトリ
val packageName = "com.example.db.jooq.gen" // 自動生成するコードのjavaパッケージ名
val strategyName = SampleNamingGeneratorStrategy::class.java.name // 自動生成するクラスの名前を少しカスタムする
val postgresDriverName = org.postgresql.Driver::class.java.name
val generatorMetaName = org.jooq.meta.postgres.PostgresDatabase::class.java.name
val jdbcUrl = "jdbc:postgresql://127.0.0.1:5432/sampledb"
val dbUser = "sampledbuser"
val dbPassword = "samplepassword"
val dbSchema = "public"
val config = Configuration()
.withJdbc(Jdbc()
.withDriver(postgresDriverName)
.withUrl(jdbcUrl)
.withUser(dbUser)
.withPassword(dbPassword))
.withGenerator(Generator()
.withDatabase(Database()
.withName(generatorMetaName)
.withIncludes(".*")
.withExcludes("flyway_schema_history") // flywayの管理用テーブルをうっかり触らないように予防
.withInputSchema(dbSchema)
)
.withGenerate(Generate()
.withImmutablePojos(true) // SELECT結果格納などに使うPOJOクラスをimmutableにする
.withPojosEqualsAndHashCode(true)
.withSerializablePojos(true)) // DB検索結果をどこかにキャッシュする場合はSerializableにしておくほうが便利
.withStrategy(Strategy()
.withName(strategyName))
.withTarget(Target()
.withPackageName(packageName)
.withDirectory(sourceDirectory)))
GenerationTool.generate(config)
ツール比較しながら語る O/RマッパーとDBマイグレーションの実際のところ の50ページ付近で話したとおり、デフォルトではjOOQはテーブル名とまったく同じ名前のJavaクラスを作ってしまいます。 カスタムを強く推奨します。 クラス名の衝突はコーディング作業中のけっこうなストレスにつながります。
そこで、上のbuild.gradleのこの部分
val strategyName = SampleNamingGeneratorStrategy::class.java.name
val config = Configuration()
(snip)
.withStrategy(Strategy()
.withName(strategyName))
が重要になってきます。このクラスは pj-db-gen/src/test/kotlin/com/example/db/jooq/gen/SampleNamingGeneratorStrategy.kt です。
open class SampleNamingGeneratorStrategy: DefaultGeneratorStrategy() {
override fun getJavaClassName(definition: Definition, mode: GeneratorStrategy.Mode): String {
val name = super.getJavaClassName(definition, mode)
return when (mode) {
GeneratorStrategy.Mode.POJO -> "${name}Vo"
GeneratorStrategy.Mode.RECORD -> name
GeneratorStrategy.Mode.DEFAULT -> "J${name}"
else -> "J${name}"
}
}
}
見ての通り、jOOQが自動生成するJavaクラスのプレフィクスとして全て”J”がつくようになっています。これによって、実際のDBアクセスのKotlinクラスでは
val jBook: JBook = JBook.BOOK
fun selectAll(): List<Book> {
val selected: List<BookVo> = dslContext
.select(
jBook.ISBN,
jBook.TITLE,
jBook.PUBLISH_DATE
)
.from(jBook)
.orderBy(jBook.PUBLISH_DATE)
.fetchInto(BookVo::class.java)
return selected
.map { bookVo: BookVo ->
Book(
bookVo.title,
bookVo.isbn,
bookVo.publishDate.toLocalDate()
)
}
.toList()
}
このように、
これらの名前衝突を避けることができるようになります。
2019年夏ごろから、github上で様々なビルドタスクを実行できるようになりました。Github Actionsというやつです。
ただ、まだ不完全な点があって、テストが成功するとgradleのテスト結果のhtmlがアーカイブされてactions上からダウンロードできるようになっているのですが、テストが失敗するとアーカイブのタスクの前にActionそのものが終わってしまいます。つまり「どのテストが失敗したのか?」が見れないという本末転倒な状況です。どなたかyml定義の書き方を教えて下さい!
それぞれ読みながらサンプルを動かしてIDEにインポートしてみれば、勘所がおわかりいただけるかと思います。
Kotlinいいですね!