Spring BootにjOOQ + Flyway + MySQLを導入する方法(Kotlinの場合)


Spring BootにjOOQ + Flyway + MySQLをKotlinで導入する記事があまりなかった&自分の学習の一環として導入する手順を確認し、理解を深めるため記事にしました。
これから新規でサーバーサイドKotlinを始める方の参考に少しでもなれば幸いです。

前提

記事内に出てこない開発環境は以下の通りです。

Gradle:7.3.2
Kotlin:1.5.31
JVM:11.0.14 (Amazon.com Inc. 11.0.14+9-LTS)
Spring Boot:2.6.3

dockerでMySQLを用意する

今回はお手軽に準備ができるdockerを使用し、MySQLを用意しました。
自分が用意したdocker-compose.ymlを参考までに載せておきます。

docker-compose.yml
version: "3.8"
services:
  mysql:
    container_name: todo-mysql
    image: mysql:8.0
    restart: always
    ports:
      - ${MYSQL_PORT}:3306
    environment:
      MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
      MYSQL_DATABASE: ${MYSQL_DB_NAME}
      MYSQL_USER: ${MYSQL_USER}
      MYSQL_PASSWORD: ${MYSQL_PASSWORD}

docker-compose up -dで起動すれば、MySQLが利用できるはずです。

また環境変数は.envを用意し、direnvで読み込んでいます。

.env(ローカル開発環境用)
MYSQL_HOST=localhost
MYSQL_PORT=3306
MYSQL_ROOT_PASSWORD=password
MYSQL_DB_NAME=example
MYSQL_USER=user
MYSQL_PASSWORD=password
MYSQL_OPTIONS=autoReconnect=true&allowPublicKeyRetrieval=true&useSSL=false
MYSQL_URL=jdbc:mysql://${MYSQL_HOST}:${MYSQL_PORT}/${MYSQL_DB_NAME}?${MYSQL_OPTIONS}

Flywayプラグインのセットアップ

今回はFlywayが公式で提供しているGradleプラグインを利用します。

build.gradle.kts
plugins {
    id("org.flywaydb.flyway") version "8.0.1"
}

dependencies {
    runtimeOnly("mysql:mysql-connector-java")
}

flyway {
    url = System.getenv("MYSQL_URL")
    user = System.getenv("MYSQL_USER")
    password = System.getenv("MYSQL_PASSWORD")
}

これにより、GraldeのtasksにFlywayのtaskが追加されます。

利用方法

マイグレーション用のSQLの準備

まずはマイグレーション用のディレクトリをresourcesに用意します。
デフォルトの設定ではdb/migration配下にSQLファイルを配置すればOKです。

resources
└── db
    └── migration
        └── V1.0.0__create_table.sql

ちなみにファイル名はV<Version>__<Description>.sqlという形式が基本になります。
更に詳細を理解したい方は、以下のページが参考になります。

https://flywaydb.org/documentation/concepts/migrations.html#naming

コマンド

あとはマイグレーションを実際に実行していくだけです。
その際に、よく使うコマンドをいくつか紹介しておきます。

$ ./gradlew flywayMigrate // マイグレーションを実行する

$ ./gradlew flywayRepair // スキーマの履歴テーブルを修復する

$ ./gradlew flywayClean // 全テーブルをdropする

その他の情報については、公式のドキュメントを参考にしてみてください。

https://flywaydb.org/documentation/usage/gradle/

余談

Spring Bootに詳しい方の中には、flyway-coreを導入すれば起動時にデフォルトでマイグレーションを行ってくれるので良いのでは?といった疑問をもつ方もいるかもしれません。
この反応は当然で自分も最初はflyway-coreを導入して起動できないかと試してみたのですが、後述するjOOQのプラグインとの兼ね合いから採用を諦めました。

ただし、jOOQのプロセスをSpring Bootに上手く組み込むことができれば可能かもしれません。
いずれ時間ができた際には、調べてみたいと思っています。

jOOQプラグインのセットアップ

jOOQは公式のGradleプラグインがないため、公式が紹介しているサードパーティのgradle-jooq-pluginを導入します。

build.gradle.kts
plugins {
    id("nu.studer.jooq") version "7.1.1"
}

dependencies {
    jooqGenerator("mysql:mysql-connector-java")
    jooqGenerator("jakarta.xml.bind:jakarta.xml.bind-api:3.0.1")
}

