Tags: kotlin flyway jOOQ

この記事は過去記事Java版-jOOQで自動生成したJavaコードの管理方法&ついでにFlywayのサンプルをKotlinで書きなおしたものです。文章はほとんどそのままですが、少し加筆というか、加えられた工夫もあります。

Java版からの変更点


TL;DR

画像

Detail

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 を開く

サンプルコードをIntelliJ IDEAにプロジェクトとしてインポートしてみる

  1. 上の手順で sh setup.sh を実行するところまでは済ませておく
  2. File -> New -> Project from existing sources
  3. jooq-flyway-spboot-sample-kt ディレクトリ直下の build.gradle を指定してOpen
  4. use auto importにチェックを入れてOKボタンを押す
  5. pj-webプロジェクトの SampleApplication.kt をクリックして起動
  6. ブラウザで http://localhost:8080 を開く

Dockerの使い方のポイント

Flywayの使い方のポイント

jOOQの使い方のポイント

jOOQによるJavaコード自動生成

このあたりから下だけでももう少し解説しましょう。

    @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)

jOOQが自動生成するコードのクラス名にプレフィクス/サフィックスをつける

ツール比較しながら語る 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()
    }

このように、

これらの名前衝突を避けることができるようになります。

Github Actions上でのビルドとテストの自動実行

2019年夏ごろから、github上で様々なビルドタスクを実行できるようになりました。Github Actionsというやつです。

ただ、まだ不完全な点があって、テストが成功するとgradleのテスト結果のhtmlがアーカイブされてactions上からダウンロードできるようになっているのですが、テストが失敗するとアーカイブのタスクの前にActionそのものが終わってしまいます。つまり「どのテストが失敗したのか?」が見れないという本末転倒な状況です。どなたかyml定義の書き方を教えて下さい!

おわりに

それぞれ読みながらサンプルを動かしてIDEにインポートしてみれば、勘所がおわかりいただけるかと思います。
Kotlinいいですね!