壊さずに変える、3段階のデータ・API変更術
「カラム名を変えただけ」「必須項目を1つ足しただけ」。そんな小さな変更が、古いアプリの画面崩れや、処理中のジョブの失敗を引き起こすことがあります。この記事は設計工程の話です。データやAPIを変えるときに、一度に変えず3段階に分けて設計する方法を説明します。
なぜ一度に変えると壊れるのか
変更の瞬間、世の中には新旧が混在しています。
- ストアの更新が済んでいない、古いバージョンのモバイルアプリ
- 変更前のコードで積まれ、まだ実行されていないジョブ
- ブラウザやCDNに残っている、古い形式のキャッシュ
これらは「デプロイした」だけでは消えません。設計の段階で新旧が混ざる期間を前提にしておく必要があります。
3段階で変える:expand / migrate / contract
1. expand(広げる)
新しい形を追加するだけで、古い形は消しません。カラム名をnameからdisplay_nameに変えるなら、まずdisplay_nameを追加します。書き込みは両方に行い、読み込みは新しい方を優先して、無ければ古い方を使います。
2. migrate(移す)
既存データを新しい形へ埋め直します。件数が多い場合は小分けにし、途中で止まっても再開できる作りにします。終わったら、移行前後の件数と欠損の数を照合します。
3. contract(縮める)
古い形を使うものが残っていないことを確認してから、古いカラムや古い項目を消します。この段階が一番危険なので、判断基準を先に決めておきます。
各段階で確認すること
- expand:旧クライアントが、新しい項目を無視しても動くか
- migrate:移行後の件数が、移行前と一致するか。やり直しても二重に書き込まないか
- contract:旧クライアントの利用割合が十分に下がったか。読み書き両対応の期間は決めてあるか
目安の判断基準は、たとえば次のとおりです。「旧バージョンからのリクエストが、直近2週間で全体の1%未満」「両対応の期間は最低1リリースサイクル」。数字はサービスごとに決めてください。決めておくこと自体が重要です。
活用例:必須項目を足すAPIの変更
報告APIに、必須項目severityを足す場合を考えます。
// NG: 最初から必須にする
// 旧アプリは severity を送らないので、全リクエストが 422 になる
// OK: 3段階に分ける
// 1. expand : severity は任意。未指定なら "normal" として保存する
// 2. migrate: 既存データの severity を "normal" で埋める
// 3. contract: 旧バージョンの割合が下がってから必須にする失敗例も1つ挙げます。カラム名を直接変えたところ、デプロイ前に積まれたジョブが古い名前で読み出し、静かに失敗し続けました。ジョブの待ち行列は、デプロイ前後で新旧が混ざる代表的な場所です。
設計時のチェックリスト
- この変更を、1回のデプロイで完結させていないか
- 新旧が混在する期間を、日数で書いてあるか
- 古いクライアント・ジョブ・キャッシュの3つを洗い出したか
- 消す判断を、数字で決めてあるか
Bugoon での実践
互換性の問題は、変更の直後に「あの画面が崩れた」という報告として現れます。Bugoon のウィジェットをサイトに埋め込んでおくと、報告にスクリーンショットと操作ステップ、ブラウザや画面サイズなどの環境情報が自動で付きます。iOS / Android アプリからの報告でも同様です。旧バージョンの利用者から報告が届いたとき、どの環境で起きたのかを往復なしで確かめられます。報告は GitHub Issue と連携でき、カンバンボードで contract 前の確認事項として管理することもできます。