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でできることは次の通りです。
- 指定したパス内のマイグレーションファイル(.sql)を収集し実行する。
- 実行したマイグレーションファイルのバージョン情報を永続化し、バージョンに差分があればマイグレーションを実行する
- 実行済みのマイグレーションと、収集したマイグレーションファイルのchecksumを比較して、変化を検出した場合はエラーを送出する
使い心地としては概ね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を作る
次にSpannerKaseDatabaseClient
とMigrationDataScanner
を作ります。
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は割とレアな気がしますが、もし機会があれば触ってみてください。