データベースとAPIの段階的な移行
追加・移行・削除の順序を、読取り側・書込み側・保存済みデータの関係から判断します。
説明
アプリの版を戻せば、DBや外部APIの変更も元へ戻るとは限りません。共有する列や項目を先に削除すると、古いプログラムが読めなくなります。新旧を一時的に併存させ、利用側の切替えを確認してから古い形式を取り除く方法があります。
実SQLiteのusers表でnameをdisplay_nameへ移します。列を追加し、両方へ書く版と古い版が混在した場合の不整合を確かめます。旧書込みを止めて値をそろえた後、APIの互換性と列削除後の復旧を確認します。
追加・移行・削除の順序を、読取り側・書込み側・保存済みデータの関係から判断します。
1旧列だけの開始状態
name列を持つusers表に一人を登録します。
python3 db.py init旧形式の表を作る
python3 db.py rows列名と実際の行を読む
python3 db.py read-v1旧版の読取りを実行する旧版はnameから「あおい」を読めます。idは1で固定し、同じ人物の値が新旧の列でどう変わるかを追います。
2新列を読む版を先に使うと失敗
まだないdisplay_nameを参照する読取りを実行します。
python3 db.py read-v3新列を読む処理を先に実行するSQLiteがno such columnを返します。アプリだけ先に新列へ切り替えても、保存先の構造が対応していなければ動きません。
3旧列を残して新列を追加
display_nameをNULL可能な列として追加します。
python3 db.py expand旧列を保持したまま新列を追加する
python3 db.py rows
python3 db.py read-v1追加後も旧版が読めることを確認する旧列の値は残り、新列はNULLです。旧版の読取りは継続できます。ただし列が存在するだけでは、新版が必要な値をすべて読める状態ではありません。
4新旧の両列へ書く版を使う
v2の書込みは二つの列を同じ値へ更新します。
python3 db.py write-v2 はる同じ更新を新旧の両列へ反映する
python3 db.py rows
python3 db.py read-v1
python3 db.py read-v3両列が「はる」になり、新旧の読取りが一致します。移行中の二重書込みは、一方だけを更新する経路が残っていないかも確認する必要があります。
5古い書込みが残るとずれる
旧版がnameだけを「みお」へ更新する状態を再現します。
python3 db.py write-v1 みお旧列だけへ更新する
python3 db.py rows
python3 db.py read-v3新列に残った古い値を読むnameは「みお」、display_nameは「はる」のままです。新列への読取り切替えを急ぐと古い値を返します。新旧が混在しても常に互換であるとは限りません。
6旧書込みを止めて既存値をそろえる
旧書込みを無効にし、この例で最新値を持つnameから新列へ値をそろえます。
python3 db.py disable-v1旧版の書込みを無効にする
python3 db.py backfill既存データを新列へ反映する
python3 db.py rows
python3 db.py read-v3両列が「みお」になります。ここでは旧列を正本として全行を埋め直しています。移行中の更新を止めずに埋戻す場合は、更新との競合や順序も設計する必要があります。
7禁止した旧書込みを確認
停止した旧経路へ書こうとします。
python3 db.py write-v1 旧名無効にした書込み経路を試すold writer disabledで拒否されます。停止を宣言するだけでなく、この実行環境では旧経路での更新ができない状態にしています。
8新形式だけのAPIは旧クライアントを壊す
display_nameだけを返すAPI出力を、nameを必要とする旧クライアントへ渡します。
python3 db.py api-v3新しい項目名のJSONを出力する | python3 client.py旧クライアントへそのまま渡すrequired name field missingになります。DB内部の移行が終わっても、外部へ返す項目名を同時に削除すると利用側へ影響します。
9移行期間は旧項目も返す
互換用のAPIは、同じ現在値をnameとdisplay_nameの両方で返します。
python3 db.py api-bridge新旧の項目を持つ互換出力を作る | python3 client.py旧クライアントも「みお」を表示できます。利用側が新項目へ切り替わるまで旧項目を保ち、削除の時期を別に決められます。
10旧列を削除すると旧読取りは戻せない
旧列を参照する処理を退役させた段階として、name列を削除します。確認のため旧読取りを試します。
python3 db.py contract旧列を取り除く
python3 db.py read-v1旧版だけに戻した場合の失敗を確認する旧版はno such columnで失敗します。ここまで進んだ後は、アプリの実行ファイルだけを旧版へ戻しても動きません。削除は新旧併存より復旧の制約が大きくなります。
11新版での更新を保って互換列を復元する
新版で「はな」へ更新した後、旧列を追加し直し、現在のdisplay_nameから値を入れます。
python3 db.py write-v3 はな新列へ最新の更新を行う
python3 db.py read-v3
python3 db.py restore-column最新値から旧列を再構成する
python3 db.py read-v1復元後の旧読取りを確認する新しい更新を保ったまま旧読取りも「はな」を返します。この例は同じ意味の列なので値を戻せました。失った情報や不可逆な変換がある変更では、この方法だけで復元できるとは限りません。
まとめ
新しい列を追加して併存させ、書込み経路と既存データをそろえてから読取りを切り替えます。
DB列とAPI項目は別の互換性です。利用側が切り替わるまで旧項目を保つ方法があります。
古い列の削除後はコードだけのロールバックでは戻れません。保存データも含めた復旧方法を設計します。
操作履歴と解説を振り返れます。
Python 3 / シェル
python3 db.py init
python3 db.py rows
python3 db.py read-v1initialized name schema ['id', 'name'] [(1, 'あおい')] id=1 name=あおい
python3 db.py read-v3OperationalError: no such column: display_name
python3 db.py expand
python3 db.py rows
python3 db.py read-v1added nullable display_name ['id', 'name', 'display_name'] [(1, 'あおい', None)] id=1 name=あおい
python3 db.py write-v2 はる
python3 db.py rows
python3 db.py read-v1
python3 db.py read-v3v2 wrote both=はる ['id', 'name', 'display_name'] [(1, 'はる', 'はる')] id=1 name=はる id=1 name=はる
python3 db.py write-v1 みお
python3 db.py rows
python3 db.py read-v3v1 wrote みお ['id', 'name', 'display_name'] [(1, 'みお', 'はる')] id=1 name=はる
python3 db.py disable-v1
python3 db.py backfill
python3 db.py rows
python3 db.py read-v3v1 writes disabled display_name filled from name ['id', 'name', 'display_name'] [(1, 'みお', 'みお')] id=1 name=みお
python3 db.py write-v1 旧名ValueError: old writer disabled
python3 db.py api-v3 | python3 client.pyold client: required name field missing
python3 db.py api-bridge | python3 client.pyold client: みお
python3 db.py contract
python3 db.py read-v1dropped name OperationalError: no such column: name
python3 db.py write-v3 はな
python3 db.py read-v3
python3 db.py restore-column
python3 db.py read-v1v3 wrote はな id=1 name=はな restored name from current display_name id=1 name=はな
演習完了