Kotlin の Exposed と Flyway で実現するシームレスなデータベースマイグレーション：Prisma ライクな開発体験

## 概要

この記事では、Kotlin で Exposed と Flyway を使用して、Prisma に似たデータベーススキーマ管理とマイグレーションの開発体験を作る方法を紹介します。

## 必要なもの

- **Exposed**: Kotlin 用の JetBrains 製 ORM フレームワーク（`org.jetbrains.exposed:exposed-core`、`org.jetbrains.exposed:exposed-migration`などの依存関係）
- **Flyway**: データベースバージョン管理用のマイグレーションツール（`org.flywaydb:flyway-core`などの依存関係）
- **データベースドライバ**: 選択したデータベース用の JDBC ドライバ

## 1. テーブルの定義

Exposed を使うと、データベーススキーマを Kotlin オブジェクトとして定義でき、コンパイル時の安全性と IDE 支援が得られます。以下は顧客テーブルの定義例です：

```kotlin
// ...
import org.jetbrains.exposed.dao.id.UUIDTable
// ...

object CustomerTable : Table("customers") {
    val id = integer("id").autoIncrement()
    val name = text("name")
    val email = text("email")

    override val primaryKey = PrimaryKey(id)
}
```

詳しくは[Exposed のドキュメント](https://www.jetbrains.com/help/exposed/home.html)をご覧ください。

## 2. 必要なマイグレーションの確認

マイグレーションを適用する前に、データベースをテーブル定義と同期するために必要な SQL ステートメントを確認すると便利です。これは特に開発中にスキーマ変更の影響を確認するのに役立ちます：

```kotlin
// ...
import MigrationUtils
import org.jetbrains.exposed.sql.Database
import org.jetbrains.exposed.sql.StdOutSqlLogger
import org.jetbrains.exposed.sql.addLogger
import org.jetbrains.exposed.sql.transactions.transaction
import org.slf4j.LoggerFactory
// ...


fun checkForRequiredMigrations() {
    Database.connect(
        ...
    )

    transaction {
        addLogger(StdOutSqlLogger)

        val statements = MigrationUtils.statementsRequiredForDatabaseMigration(
            tables = arrayOf(CustomerTable, ...) // ここに全てのテーブルを追加
        )

        if (statements.isNotEmpty()) {
            val combinedStatements = statements.joinToString("\n")

            println("\n=== データベースマイグレーションが必要です！ ===\n$combinedStatements\n=== マイグレーション終了 ===")
        }
    }
}
```

この関数は以下を行います：

1. Exposed のテーブル定義と実際のデータベーススキーマを比較
2. データベースを同期するために必要な SQL ステートメントを生成
3. レビュー用に出力

例えば、`CustomerTable`を追加した後にこのチェックを実行すると、次のような出力が表示されます：

```
=== データベースマイグレーションが必要です！ ===
CREATE TABLE IF NOT EXISTS customers (id SERIAL PRIMARY KEY, "name" TEXT NOT NULL, email TEXT NOT NULL)
CREATE SEQUENCE IF NOT EXISTS customers_id_seq START WITH 1 MINVALUE 1 MAXVALUE 9223372036854775807
=== マイグレーション終了 ===
```

この出力が表示されたら、`src/main/resources/db/migration`に有効な Flyway マイグレーションファイル名で新しいファイルを作成してください。

```sql:V1__add_customer_table.sql
CREATE TABLE IF NOT EXISTS customers (id SERIAL PRIMARY KEY, "name" TEXT NOT NULL, email TEXT NOT NULL)
CREATE SEQUENCE IF NOT EXISTS customers_id_seq START WITH 1 MINVALUE 1 MAXVALUE 9223372036854775807
```

詳しくは[Flyway のドキュメント](https://documentation.red-gate.com/flyway)をご覧ください。

## 3. Flyway によるマイグレーションの適用

Exposed はマイグレーション用の SQL を生成できますが、Flyway はそれらのマイグレーションをバージョン管理し適用する堅牢な方法を提供します：

```kotlin
// ...
import org.flywaydb.core.Flyway
// ...

fun applyMigrations() {
    Flyway.configure().dataSource(
        ...
    ).load().migrate()
}
```

この関数は以下を行います：

1. デフォルトの場所（`db/migration`）からマイグレーションスクリプトを見つける
2. 保留中のマイグレーションを順番に適用する
3. Flyway のメタデータテーブルでマイグレーション履歴を追跡する

こちらも[Flyway のドキュメント](https://documentation.red-gate.com/flyway)で詳細を確認してください。

## おまけ：マイグレーションファイルの自動生成

マイグレーションファイルを自動生成することもできます：

```kotlin
MigrationUtils.generateMigrationScript(
    tables = arrayOf(CustomerTable, OrderTable, ProductTable),
    "./src/main/resources/db/migration",
    "V1__add_customer_table"
)
```

これにより Flyway のマイグレーションディレクトリに SQL ファイルが作成されます。

## まとめ

ExposedとFlywayを組み合わせることで、型安全なスキーマ定義とシンプルで確実なマイグレーション管理が可能になります。これにより、Prismaのような直感的な開発体験をKotlinでも実現でき、データベーススキーマの変更を簡潔で効率的に進めることができます。


ほなまた！