私のチームでは、Spring Boot と Flyway を使ってチーム開発の業務にあたっています。
その中でマイグレーション管理がうまくかみ合わず、ビルドエラーで頭を抱えることが日常茶飯事です、、、
対症療法でしのいできましたが、仕組みがわからないままで同じ失敗を繰り返していました。
いちばん多いつまずきは、SQLは正しいのに、マイグレーションで失敗してビルドが落ちるというものでした。
文法エラーでも接続エラーでもないのに、なぜ落ちるのでしょうか?
鍵は、「Flywayが何を見て動いているか」にあります。
Flywayは、DBの実際の状態(どんなテーブルや列があるか)を見て動いているわけではないのです。
この記事では、マイグレーションとFlywayの基本から始め、Flywayが実際に見ている flyway_schema_history という1枚の表を軸に、チェックサムとバージョン衝突のつまずきを、原因から直し方までたどります。
本記事のリポジトリ名、テーブル名、マイグレーション、パスやチェックサム値などの固有名詞は、すべてフィクションの例示です。
実在の顧客・契約・個人情報・社内情報とは一切関係ありません。
まずマイグレーションとは
データベースの構造、つまり「どんなテーブルがあり、各テーブルがどんな列を持つか」の定義をスキーマと呼びます。
たとえば「users テーブルに id、name、email がある」という設計そのものがスキーマです。
開発では、コードの変更に合わせてスキーマも変えていく必要があります。
「注文にキャンセル日時を持たせたい」なら、orders テーブルに canceled_at 列を足す、といった変更です。
厄介なのは、変更を反映する先が1つではないことです。
ローカルDB、検証環境、本番環境、、、
すべてに同じ変更を、同じ順序で適用しなければなりません。
手作業の ALTER TABLE でこれをやると、記録が残りません。
どの環境に何を入れたかがわからなくなり、本番への反映漏れやコードとの食い違いが起きます。
マイグレーションは、スキーマの変更を1つずつSQLファイルに切り出して管理する仕組みです。
ルールはシンプルです。
- 変更1つにファイル1つ。順序を表すバージョンを付ける
- 各ファイルは、どのDBにも一度だけ、バージョン順に適用する
- どのDBがどこまで適用したかを記録する
たとえば次の3ファイルは、users を作り、email を足し、orders を作る、という3つの変更です。
V1__create_users.sql
V2__add_email_to_users.sql
V3__create_orders.sql
まっさらなDBは、3つを順に適用すれば全員と同じ形になります。
V2 まで適用済みのDBは、足りない V3 だけを適用すれば済みます。
この「変更を積み上げて記録する」考え方は、Flyway固有のものではありません。
RailsやLaravelなどのフレームワークにも、同じ発想のマイグレーション機能があります。
Flywayは、変更をSQLファイルとして素直に書ける独立したツールです。
Flywayの基本と用語
Flywayはマイグレーションを実行するオープンソースのツールです。
Java製で、アプリに組み込むライブラリとしても、単体のコマンドとしても使えます。
(Spring Bootではライブラリとして組み込みます)
また無償のCommunityエディションと、機能を足した有償エディションがあります。
Flywayでは、マイグレーションファイルの名前そのものが情報を持ちます。
V1__create_users.sql
└┬┘ └──────┬─────┘
│ └ 説明(内容がわかる名前)
└ 種類(V/R/U)とバージョン番号
※区切りはアンダースコア2つです。
1つだとファイルとして認識されないので注意です。
先頭の文字(プレフィックス)で、マイグレーションの種類が決まります。
- V(versioned):バージョン順に一度だけ適用する。テーブル定義など通常の変更に使う
- R(repeatable):バージョンを持たず、中身が変わるたびに再適用する。ビューやストアドプロシージャ(SQLで定義する仮想の表や処理)向け
- U(undo):適用済みの変更を取り消す(ロールバック)。有償のTeamsエディションの機能
操作は主に下記コマンドに分かれます。
| コマンド | 役割 |
|---|---|
migrate |
未適用のマイグレーションをバージョン順に適用する |
info |
適用済みと未適用の状態を一覧表示する |
validate |
ファイルと適用記録の整合を検証する |
repair |
適用記録のテーブルを修復する |
baseline |
既存DBに途中から導入する起点を設定する |
clean |
対象スキーマのオブジェクトをすべて削除する |
日常的に使うのは migrate と info です。
残りの validate、repair、baseline、clean は、記録が壊れたときや既存DBへ導入するときなど、特定の場面で使うコマンドです。
例えばコマンドラインで info を実行すると、各マイグレーションの状態が一覧で出ます。
$ flyway info
+---------+--------------------+------+---------+
| Version | Description | Type | State |
+---------+--------------------+------+---------+
| 1 | create users | SQL | Success |
| 2 | add email to users | SQL | Success |
| 3 | create orders | SQL | Pending |
+---------+--------------------+------+---------+
Success は適用済み、Pending は未適用です。
この状態で migrate を実行すると、未適用の V3 だけが適用されます。
$ flyway migrate
Migrating schema "public" to version "3 - create orders"
Successfully applied 1 migration to schema "public" (execution time 00:00.03s)
validate、repair、baseline、cleanはこの後の「壊れたときの直し方」の章で
具体例を示します。
Spring Bootで使うとき
Spring Bootでは、これらのコマンドを手で打つ場面はほとんどありません。
依存にFlywayを追加すると、アプリの起動時に migrate が自動で実行されるためです。
設定は application.properties(または application.yml)に spring.flyway.* の形で書きます。
spring.flyway.enabled=true
spring.flyway.out-of-order=false
spring.flyway.baseline-on-migrate=false
主な設定は次のとおりです。
| 設定 | 何を決めるか | 既定値 |
|---|---|---|
spring.flyway.enabled |
Flywayを有効にするか(依存を入れると既定でオン) | true |
spring.flyway.out-of-order |
番号の追い越し(あとから来た小さい番号)を許すか | false |
spring.flyway.baseline-on-migrate |
既存データのあるDBへ初めて適用するとき、現状を起点として登録するか | false |
spring.flyway.locations |
マイグレーションSQLの置き場所 | classpath:db/migration |
上の値はいずれも既定値なので、変えたいものだけ書けば十分です。
なお、記事の後半で出てくる outOfOrder は、この spring.flyway.out-of-order のことです。
置き場所(spring.flyway.locations)は、複数をカンマ区切りで指定できます。
先頭のプレフィックスで探し方が変わります。
-
classpath:… ビルドに含まれるリソースの中を探す。既定のsrc/main/resources/db/migrationはこれ -
filesystem:… ファイルシステム上のパスを探す。ビルドに含めない別リポジトリやサブモジュールのSQLを、直接指したいときに使う
spring.flyway.locations=classpath:db/migration,filesystem:../db-repo/src/main/resources/db/migration
上の例では、アプリに同梱したSQLと、サブモジュール側のSQLの両方を読み込みます。
失敗の見え方にも注意が要ります。
migrate が起動時に走るので、マイグレーションが失敗すると、アプリ自体が起動しません。
CLIならコマンドが落ちるところが、Spring Bootでは起動ログのエラーとして現れます。
なお repair と clean は、起動時には実行されません。
必要なときは、Maven/Gradleプラグインか単体のCLIから実行します。
Flywayの心臓部 flyway_schema_history
Flywayは、適用したマイグレーションを1行ずつ記録するテーブルを、対象DBに自動で作ります。
名前は flyway_schema_history です。
(Flyway 5より前は schema_version でした)
Flywayの挙動は、このテーブルの読み書きに尽きます。
migrate のとき、Flywayが見るのは2つだけです。
手元のマイグレーションファイルの一覧と、flyway_schema_history に記録された適用済みの行。
この2つを突き合わせ、ファイルにあってテーブルにない分(=未適用)を、順に適用して行を足します。
手元のファイル flyway_schema_history(DB内の表)
-------------------- ------------------------------
V1__create_users.sql ─┐ version | checksum | success
V2__add_email...sql ─┼──▶ V1 | 89af.. | true
V3__create_orders.sql ─┘ V2 | 7c12.. | true
→ ファイルにあって表に無い V3(=未適用)を適用し、行を追加する
見ているのは適用記録のテーブルであって、DBの実際のスキーマではありません。
だから、手でこっそりテーブルを作ってもFlywayは気づきません。
逆に、記録さえ合っていればスキーマの中身は問いません。
テーブルは、主に下記の列で構成されています。
| 列 | 意味 |
|---|---|
version |
適用したバージョン |
script |
適用したファイル名 |
checksum |
適用時のファイル内容から計算した値 |
installed_on |
適用した日時 |
success |
適用が成功したか |
チェックサムもバージョンの順序も、この表に書かれています。
チーム開発のトラブルは、だいたい「ファイルの側」と「表の側」がずれて起きます。
チェックサム:適用済みを編集すると落ちる理由
チェックサム(checksum)は、ファイルの中身から計算した短い値です。
中身が1文字でも変わると、まったく別の値になります。
ファイルの指紋、と考えるとイメージしやすいです。
Flywayはこの指紋で、適用済みのファイルが後から書き換えられていないかを確かめます。
migrate は適用のたびに、適用済みファイルのチェックサムを計算し直し、テーブルの checksum 列と照合します(既定で有効。設定 validateOnMigrate)。
一致しなければ、先へ進む前にエラーで止まります。
これがチーム開発で厄介な挙動を生みます。
適用済みのファイルを後から編集すると、そのチェックサムが変わります。
一方、自分やCI(push のたびに自動でビルドやテストを走らせる仕組み)のDBには、
編集前の値が記録されたままです。
次の migrate で記録値と計算値が食い違い、ビルドが落ちます。
やっかいなのは、SQLの意味を変えない修正でも起きることです。
タイポ修正、コメント追記、インデント調整。
どれも中身を変えるので、チェックサムは変わります。
エラーは、おおむね次の形で出ます。
Migration checksum mismatch for migration version 2
-> Applied to database : 2145208862
-> Resolved locally : -1101818402
Either revert the changes to the migration, or run repair to update the schema history.
一度適用したマイグレーションは、確定した歴史です。
後から書き換えるのは、その歴史の改ざんにあたります。
Flywayは差異を検知して、意図しない変更を未然に止めています。
だから原則はシンプルです。
適用済みのマイグレーションファイルは編集しない。
仕様を変えるときは、古いファイルを直さず、新しいバージョンを足して差分で表します。
バージョン衝突とチーム開発
チェックサムが「中身のずれ」なら、バージョンは「順序のずれ」を生みます。
Flywayはバージョンの昇順で、一度だけ適用します。
この前提が、複数人が並行して作業すると崩れます。
崩れ方は2つあります。
【衝突】番号がかぶる 【追い越し】あとから小さい番号
feature-A: V5 (coupon) 自分のDB: V5 まで適用済み
feature-B: V5 (review) あとから V4 がマージされて登場
↓ 両方マージ ↓
V5 が2つ → どちらが先か決められない V5 より前の V4 が未適用で残る
衝突は、番号のかぶりです。
2人が別々のブランチで、次の番号として V5__... を作ったとします。
ファイル名が違えばGitはコンフリクトを検出しないので、両方がそのままマージされます。
結果、バージョン5が2つになり、Flywayはどちらを先に適用すべきか決められず止まります。
Found more than one migration with version 5
Offenders:
-> db/migration/V5__add_coupon_table.sql (SQL)
-> db/migration/V5__add_review_table.sql (SQL)
追い越しは、適用順の逆転です。
自分のDBは V5 まで適用済みだとします。
そこへ、同僚が先に作っていた V4__... がマージされてきます。
V4 は適用済みの V5 より前なので、あとから割り込む形になります。
Flywayは既定で、この「適用済みより前の番号が未適用で残る」状態を異常とみなし、validate や migrate がエラーになります。
これは outOfOrder(追い越しを許すかの設定)が、既定で無効なためです。
無効のままだと、あとから来た小さい番号は適用できません。
対策は2つあります。
① 番号を取る前に、チームで調整する
「次は V6 を使う」とチャットなどで共有してから作れば、番号のかぶりは防げます。
小規模なチームでは十分実用的で、番号もきれいな連番に保てます。
ただし人の運用に頼るので、規模が大きくなったり非同期の作業が増えると、共有漏れから衝突しやすくなるため注意です。
② 番号の付け方で、かぶりにくくする
日時をそのままバージョンにする、タイムスタンプ方式が広く使われています。
V20260720103000__create_users.sql
V20260720114500__add_email_to_users.sql
秒まで含めれば、2人が同じ番号を取る確率はほぼなくなります。
追い越しを許すため、outOfOrder を有効にして運用するチームも多くあります。
ただし outOfOrder を有効にすると、適用順が環境によって変わりえます。
前の変更を前提にした変更では、割り込みで依存が壊れないかの確認が要ります。
壊れたときの直し方
壊れたときは、手を動かす前に状態を確認します。
ここでは、チェックサム不一致を例に、確認から修復までを実際のコマンドで追います。
(コマンドはCLIの表記です。Spring BootのプロジェクトではMaven/Gradleプラグイン経由で実行します)
状況はこうです。
適用済みの V3__add_orders_index.sql を、同僚がインデックス名のタイポ修正のために編集してマージしました。
あなたが git pull してアプリを起動すると、V3 のチェックサムが合わず起動が止まります。
1. info で状態を見る
まず、どこまで適用されているかを確認します。
V3 は Success(適用済み)のままなので、適用そのものは終わっているとわかります。
$ flyway info
+---------+--------------------+------+---------+
| Version | Description | Type | State |
+---------+--------------------+------+---------+
| 1 | create users | SQL | Success |
| 2 | add email to users | SQL | Success |
| 3 | add orders index | SQL | Success |
+---------+--------------------+------+---------+
2. validate で原因を特定する
validate を実行すると、V3 のチェックサム不一致だと具体的にわかります。
$ flyway validate
ERROR: Validate failed: Migrations have failed validation
Migration checksum mismatch for migration version 3
-> Applied to database : 1699834721
-> Resolved locally : -905477136
Either revert the changes to the migration, or run repair to update the schema history.
3. どちらかで直す
エラーメッセージのとおり、直し方は2つです。
-
ファイルを元に戻す …
V3を編集前に戻せば、チェックサムが一致します。まずはこれが基本です。 -
repairで記録を合わせ直す … 修正を残したいときは、記録側のチェックサムを現在のファイルに合わせます。
$ flyway repair
Successfully repaired schema history table "public"."flyway_schema_history" (execution time 00:00.021s)
repair は、失敗して途中で止まった記録を消したり、適用済みの行のチェックサムを現在のファイルに合わせ直したりします。今回は後者です。
このあと migrate(Spring Bootなら再起動)すれば、validate が通って起動します。
ただし repair は記録テーブルを書き換える操作です。
共有DBや本番の場合は、チームの合意を取ってから使います。
一人が勝手に直すと、他の環境との整合が別の形で崩しまいます。
逆に、共有DBや本番の場合、clean は使いません。
clean は対象スキーマのオブジェクトをすべて削除し、まっさらに戻します。
ローカルのやり直しには手軽ですが、共有DBや本番で実行すると全データを失います。
その危険性から、Flyway 10以降は既定で無効化されています(設定 cleanDisabled)。
なお、既存のDBに後からFlywayを導入するときは、baseline で起点のバージョンを
設定します。
baseline より前は適用済みとして扱われ、それ以降だけが管理の対象になります。
まとめ
「SQLは正しいのに、なぜビルドが落ちるのか」。
Flywayが見ているのは、SQLやDBの実際の状態ではなく、flyway_schema_history という記録だからです。
トラブルのほとんどは、この記録と手元のファイルがずれることで起きます。
今回のポイントをまとめます。
- 適用済みのマイグレーションファイルは編集しない
(仕様変更は新しいバージョンで足す) - バージョンはタイムスタンプで付け、連番の取り合いを避ける
- 並行開発では
outOfOrderの方針をチームで決めておく - CIでは
migrateの前にvalidateを走らせ、ずれを早期に見つける -
repairは記録を書き換えるので、共有環境では合意のうえで使う -
cleanを共有DBと本番で実行しない
特に最初の「適用済みを編集しない」だけで、チェックサム由来のビルド停止は
ほぼ消えます。
この観点を理解すると、長いエラーログも「ファイルと記録、どちらがずれたのか」を探すだけの作業に変わります。
そうなれば、原因不明のビルドエラーに振り回される日々とは、そろそろ縁を切れそうですよね?
参考文献
Flyway(Redgate 公式ドキュメント)
- Migrations … マイグレーションと schema history の概念
- Versioned migrations … 番号順に一度だけ適用し、checksum で不変性を検証
-
Out Of Order setting …
outOfOrderの挙動 - Repair … 記録テーブルの修復
- Clean / Clean Disabled setting … 全削除と、既定で無効な理由
チェックサムの実装(ソース)
- ChecksumCalculator.java(flyway/flyway) … 行単位・改行コード除外・BOM除去でCRC32を計算
Spring Boot
-
Database Initialization(Spring Boot 公式) … 起動時のFlyway自動実行と
spring.flyway.*