🗂

Kotlin-DSLなどKotlinでLiquibaseを使うためのライブラリを作った

2024/10/15に公開

※大事なことなので2回書きました

課題

  • Kotlinを使っているプロダクトで、LiquibaseによるマイグレーションでもKotlinの文法が使えるようになったら、Kotlinでのコーディングと同じノリで書けるようになるため、Kotlinエンジニアにとってはありがたいが、そういうライブラリがあまり無い。
  • 今まであったLiquibaseのKotlin-DSLは、kotlin-scriptでの実行であった。そのため、IDEででの補完が効かないことがあった。
  • 今までのLiquibaseのKotlin-DSLがメンテされていない。
  • Liquibaseがコマンドラインベースでの使用を想定されていて、プログラマブルに実行をする方法が分からない
  • データマイグレーションなど、DBマイグレーションの中で、ORMを使いたいケースがある

(ちなみに)各DBマイグレーションツールについて雑にまとめ

Flyway: SQLで書くもの。
Liquibase: XMLで管理。JSON. YAML, SQL、Groovyでも書けるが、JSONやYAMLだと子要素という概念が無くプロパティのみで読みにくいので、XMLかGroovy-DSL、または今回作ったKotlin-DSLが良さそう。
Harmonica: Railsライク。メンテナンスされてなさそう。
Exposed-migration: 履歴管理のためのテーブルが無いらしく、各自で工夫が必要

解決策(作ったもの)

https://github.com/momosetkn/liquibase-kotlin
こちらです。

https://momosetkn.github.io/liquibase-kotlin-docs/home.html
ドキュメントは、こちら

機能としては、以下の通りです。

  • Liquibaseを実行できるKotlinのラッパー
  • kotlin-scriptと通常のコンパイルするKotlinでマイグレーションを書くことができる、Kotlin-DSLのモジュール
  • Liquibaseのマイグレーションの中で、ORMを使うことができるモジュール

CIにも工夫というか努力をしており、毎日SNAPSHOT版のLiquibaseでのテストを定期実行するようにしています。非公式のLiquibaseのDSLは、最新のLiquibaseに追いついていないという課題がだいたいあるため、ここは重要なところだと思います。
また、私の作成したプロダクトのバージョン名にLiquibaseのバージョンを含めることで、対応しているLiquibaseのバージョンがわかりやすいようにしております。
そして、Kotlin-scriptであってもIntelliJ IDEAでの補完が効くようにはなっております。

セットアップ

build.gradle.ktsに以下追加します

dependencies {
    implementation("org.liquibase:liquibase-core:4.29.2")
    implementation("io.github.momosetkn:liquibase-kotlin-starter-compiled:4.29.2-0.8.0")
}

kotlin-scriptを使いたい場合は、以下となります

dependencies {
    implementation("org.liquibase:liquibase-core:4.29.2")
    implementation("io.github.momosetkn:liquibase-kotlin-starter-script:4.29.2-0.8.0")
}

これから紹介するLiquibaseClientを使うのであれば、基本的には、liquibase-kotlin-starter-compiledで良いかなとは思います。
今までのファイルシステム上のファイルを読み取って使うように使っているのであれば、liquibase-kotlin-starter-scriptを使うことになると思います。

使い方

コンパイルされたマイグレーションでの実行を説明します。
configureLiquibaseで、Liquibaseのグローバルな設定を書きます。
LiquibaseDatabaseFactory.createで、接続先のデータベースを定義し、LiquibaseClientへchangeLogFileを渡して、マイグレーション実行となります。

import momosetkn.liquibase.client.LiquibaseDatabaseFactory
import momosetkn.liquibase.client.configureLiquibase
import momosetkn.liquibase.kotlin.parser.KotlinCompiledDatabaseChangeLog
import momosetkn.liquibase.client.LiquibaseClient

fun main() {
    // Liquibaseのグローバルな設定
    // 各設定値については以下公式ドキュメントを!
    // https://docs.liquibase.com/parameters/home.html
    configureLiquibase {
        global {
            general {
                showBanner = false
            }
        }
    }
    // データベースをセットする
    val database = LiquibaseDatabaseFactory.create(
        driver = "com.mysql.cj.jdbc.Driver",
        url = "jdbc:mysql://localhost:3306/test_db",
        username = "root",
        password = "",
    )
    // changeLogのファイルとデータベースを指定
    val client = LiquibaseClient(
        changeLogFile = example.DatabaseChangelog20241007Employee1::class.qualifiedName!!,
        database = database,
    )
    // マイグレーション実行!
    client.update()
}

class DatabaseChangelog20241007Employee1 : KotlinCompiledDatabaseChangeLog({
    changeSet(author = "your_name", id = "20241007-2000-1") {
        createTable(tableName = "employee") {
            column(name = "id", type = "UUID") {
                constraints(nullable = false, primaryKey = true)
            }
            column(name = "company_id", type = "UUID") {
                constraints(nullable = false)
            }
            column(name = "name", type = "VARCHAR(256)")
            column(name = "not_null_name", type = "VARCHAR(256)") {
                constraints(nullable = false)
            }
            column(name = "not_null_name2", type = "VARCHAR(256)") {
                constraints(nullable = false)
            }
        }
    }
})

include/includeAll