jooq {
    configurations {
        create("main") {
            jooqConfiguration.apply {
                jdbc.apply {
                    url = System.getenv("MYSQL_URL")
                    user = System.getenv("MYSQL_USER")
                    password = System.getenv("MYSQL_PASSWORD")
                }
                generator.apply {
                    name = "org.jooq.codegen.KotlinGenerator"
                    database.apply {
                        name = "org.jooq.meta.mysql.MySQLDatabase"
                        inputSchema = System.getenv("MYSQL_DB_NAME")
                        excludes = "flyway_schema_history"
                    }
                    generate.apply {
                        isDeprecated = false
                        isTables = true
                    }
                    target.apply {
                        packageName = "com.example.ktknowledgeTodo.infra.jooq"
                        directory = "${buildDir}/generated/source/jooq/main"
                    }
                }
            }
        }
    }
}

Flywayと同様に、GraldeのtasksにgenerateJooqが追加されます。
ただしFlywayと異なり、デフォルトの設定ではbuildtestの実行前にもgenerateJooqが実行されるようになります。(必要がない場合は設定でオフにもできます。)

ちなみにjooqGenerator("jakarta.xml.bind:jakarta.xml.bind-api:3.0.1")を追加しているのは、SpringのBOMがjakarta.xml.bind-apiのバージョンをjOOQのプラグイン側がサポートしている3系ではなく2系に強制ダウングレードするせいで起こるエラーを解消するためです。

https://github.com/etiennestuder/gradle-jooq-plugin/issues/207

設定について

今回利用している設定は基本的に公式のExampleをカスタマイズしたものなので、詳細についてはGitHubを参考にしてください。

https://github.com/etiennestuder/gradle-jooq-plugin#gradle-kotlin-dsl-4

またjOOQでMySQLのBooleanを扱う場合は別途設定が必要になるのですが、そちらは別の記事で解説をしています。

https://zenn.dev/yamachoo/articles/setup-jooq-mysql-bool

利用方法

jOOQはデータベースの状態をもとに、ORMのコードを生成してくれます。
以下のコマンドを実行すると、build/generated/source/jooq/main配下にKotlinのコードが生成されます。

$ ./gradlew generateJooq

生成されたコードの使い方は公式のドキュメントを参考にしてください。

https://www.jooq.org/learn/

基本的な使い方

基本的にはFlywayでデータベースの状態をバージョン管理しつつ、最新のデータベースの状態をもとにjOOQでリポジトリの実装に必要なコードを生成します。

jOOQで生成されるコードをGit管理するパターンも調査したところあるようですが、Git管理をしてしまうと「データベースの変更に強いというjOOQのメリットが活かしにくい」「チーム開発時にコンフリクトが多発する」といった意見に納得がいったため、自分の実装ではGit管理をしない選択をしています。

Tips

データベースの更新をすると、何度も以下のようなコマンドを打つことになると思います。

$ ./gradlew flywayMigrate
$ ./gradlew generateJooq

しかし、毎回同じコマンドを2回も打つのは手間ですし、何よりタスク名が長いので面倒です。
そのため、buildなどのタスク時にも実行されるgenerateJooqの直前に、flywayMigrateを実行させるようにタスクを設定しておくと便利です。

以下のようにbuild.gradle.ktsに記述を足すことで、generateJooqの前にflywayMigrateが実行されるようになります。

build.gradle.kts
tasks.named("generateJooq") {
    dependsOn(tasks.flywayMigrate)
}

Gradleでは-mオプションを使うとタスクの実行を全てスキップし、実行されるタスクの一覧を確認できます。
例えばbuildを-mオプションで実行してみると、generateJooqよりも前にflywayMigrateが実行されていることが確認できます。

$ ./gradlew -m build

:flywayMigrate SKIPPED
:generateJooq SKIPPED
:compileKotlin SKIPPED
:compileJava SKIPPED
... // 省略

これでデータベースの更新を気にせずにbuildなどのタスクを実行できます!

さいごに

以上でSpring BootにjOOQ + Flyway + MySQLを導入できます。
今回の実装ではjOOQとFlywayのGradleプラグインを利用しているので、Spring BootだけでなくKtorなどのフレームワークにも同様の手順で導入が可能だと思います。