一覧SE-08-2 · リリースと変更

データベースと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 / シェル

1
python3 db.py init python3 db.py rows python3 db.py read-v1
initialized name schema
['id', 'name']
[(1, 'あおい')]
id=1 name=あおい
2
python3 db.py read-v3
OperationalError: no such column: display_name
3
python3 db.py expand python3 db.py rows python3 db.py read-v1
added nullable display_name
['id', 'name', 'display_name']
[(1, 'あおい', None)]
id=1 name=あおい
4
python3 db.py write-v2 はる python3 db.py rows python3 db.py read-v1 python3 db.py read-v3
v2 wrote both=はる
['id', 'name', 'display_name']
[(1, 'はる', 'はる')]
id=1 name=はる
id=1 name=はる
5
python3 db.py write-v1 みお python3 db.py rows python3 db.py read-v3
v1 wrote みお
['id', 'name', 'display_name']
[(1, 'みお', 'はる')]
id=1 name=はる
6
python3 db.py disable-v1 python3 db.py backfill python3 db.py rows python3 db.py read-v3
v1 writes disabled
display_name filled from name
['id', 'name', 'display_name']
[(1, 'みお', 'みお')]
id=1 name=みお
7
python3 db.py write-v1 旧名
ValueError: old writer disabled
8
python3 db.py api-v3 | python3 client.py
old client: required name field missing
9
python3 db.py api-bridge | python3 client.py
old client: みお
10
python3 db.py contract python3 db.py read-v1
dropped name
OperationalError: no such column: name
11
python3 db.py write-v3 はな python3 db.py read-v3 python3 db.py restore-column python3 db.py read-v1
v3 wrote はな
id=1 name=はな
restored name from current display_name
id=1 name=はな

演習完了