includeやincludeAllでは、パスを指定しますが、コンパイルされたKotlin(拡張子がkt)で書く場合にはパッケージ名・クラス名が使えます。
kotlin-script(拡張子がkts)から、コンパイルされたKotlinのchangeLogを呼び出す場合、クラスパス上での名前の指定となるため、.を/へ置き換えて、クラスの指定のために.classをつけます

コンパイルされたKotlinの場合

import momosetkn.liquibase.kotlin.parser.KotlinCompiledDatabaseChangeLog

class DatabaseChangelogAll : KotlinCompiledDatabaseChangeLog({
    // コンパイルされたKotlin(拡張子がkt)の場合、パッケージ名・クラス名で書く
    includeAll("example.migration")
    include("example.migration2.Example")
})

kotlin-scriptの場合

databaseChangeLog {
    // kotlin-script(拡張子がkts)の場合、クラスパス上での名前を書く
    includeAll("example/migration")
    include("example/migration2/Example.class")
}

ORMをマイグレーションの中で使う

Komapper(JDBC)

build.gradle.ktsに以下追加する

dependencies {
    implementation("io.github.momosetkn:liquibase-kotlin-custom-komapper-jdbc-change:4.29.2-0.8.0")
}

そうすると、以下のようにマイグレーションの中でKomapperでのマイグレーションが実行できるようになります。

import momosetkn.liquibase.kotlin.parser.KotlinCompiledDatabaseChangeLog
import momosetkn.liquibase.kotlin.change.custom.komapper.customKomapperJdbcChange
import org.komapper.core.dsl.QueryDsl

class CompiledDatabaseChangelog1 : KotlinCompiledDatabaseChangeLog({
    changeSet(author = "your_name", id = "20241007-2000-1") {
        customKomapperJdbcChange(
            execute = { db ->
                val query = QueryDsl.executeScript(
                    """
                    CREATE TABLE created_by_komapper (
                        id uuid NOT NULL,
                        name character varying(256)
                    );
                    """.trimIndent()
                )
                db.runQuery(query)
            },
            rollback = { db ->
                val query = QueryDsl.executeScript("DROP TABLE created_by_komapper")
                db.runQuery(query)
            },
        )
    }
})

また、kotlin-scriptの場合でも使うことができます。QueryDslがデフォルトインポートされているため、QueryDslをimport無しで使うことができます。

databaseChangeLog {
    changeSet(author = "your_name", id = "20241007-2000-1") {
        customKomapperJdbcChange(
            execute = { db ->
                val query = QueryDsl.executeScript(
                    """
                    CREATE TABLE created_by_komapper (
                        id uuid NOT NULL,
                        name character varying(256)
                    );
                    """.trimIndent()
                )
                db.runQuery(query)
            },
            rollback = { db ->
                val query = QueryDsl.executeScript("DROP TABLE created_by_komapper")
                db.runQuery(query)
            },
        )
    }
}

Exposed-migration

build.gradle.ktsに以下追加する

dependencies {
    implementation("io.github.momosetkn:liquibase-kotlin-custom-exposed-migration-change:4.29.2-0.8.0")
}

そうすると、以下のようにマイグレーションの中でExposed-migrationでのマイグレーションが実行できるようになります。

import momosetkn.liquibase.kotlin.parser.KotlinCompiledDatabaseChangeLog
import momosetkn.liquibase.kotlin.change.custom.exposed.customExposedMigrationChange
import org.jetbrains.exposed.sql.SchemaUtils
import org.jetbrains.exposed.sql.Table
import org.jetbrains.exposed.sql.transactions.transaction

class CompiledDatabaseChangelog1 : KotlinCompiledDatabaseChangeLog({
    changeSet(author = "your_name", id = "20241007-2000-1") {
        // https://jetbrains.github.io/Exposed/table-definition.html#dsl-create-table
        val createdByExposed = object : Table("created_by_exposed") {
            val id = integer("id").autoIncrement()
            val name = varchar("name", 256)
            override val primaryKey = PrimaryKey(id)
        }
        customExposedMigrationChange(
            execute = { db ->
                transaction(db) {
                    SchemaUtils.create(createdByExposed)
                }
            },
            rollback = { db ->
                transaction(db) {
                    SchemaUtils.drop(createdByExposed)
                }
            },
        )
    }
})

また、kotlin-scriptの場合でも使うことができます。org.jetbrains.exposed.sql.*org.jetbrains.exposed.sql.transactions.transactionがデフォルトインポートされています。

databaseChangeLog {
    changeSet(author = "your_name", id = "20241007-2000-1") {
        // https://jetbrains.github.io/Exposed/table-definition.html#dsl-create-table
        val createdByExposed = object : Table("created_by_exposed") {
            val id = integer("id").autoIncrement()
            val name = varchar("name", 256)
            override val primaryKey = PrimaryKey(id)
        }
        customExposedMigrationChange(
            execute = { db ->
                transaction(db) {
                    SchemaUtils.create(createdByExposed)
                }
            },
            rollback = { db ->
                transaction(db) {
                    SchemaUtils.drop(createdByExposed)
                }
            },
        )
    }
}

他のORM

現在、他のORMだとjOOQやktormの拡張もサポートしております。

dependencies {
    // jOOQ
    implementation("io.github.momosetkn:liquibase-kotlin-custom-jooq-change:4.29.2-0.8.0")
    // Ktorm
    implementation("io.github.momosetkn:liquibase-kotlin-custom-ktorm-change:4.29.2-0.8.0")
}
GitHubで編集を提案

Discussion