Expand and Contractパターンとは、互換性を壊す可能性のあるDB変更を一度に行わず、「新しい構造を追加する(Expand)→アプリケーションやデータを新しい構造へ移行する→古い構造を削除する(Contract)」という複数段階に分けてリリースする方法です。旧バージョンと新バージョンのアプリケーションが一時的に混在しても動作できる期間を設けることで、カラム名変更や型変更などによる障害リスクを抑えられます。

1. なぜDB変更を一度に行うと危険なのか
1-1. アプリケーションとDBは同時に切り替わるとは限らない
DB変更を安全に行うには、DBとアプリケーションが完全に同じ瞬間に新バージョンへ切り替わるとは限らないことを前提にする必要があります。
例えば、複数台のアプリケーションサーバーを順番に更新するローリングデプロイでは、一時的に旧バージョンと新バージョンが同時に動作します。また、DBマイグレーションを先に実行してからアプリケーションをデプロイする構成でも、「新しいDB+古いアプリ」という組み合わせが発生する場合があります。
この期間に古いアプリケーションが必要としているカラムを削除すると、そのアプリケーションからのSQLが失敗する可能性があります。そのため、DB変更では「変更後のアプリが動くか」だけでなく、移行途中の組み合わせでも動くかを考える必要があります。
1-2. カラム名変更を一度に行う例
例として、usersテーブルのnameというカラムを、より意味の明確なdisplay_nameへ変更するとします。
単純に考えると、次のような変更を行いたくなります。
ALTER TABLE users
RENAME COLUMN name TO display_name;
このSQL自体が間違っているわけではありません。しかし、旧アプリケーションが次のSQLを実行している状態では問題があります。
SELECT name FROM users WHERE id = ?;
DB側でnameをdisplay_nameへ変更した直後から、旧アプリケーションが参照しているnameは存在しなくなります。
旧アプリ
│
│ SELECT name
▼
DB
name → display_name に変更済み
│
└─ nameが存在しないためエラー
DB変更そのものが成功していても、アプリケーションとの互換性が失われればシステムとしては正常に動きません。
1-3. 「変更できる」と「安全にリリースできる」は別
DBがSQLを受け付けることと、その変更を本番環境へ安全にリリースできることは別問題です。
特に注意したいのは、カラムの削除、名前変更、互換性のない型変更、既存データがあるカラムへの制約追加などです。これらは、現在動作しているアプリケーションが既存スキーマに依存している場合、互換性を壊す可能性があります。
そこで、一度に古い構造を置き換えるのではなく、新旧の構造が一時的に共存できる状態を作るのがExpand and Contractパターンです。
2. Expand and Contractパターンとは
2-1. Expandで新しい構造を追加する
最初のExpandでは、古い構造を残したまま新しい構造を追加します。
nameをdisplay_nameへ移行する例なら、いきなりnameを削除・変更するのではなく、まずdisplay_nameを追加します。
ALTER TABLE users
ADD COLUMN display_name VARCHAR(100);
この時点では次のようになります。
users
id
name ← 旧アプリが使用
display_name ← 新しく追加
nameが残っているため、旧アプリケーションは引き続き動作できます。一方、新しいアプリケーションへ移行するためのdisplay_nameも用意されています。
なお、新しいカラムを最初からNOT NULLにできるかどうかは、既存データやデフォルト値などによって異なります。Expandでは、既存アプリケーションとの互換性を壊さないことが重要です。
2-2. 新旧両方が使える期間を作る
Expand and Contractの重要なポイントは、新旧の構造を一時的に共存させることです。
Expand直後は旧アプリがnameを使用し、新アプリへの切り替え途中では新旧のアプリが混在する可能性があります。そのため、移行期間中は両方のカラムが必要になります。
users
┌──────────────┐
旧アプリ → name │
│ │
新アプリ → display_name │
└──────────────┘
一見すると不要なカラムが増えて非効率に見えます。しかし、この一時的な冗長性によって、DB変更とアプリケーション変更を別々のタイミングでリリースできるようになります。
2-3. Contractで古い構造を削除する
新しい構造への移行が完了したら、最後にContractを行います。
すべてのアプリケーションがdisplay_nameを利用し、nameを参照・更新する処理がなくなったことを確認してから、古いカラムを削除します。
ALTER TABLE users
DROP COLUMN name;
最終的には次の状態になります。
変更前
name
↓ Expand
name
display_name
↓ アプリ・データを移行
name
display_name ← 使用中
↓ Contract
display_name
重要なのは、ExpandとContractを同じタイミングで一気に済ませないことです。新旧アプリケーションが共存できる期間を確保し、移行が完了したことを確認してから古い構造を削除します。
3. 新しいカラムへ安全に切り替える
3-1. まず新しいカラムを追加する
Flywayを利用している場合、Expandの変更を新しいVersioned Migrationとして追加できます。
例えば、現在V4まで適用済みなら、次のようなV5を作成します。
-- V5__add_display_name.sql
ALTER TABLE users
ADD COLUMN display_name VARCHAR(100);
このマイグレーションでは、既存のnameには触れていません。
FlywayのVersioned Migrationとして管理することで、開発・検証・本番といった各環境へ同じ順序で変更を適用できます。
3-2. アプリケーションを新しい構造へ対応させる
次に、アプリケーションを新しいカラムへ対応させます。
移行方法の一例が、新旧両方へデータを書き込むdual-writeです。
概念的には次のような処理になります。
user.setName(inputName);
user.setDisplayName(inputName);
ここでは、同じ値をnameとdisplay_nameの両方へ保存しています。
ただし、dual-writeには二つの更新結果の整合性をどう保つかという問題があります。そのため、「Expand and Contractでは必ずdual-writeしなければならない」という意味ではありません。DBトリガー、別の移行処理、アプリケーション側の互換ロジックなど、システムに応じて方法を選択します。
重要なのは、移行期間中に旧構造と新構造のどちらを利用するコードが存在しても破綻しない状態を設計することです。
3-3. 既存データを新しいカラムへ移す
新しいカラムを追加しても、既存レコードのdisplay_nameには値が入っていません。そのため、過去のデータも新しい構造へ移す必要があります。
小規模な例なら、次のようなSQLが考えられます。
UPDATE users
SET display_name = name
WHERE display_name IS NULL;
何をしているのかというと、既存のnameを、新しく追加したdisplay_nameへコピーしています。
これが必要なのは、新アプリケーションがdisplay_nameを参照するようになったとき、移行前から存在するユーザーについても値を取得できるようにするためです。
ただし、数百万件・数千万件といった大量データを一括UPDATEすると、長時間のトランザクション、ロック、DB負荷などが問題になる場合があります。大規模なデータ移行では、一括更新以外の方法も検討する必要があります。
4. 読み取り先を新しいカラムへ切り替える
4-1. データが揃ってから読み取り先を変更する
新しいカラムを追加しただけで、すぐに読み取り先まで切り替える必要はありません。
まず、新規データがdisplay_nameへ正しく保存されていること、既存データの移行が完了していることを確認します。その後、新しいバージョンのアプリケーションでdisplay_nameを読み取るように変更します。
Expand直後
WRITE → name / display_name
READ → name
↓
移行確認後
WRITE → name / display_name
READ → display_name
このように書き込みと読み取りの切り替えを分けることで、問題が発生したときにどの段階で不具合が起きたのかも切り分けやすくなります。
4-2. 新旧アプリの混在を考える
ローリングデプロイでは、すべてのアプリケーションインスタンスが同時に新バージョンへ切り替わるとは限りません。
App v1 ──→ name
\
users
/
App v2 ──→ display_name
この状態でnameを削除すると、まだ動いているApp v1が失敗します。
そのためContractへ進む条件は、「新しいアプリをデプロイした」ではなく、古い構造を利用するアプリケーションがもう存在しないことを確認した、と考える方が安全です。
4-3. 切り替え後もすぐには削除しない
読み取り先をdisplay_nameへ変更できても、直後にnameを削除する必要はありません。
新しいアプリケーションが正常に動作していることや、古いアプリケーションへのロールバックが必要ないことなどを確認する期間を設けることで、Contractによる破壊的変更のリスクを抑えられます。
どれだけ待てばよいかに一律の正解はありません。デプロイ方式、ロールバック方針、監視方法、利用規模などに応じて、古い構造を安全に削除できる条件を事前に決めておくことが重要です。
5. Contractで古いカラムを削除する
5-1. 古いカラムが使われていないことを確認する
Contractを実行する前には、古いnameカラムが本当に不要になったかを確認します。
確認対象はアプリケーションコードだけとは限りません。バッチ処理、管理ツール、集計処理、外部連携、手動運用のSQLなどが古いカラムを参照している可能性もあります。
少なくとも次のような状態になっていることを確認します。
- 新しいカラムへのデータ移行が完了している
- 新アプリケーションが新しいカラムを利用している
- 旧バージョンのアプリケーションが稼働していない
- バッチや外部連携なども古いカラムへ依存していない
- 問題発生時の対応方法が決まっている
「新しいカラムが使える」だけではなく、「古いカラムを消しても困るものがない」ことがContractの条件です。
5-2. 削除も新しいマイグレーションとして追加する
古いカラムの削除も、過去のマイグレーションを書き換えるのではなく、新しいVersioned Migrationとして追加します。
例えばContractをV7で行うなら、次のようになります。
-- V7__drop_name.sql
ALTER TABLE users
DROP COLUMN name;
何をしているのかというと、移行期間中に残していた旧nameカラムを削除しています。
別のマイグレーションにする理由は、ExpandとContractを別々の変更履歴として管理することで、「新しい構造を追加した時点」と「古い構造を削除した時点」を明確にできるためです。
5-3. Contractは後戻りしにくい変更になる
カラム追加とカラム削除には大きな違いがあります。
ADD COLUMN
→ 既存構造を残したまま追加できる
DROP COLUMN
→ 古い構造やデータを失う可能性がある
そのため、Expandは比較的互換性を維持しやすい一方、Contractは慎重な判断が必要です。
特にカラム削除後に旧アプリへロールバックすると、そのアプリが削除済みカラムを要求して動作しない可能性があります。アプリケーションのロールバックだけを考えるのではなく、DBスキーマとの互換性まで含めて判断する必要があります。
6. Expand and Contractを使うときの注意点
6-1. すべてのDB変更に使う必要はない
Expand and Contractは、安全なDB変更に役立つパターンですが、すべての変更を複数リリースへ分割する必要はありません。
例えば、既存アプリケーションが参照しない新しいテーブルを追加するだけなら、旧アプリとの互換性を壊さないことがあります。一方、既存カラムの削除や名前変更などは、現在動いているコードへ直接影響します。
判断基準は、そのDB変更によって旧バージョンのアプリケーションが動かなくなる可能性があるかです。
| DB変更 | 互換性への影響 | 段階的変更を検討 |
|---|---|---|
| 未使用の新規カラム追加 | 比較的小さい | 状況による |
| 新規テーブル追加 | 比較的小さい | 状況による |
| 使用中カラムの名前変更 | 大きい | 推奨 |
| 使用中カラムの削除 | 大きい | 推奨 |
| カラム型の互換性のない変更 | 大きい | 推奨 |
既存データへのNOT NULL追加 | 条件によって大きい | 推奨 |
6-2. ExpandとContractの間は一時的に複雑になる
Expand and Contractでは、新旧両方の構造を保持するため、一時的にシステムが複雑になります。
カラムが2つ存在したり、両方へ書き込んだりする期間が発生するため、「最終的なDB構造だけ」を見れば遠回りに感じるかもしれません。
しかし、その複雑さは永続的なものではありません。
変更前
シンプル
↓
Expand・移行期間
一時的に複雑
↓
Contract
再びシンプル
一時的な複雑さを受け入れる代わりに、アプリケーションとDBを段階的に切り替えられることが、このパターンの利点です。
6-3. 各段階を独立して安全にリリースできるようにする
Expand and Contractでは、各段階が独立してリリース可能であることが重要です。
例えば、「V5で新カラム追加、直後にV6でデータ移行、同じデプロイのV7で旧カラム削除」としてしまうと、新旧構造を共存させる意味が薄くなります。
重要なのは、SQLファイルを何個に分けるかではありません。新しい構造を追加し、アプリケーションとデータを移行し、旧構造への依存がなくなったことを確認してから削除する、というリリース境界を作ることです。
各段階で問題がないことを確認してから次へ進めることで、移行途中の障害を発見しやすくなり、破壊的変更を行うタイミングも管理しやすくなります。
7. まとめ
7-1. Expand → 移行 → Contractの順番を理解する
Expand and Contractパターンは、互換性を壊すDB変更を複数段階へ分割する方法です。
基本的な流れは次のようになります。
変更前
旧カラムのみ
│
▼
Expand
新カラムを追加
旧+新を共存
│
▼
移行
新しいカラムへ書き込み
既存データを移行
読み取り先を切り替え
│
▼
確認
旧カラムへの依存がないことを確認
│
▼
Contract
旧カラムを削除
この方法なら、DB変更とアプリケーション変更を完全に同時実行する必要がなくなり、新旧バージョンが一時的に混在する環境でも互換性を維持しやすくなります。
7-2. 安全に段階移行するためのチェックリスト
Expand and Contractで重要なのは、単にSQLを複数ファイルへ分割することではありません。旧アプリと新アプリの両方が動ける期間を作り、安全性を確認してから破壊的変更を行うことが本質です。
実際にDB変更を設計するときは、次を確認します。
- 変更によって旧アプリケーションとの互換性が壊れないか確認した
- いきなり既存カラムを削除・名前変更していない
- Expandでは既存構造を残したまま新しい構造を追加した
- 移行期間中に新旧アプリが混在する可能性を考慮した
- 新旧データの整合性を保つ方法を決めた
- 既存データを新しい構造へ移行する方法を決めた
- 新しい構造への読み取り切り替えを確認した
- バッチや外部連携なども古い構造へ依存していないか確認した
- Contract前に旧アプリケーションが残っていないことを確認した
- 古い構造の削除を新しいVersioned Migrationとして管理した
- Contract後に旧アプリへロールバックできるか確認した
- ExpandとContractを安易に同じリリースへまとめていない
DB変更では、最終的なスキーマだけでなく、そこへ到達する途中の状態も正常に動作することが重要です。Flywayで変更履歴を管理しながらExpand and Contractを組み合わせることで、既存アプリケーションとの互換性を保ちつつ、破壊的なスキーマ変更を段階的に進められます。