visible true

技術的なメモを書く

Ktor用のSpannerのスキーマバージョン管理ライブラリ「spanner-kase」を作った話

Ubie Advent Calendar 2019の2日目です。

最近チームで新しいサービスが必要になったのでKtorでやろうか!ってことでKtorでサービスを書き始めています。 データベースはGoogle Cloud Spannerを使おうということになりました。

Java/KotlinのWebアプリケーションにおいて、データベースのマイグレーションライブラリというとFlywayが有名かと思いますが、 残念ながらFlywayはGoogle Cloud Spannerをサポートしていません。

SpannerをサポートするPull Requestは存在するのですが、2018/01/01に作られたもので、Pull Requestを出した方はその後、google-cloud-spanner-jdbcを公式のgoogle-cloud-javaライブラリに追加*1したりしていますが、Flyway側の動きはなさそうです。

Pull Requestをつついたり引き継いだりしようか考えましたが、リリースタイミングをコントロールできない点と、機能的にKtorで使える範囲であれば小さそうということで、自分で作ってしまおうと考えました。

spanner-kase

名前はCloud SpannerのSchemaを入れておく場所ということでspanner-kase(スパナケース)にしました。

https://github.com/ubie-inc/spanner-kase

色々ケアレスミスをしてバージョンはいきなり1.1.3です。

spanner-kaseでできること

spanner-kaseでできることは次の通りです。

使い心地としては概ねFlywayかなと思います。Ktorで使うことだけを想定しているので、Spring Boot等ではうまく動かないかもしれません。

spanner-kaseの使い方

spanner-kaseを使うには次の手順が必要です

  • spanner-kaseをプロジェクトに追加する
  • マイグレーションファイルをresourcesに配置する
  • google-cloud-spannerのクライアントを初期化する
  • SpannerKaseDatabaseClient、MigrationDataScannerを作る
  • SpannerKaseを作ってマイグレーションを実行する

spanner-kaseをプロジェクトに追加する

spanner-kaseは内部でgoogle-cloud-spannerを使っていますが、推移的な依存関係を避けるためにimplementationで宣言しているので、利用時には別途google-cloud-spannerを追加する必要があります。

// build.gradle.kts
implementation("app.ubie.spanner-kase:1.1.3")
implementation("com.google.cloud:google-cloud-spanner:$GOOGLE_CLOUD_SPANNER_VERSION")

マイグレーションファイルをresourcesに配置する

Ktorプロジェクトを作ると最初からresourcesディレクトリがあると思うので、そこにマイグレーションファイルを置いていきます。パスは特に指定はないですが、ここではdb/migrationに配置しています。

.
├── build.gradle.kts
├── src
│   └── ...
├── resources
│   └── db
│       └── migration
│           ├── V1__User.sql
│           ├── V2__Todo.sql
│           └── V3__Permission.sql
...

マイグレーションファイルには命名規則があります。

V[VERSION]__[NAME].sql

VERSIONの範囲はLong*2です。

VERSIONが若い順に順次実行します。すでに実行済みのVERSIONは実行しません。VERSIONは年月日時分秒で書くのがおすすめです。

V20191201142511__User.sql

google-cloud-spannerのクライアントを初期化する

spanner-kaseは内部でgoogle-cloud-spannerを使っているので、まずはSpannerクライアントを作ります。

val options = SpannerOptions.newBuilder().build()
val projectId = options.projectId
val spanner = options.service

// spanner-kaseで使う
val instanceId = InstanceId.of(projectId, YOUR_INSTANCE_ID)
val databaseId = DatabaseId.of(projectId, instanceId.instance, YOUR_DATABSE_ID)
val databaseAdminClient = spanner.databaseAdminClient
val databaseClient = spanner.getDatabaseClient(databaseId)

databaseAdminClientはSpannerのDDL(Data Definition Language)を更新する際に、databaseClientはデータのCRUDを行う際に利用します。

SpannerKaseDatabaseClient、MigrationDataScannerを作る

次にSpannerKaseDatabaseClientMigrationDataScannerを作ります。

SpannerKaseDatabaseClientはspanner-kaseがバージョン管理のために使うテーブルの操作をする他に、マイグレーションファイルのSQLの実行などを行います。

val spannerKaseDatabaseClient = SpannerKaseDatabaseClient(
    instanceId.instance,
    databaseId.database,
    databaseAdminClient,
    databaseClient
)

MigrationDataScannerマイグレーションファイルの収集を受け持ちます。MigrationDataScanner自体はinterfaceなので、任意の実装を利用できます。予めClassLoaderMigrationDataScannerを用意しています。

KtorではApplicationを初期化する際に、environmentのClassLoaderを使うことで、resources内のマイグレーションファイルを利用できます。

@kotlin.jvm.JvmOverloads
fun Application.module() {
    // 省略
    val migrationDataScanner = ClassLoaderMigrationDataScanner(
        environment.classLoader, // io.ktor.application.Application.environment
        "db/migration" // relative path from resources dir
    )
}

SpannerKaseを作ってマイグレーションを実行する

あとはSpannerKaseを初期化して、migratie() を実行するだけです。

val configure = SpannerKase.Configure(
        spannerKaseDatabaseClient,
        migrationDataScanner
    ) 
SpannerKase(configure).migrate()

おわりに

早急に必要になる!と思ってザーッと作ったけど、優先度いくつか入れ替えてまだspanner-kaseを使うサービスはプロダクションでは出ていないので、まだもうちょいアップデートあるかもしれません。Ktor + Spannerは割とレアな気がしますが、もし機会があれば触ってみてください。

*1:FlywayでのサポートPR時に作ったものを公式に提案して取り入れられたっぽい

*2:1 - 9,223,372,036,854,775,807