再生成に耐えるスキーマ進化と予測可能なマイグレーション
バックエンドを再生成しても本番データを安全に保つためのスキーマ進化の実践。スキーマ変更とマイグレーションを計画する実用的な手法を学びます。

再生成されるバックエンドでスキーマ変更が怖く感じる理由
視覚的なモデルからバックエンドを再生成する場合、データベースの変更はセーターの糸を引っ張るように感じられます。Data Designer でフィールドを更新して再生成をクリックすると、単にテーブルが変わるだけでなく、生成される API、バリデーションルール、アプリがデータの読み書きに使うクエリまで変わることがあります。
一般的に問題になるのは新しいコードがビルドできないことではありません。多くのノーコードプラットフォーム(AppMaster のように本物の Go コードを生成するものを含む)は、きれいなプロジェクトを毎回生成できます。本当のリスクは本番に既にデータが存在していて、そのデータが自動的にあなたの新しい設計に合わせて変形してくれない点です。
最初に目にする二つの失敗はシンプルです。
- 読み取りの破壊:カラムが移動したり型が変わったり、クエリが存在しない値を期待するためにアプリがレコードを読み込めなくなる。
- 書き込みの破壊:制約や必須フィールド、フォーマットが変わり、既存クライアントが古い形で送信し続けるために新規や更新のレコードが失敗する。
どちらもユーザーが実際に触れるまで隠れることがあり、痛手になります。ステージングのデータベースは空か新しくシードされた状態かもしれませんが、本番にはヌルや古い enum 文字列、ルール導入前に作られた行などのエッジケースがあります。
だからこそ、再生成に耐えるスキーマ進化が重要です。目標は、バックエンドのコードが完全に再生成されても各変更が安全であり、古いレコードが有効であり続け、新しいレコードも作成できるようにすることです。
「予測可能なマイグレーション」とは、デプロイ前に次の四つの質問に答えられることを意味します:データベースで何が変わるか、既存の行に何が起こるか、ロールアウト中にどのバージョンのアプリが動けるか、そして予期せぬことが起きたときにどうロールバックするか。
シンプルなモデル:スキーマ、マイグレーション、再生成されたコード
プラットフォームがバックエンドを再生成できる場合、頭の中で三つのものを分けて考えると助けになります:データベーススキーマ、スキーマを変更するマイグレーション手順、そして本番に既にある実データ。これらを混同すると変更が予測できなくなります。
再生成を「最新モデルからアプリケーションコードを再構築すること」と考えてください。AppMaster のようなツールでは、この再構築は通常の作業中に何度も起こります:フィールドを調整し、ビジネスロジックを変更し、エンドポイントを追加して再生成し、テストし、繰り返す。再生成は頻繁です。あなたの本番データベースはそうであってはいけません。
シンプルなモデルは次の通りです。
- スキーマ:テーブル、カラム、インデックス、制約の構造。データベースが期待するものです。
- マイグレーション:スキーマをあるバージョンから次へ移行するための順序付けられた再実行可能な手順(時にデータの移動も含む)。各環境で実行するものです。
- ランタイムデータ:ユーザーやプロセスによって作られた実際のレコード。変更の前後と途中で有効であり続ける必要があります。
再生成されたコードは「現在のスキーマに話す現在のアプリ」として扱うべきです。マイグレーションは、スキーマとランタイムデータをコードの変更に合わせて整合させ続ける橋渡しです。
再生成が状況を変える理由
頻繁に再生成すると、多くの小さなスキーマ編集を自然に行うことになります。それ自体は普通のことです。リスクが出るのは、それらの編集が後方互換でないデータベース変更を意味するとき、あるいはマイグレーションが決定論的でないときです。
実務的な管理法は、再生成に耐えるスキーマ進化を小さく取り消し可能なステップの系列として計画することです。一度に大きく切り替えるのではなく、短期間だけ古いコードと新しいコードの両方が動けるように制御された移行を行います。
たとえば、ライブ API が使っているカラム名を変更したい場合、すぐに名前を変えないでください。まず新しいカラムを追加し、両方に書き込み、既存行をバックフィルし、読み取りを新しいカラムに切り替えてから古いカラムを削除します。各ステップはテストしやすく、何か問題が起きてもデータを壊さずに一時停止できます。
この思考モデルにより、再生成が日常的に起きていてもマイグレーションは予測可能になります。
スキーマ変更の種類と本番を壊すもの
バックエンドが最新のスキーマから再生成されると、コードは通常データベースが今すぐそのスキーマに一致していると仮定します。だから再生成対応スキーマ進化は「データベースを変更できるか」よりも「ロールアウト中に古いデータや古いリクエストが生き残れるか」に関する話です。
いくつかの変更は既存の行やクエリを無効にしないため本質的に安全です。他の変更はデータの意味を変えるか、実行中のアプリがまだ期待しているものを削除してしまい、そこで本番障害が発生します。
低リスク(通常安全、加法的)
加法的な変更は既存データと共存できるため最も出荷しやすいです。
- まだ誰も依存していない新しいテーブル。
- デフォルト不要の新しい nullable カラム。
- オプションな新しい API フィールド。
例:middle_name カラムを users テーブルに nullable で追加するのは通常安全です。既存行はそのまま有効で、再生成されたコードは存在する場合に読み取れ、古い行は単に NULL を持ちます。
中リスク(意味が変わる)
これらの変更は技術的には「動く」ことが多いですが、動作を壊します。再生成によりバリデーションや生成モデル、ビジネスロジックの仮定が更新されるため、慎重な調整が必要です。
リネームは古典的な罠です:phone を mobile_phone に変えると再生成後のコードが phone を読まなくなり、本番にはまだ phone にデータが残っている、ということが起きます。同様に単位を変える(価格をドルからセントにするなど)と、コードかデータのどちらかを先に更新すると計算が壊れる可能性があります。
Enum も鋭い端です。列挙値を狭める(値を削る)と既存行が無効になることがあります。列挙を拡張する(値を追加する)方は通常安全ですが、すべてのコードパスが新しい値に対応できることが前提です。
実務的なアプローチは、意味が変わる変更を「新しく追加して、バックフィルして、切り替えて、後で削除する」方法で扱うことです。
高リスク(破壊的)
破壊的な変更は本番を即座に壊すことが多く、特にプラットフォームが再生成して古い形を期待しなくなると致命的です。
カラムやテーブルの削除、nullable から not-null への変更は、値なしで挿入しようとした瞬間に書き込みが失敗する可能性があります。「すべての行に値があるはずだ」と思っていても、次のエッジケースやバックグラウンドジョブがそうでないことを証明するかもしれません。
not-null 変更を行う必要がある場合は段階的に行ってください:まず nullable として追加し、バックフィルし、アプリのロジックを必ず値をセットするように更新してから not-null を強制します。
パフォーマンスと安全性の変更(書き込みをブロックする可能性)
インデックスや制約は「データ形状の変更」ではありませんが、それでもダウンタイムを引き起こす可能性があります。大きなテーブルでインデックスを作成したり一意制約を追加したりすると書き込みをロックしてタイムアウトを招くことがあります。PostgreSQL ではオンラインフレンドリーな方法で安全に行える場合がありますが、重要なのはタイミングです:負荷の低い時間に重い操作を行い、ステージングのコピーで所要時間を測ってください。
本番で特に注意が必要な変更は次の点を計画に入れてください:
- 互換性を保つ二段階のロールアウト(スキーマ先、コード後、またはその逆)。
- バックフィルをバッチで実行すること。
- 明確なロールバック経路(再生成されたバックエンドが早く本番に出た場合はどうするか)。
- 新ルールにデータが一致していることを証明する検証クエリ。
- 「古いフィールドは後で削除する」というチケットを立て、クリーンアップを同じデプロイに混ぜないこと。
AppMaster のように Data Designer からバックエンドコードを再生成するプラットフォームを使っているなら、安全な心構えはこうです:古いデータが今日も生きられる変更をまず出荷し、システムが適応した後でルールを厳しくする。
再生成に耐える変更の原則
再生成されたバックエンドは、スキーマ変更が本番の古い行と一致しなくなるまで素晴らしい働きをします。再生成対応スキーマ進化の目標はシンプルです:データベースと再生成されたコードが小さく予測可能なステップで追いつく間、アプリを動かし続けること。
デフォルトで「拡張・移行・縮小」を選ぶ
重要な変更はすべて三つの動きを持つと考えましょう。まずスキーマを拡張して古いコードと新しいコードの両方が動けるようにします。次にデータを移行します。最後に古いカラムやデフォルト、制約を削除して縮小します。
実践的なルール:新しい構造の追加と破壊的なクリーンアップを同じデプロイに混ぜないでください。
しばらく古い形と新しい形の両方をサポートする
次のような期間があると想定してください:
- 一部のレコードは新しいフィールドを持ち、一部は持たない。
- アプリのインスタンスの一部は古いコード、残りは再生成されたコードを動かしている。
- バックグラウンドジョブ、インポート、モバイルクライアントが遅れる可能性がある。
オーバーラップ中は、両方の形が有効になるようデータベースを設計してください。これは、Data Designer を更新して Go バックエンドを再生成するようなプラットフォームでは特に重要です。
書き込みより先に読み取りを互換にする
まず新しいコードが古いデータを安全に読めるようにします。その後に書き込みパスを切り替えて新しいデータ形を生成するようにします。
例えば単一の status フィールドを status + status_reason に分割する場合、まず新しいコードが status_reason が欠けている場合でも動けるようにしてから、段階的に status_reason を書き始めます。
部分的・未知のデータをどう扱うか決める
enum、非 NULL カラム、厳しい制約を追加する際は、値が欠けているか予期しない場合にどうするかを事前に決めておきます:
- 一時的に NULL を許容し、後でバックフィルする。
- 意味を変えない安全なデフォルトを設定する。
- 読み取りエラーを避けるために "unknown" のような値を残す。
これにより黙示的なデータ破壊(間違ったデフォルト)や厳しい障害(新しい制約が古い行を拒否する)を防げます。
各ステップにロールバック方針を持つ
拡張フェーズ中はロールバックが最も簡単です。戻す必要がある場合、古いコードは拡張されたスキーマに対してまだ動くはずです。何をロールバックするか(コードのみか、コードとマイグレーションの両方か)を書き留め、取り消し可能でない破壊的な変更は自信がつくまで避けてください。
手順:再生成に耐える変更を計画する
再生成されるバックエンドは容赦がありません。スキーマと生成コードが一致しないと本番が先に見つけます。最も安全な方法は、すべての変更を小さな取り消し可能なロールアウトとして扱うことです。ノーコードツールで作っていても同じです。
まず意図を平易な言葉で書き、現在のデータがどう見えるかを記します。本番の 3〜5 件の実際の行(または最近のダンプ)を選び、空値や古いフォーマット、驚くようなデフォルトなどの「汚い部分」をメモします。こうしておくと、現実のデータが満たせない完璧な新フィールドを設計してしまうのを防げます。
以下は、プラットフォームがバックエンドコードを再生成する場合にうまく機能する実用的な手順です(例:AppMaster が Data Designer モデルから Go バックエンドサービスを再生成する場合)。
-
まず拡張、置き換えない。 新しいカラムやテーブルは加法的に追加します。新しいフィールドは最初は nullable にするか、安全なデフォルトを付けます。新しいリレーションを導入するなら、外部キーを空許容にしてから埋めていきます。
-
何も削除せずに拡張したスキーマをデプロイする。 古いコードがまだ動作する間にデータベース変更を出荷します。目標は古いコードが古いカラムへ書き続けられること、データベースがそれを受け入れることです。
-
制御されたジョブでバックフィルする。 新しいフィールドをバッチ処理で埋め、監視と再実行ができるようにします。冪等に(2 回実行してもデータが壊れない)し、大きなテーブルでは段階的に行い、更新された行数をログに残します。
-
まず読み取りを切り替え、フォールバックを用意する。 再生成されたロジックを新しいフィールドを優先して読むようにするが、新しいデータがない場合は古いフィールドにフォールバックするようにします。読み取りが安定したら書き込みを新しいフィールドへ切り替えます。
-
最後にクリーンアップ。 信頼とロールバック計画ができたら古いフィールドを削除し、制約を厳しくします:NOT NULL にし、一意制約や外部キーを追加します。
具体例:フリーテキストの status カラムを statuses テーブルを参照する status_id に置き換えたいとします。まず status_id を nullable で追加し、既存のテキスト値からバックフィルし、アプリを status_id を読むが null の場合は status にフォールバックするように更新し、最後に status を削除して status_id を必須にします。各段階でデータベースは互換性を保つため、再生成しても安全です。
再利用できる実用的なパターン
バックエンドが再生成されると、小さなスキーマ変更が API、バリデーション、UI フォームへ波及します。再生成対応スキーマ進化の目的は、古いデータが有効なまま新しいコードをロールアウトする方法で変更を行うことです。
パターン1:破壊しないリネーム
直接の名前変更はリスクがあります。安全な方法は短期間の移行期間として扱うことです。
- 新しいカラム(例:
customer_phone)を追加し、古いカラム(phone)は残します。 - 保存時に両方へ書き込む(デュアルライト)ようロジックを更新します。
- 既存行をバックフィルして
customer_phoneを埋めます。 - カバレッジが高くなったら読み取りを新しいカラムに切り替えます。
- 後続リリースで古いカラムを削除します。
AppMaster のようなツールでは、再生成によってデータモデルやエンドポイントが最新仕様に基づいて再構築されるため、デュアルライトは古い世代と新しい世代のコードを両方満足させる良い方法です。
パターン2:一つのフィールドを二つに分割する
full_name を first_name と last_name に分割する場合、バックフィルはより手間がかかります。分割が完了するまで full_name を残しておきましょう。
実用的ルール:パースに失敗した場合は last_name に全体の文字列を入れてフラグを立て、レビュー対象にするなどのフォールバックを用意してから元のフィールドを削除します。
パターン3:フィールドを必須にする
nullable を必須に変えるのは典型的な本番ブレーカーです。安全な順序は:まずバックフィル、次に強制です。
バックフィルは機械的にデフォルトを入れる方法でも、ユーザーに入力を促すようなビジネス駆動の方法でも構いません。データが完了してから NOT NULL を追加し、バリデーションを更新してください。再生成されたバックエンドが自動的に厳しいバリデーションを追加する場合、この順序で行えば予期せぬ障害を防げます。
パターン4:enum を安全に変更する
enum の変更は古いコードが古い値を送る場合に破壊的になります。移行中は両方を受け入れる設計にしましょう。"pending" を "queued" に置き換えるなら、しばらく両方を有効にしてロジックでマップします。すべてのクライアントが古い値を送らなくなったら古い値を削除します。
もし一度にリリースしなければならない場合は、影響範囲を狭めてリスクを減らします:
- 古い項目を残しつつ新しい項目を追加する。
- データベースのデフォルトを使って挿入を継続できるようにする。
- コードを寛容にして、新しい値をまず読み、なければ古い値にフォールバックする。
- 一時的なマッピングレイヤーを設け、入力された古い値を新しい値として保存する。
これらのパターンは、再生成によりコードが素早く変わってもマイグレーションを予測可能に保ちます。
思わぬ問題を引き起こす一般的なミス
一番の誤算は、人々がコードの再生成を魔法のリセットボタンのように扱うことです。再生成されたバックエンドはアプリコードをきれいに保てますが、本番データベースには昨日のデータが残っています。再生成対応スキーマ進化とは、新しく生成されるコードとテーブルに座っている古いレコードの両方を計画することです。
よくある罠は「プラットフォームがマイグレーションを全部やってくれるだろう」と想定することです。たとえば AppMaster では Data Designer から Go バックエンドを再生成できますが、本当の顧客データをどう変換するかをプラットフォームが推測することはできません。新しい必須フィールドを追加したなら、既存行にどうやって値を入れるかの計画はあなたが用意する必要があります。
もう一つの驚きはフィールドを早まって削除や名前変更することです。あるフィールドはメイン UI で使われていないように見えても、レポート、定期エクスポート、Webhook ハンドラ、管理画面などで使われていることがあります。テストでは問題がなくても、本番で一つの忘れられた経路が古いカラム名を期待していて失敗することがあります。
夜中のロールバックを招きがちな五つのミス:
- スキーマを変えてコードを再生成したが、古い行を有効にするためのデータマイグレーションを書いて検証していない。
- すべてのリーダーとライターが更新・デプロイされる前にカラムをリネームまたは削除した。
- 大きなテーブルのバックフィルを走らせる前に所要時間を確認していない。
- 新しい制約(NOT NULL、UNIQUE、外部キー)を先に追加して、レガシーデータが壊れるのを後で発見した。
- バックグラウンドジョブやエクスポート、レポートがまだ古いフィールドを読んでいるのを忘れている。
単純なシナリオ:phone を mobile_number にリネームし、NOT NULL を追加して再生成したとします。アプリ画面は動くかもしれませんが、古い CSV エクスポートがまだ phone を参照していて、何千もの既存レコードが null の mobile_number を持っているといった事態が起きます。通常の対処は段階的な変更です:新しいカラムを追加し、しばらく両方に書き込み、バックフィルしてから制約を厳しくし、最後に古いフィールドを削除します。
安全なマイグレーションのための事前チェックリスト
バックエンドが再生成される場合、コードは速く変わりますが本番データは驚きに寛容ではありません。スキーマ変更を出荷する前に短い「これは安全に失敗できるか?」チェックを実行してください。再生成対応スキーマ進化を退屈にしておく(それが望ましい)のが目的です。
ほとんどの問題を見つける 5 つのチェック
- バックフィルの規模と速度: 既存の何行を更新する必要があるか、実際に本番でどれくらい時間がかかるかを見積もる。小さなデータベースで問題なかったバックフィルが本番では何時間もかかることがあり、アプリが遅く感じられる原因になります。
- ロックとダウンタイムのリスク: 変更が書き込みをブロックする可能性があるかを特定する。大きなテーブルの変更や型変更は長時間ロックを保持することがある。ブロックの可能性があるなら安全なロールアウト(先にカラム追加、後でバックフィル、最後にコード切り替え)を計画する。
- 古いコードと新しいスキーマの互換性: デプロイやロールバックの間に古いバックエンドが一時的に新しいスキーマに対して動くかもしれないと想定する。前バージョンが読み書きしてクラッシュしないか確認する。もし無理なら二段階リリースが必要。
- デフォルトと NULL の挙動: 新しいカラムに対して既存行は NULL のままか、デフォルトが必要かを決める。フラグ、ステータス、タイムスタンプなどで欠損を通常として扱うロジックにしておくこと。
- デプロイ後に注視する監視シグナル: 注視するアラームを具体的に選ぶ:エラー率(API 失敗)、DB の遅いクエリ、キュー/ジョブの失敗、主要なユーザー操作(チェックアウト、ログイン、フォーム送信)。バリデーションエラーの急増のような“静かな”バグも監視する。
簡単な例
orders テーブルに新しい必須フィールド status を追加する場合、一気に "NOT NULL、デフォルトなし" にしてプッシュしないでください。まずは nullable として追加し、新規行用のデフォルトを付けて再生成されたコードが欠けた status を扱えるようにしてから既存行をバックフィルし、その後に制約を厳しくします。
AppMaster を使っている場合、この考え方は特に有効です。バックエンドを頻繁に再生成できるので、スキーマ変更ごとに簡単にロールバックできる小さなリリースに扱うことでマイグレーションが予測可能になります。
例:既存レコードを壊さずにライブアプリを進化させる
内部のサポートツールでエージェントがチケットにフリーテキストの priority(例:"high", "urgent", "HIGH", "p1")をつけているとします。これを厳格な enum に変えてレポートやルーティングを安定させたい。
安全なアプローチは、再生成中も古いレコードが有効である二段階のリリースです。
リリース 1:拡張、両方へ書き、バックフィルする
まず何も削除せずスキーマを拡張します。priority_text を残したまま、priority_enum(low, medium, high, urgent など)を追加します。
次にロジックを更新して、新規作成や編集時に両方のフィールドへ書き込むようにします。AppMaster のようなノーコードツールでは、Data Designer モデルと Business Process を調整して入力を enum にマップしつつ元のテキストも保存することになります。
その後、既存チケットを小さなバッチでバックフィルします。一般的なテキスト値を enum にマップ("p1" と "urgent" -> urgent, "HIGH" -> high)し、未知のものは一時的に medium に入れてレビュー対象にします。
ユーザー側の表示は理想的には変わりません。UI は同じ優先度コントロールを見せ続け、裏で新しい enum を埋めていきます。バックフィルが進めばレポートはすぐに enum を使い始められます。
リリース 2:縮小して古いパスを削除する
十分に確信が持てたら、読み取りを priority_enum のみに切り替え、フィルタやダッシュボードを更新し、後続マイグレーションで priority_text を削除します。
リリース 2 の前に小さなサンプルで検証してください:
- 異なるチームと年代のチケットから 20〜50 件を選ぶ。
- 表示される優先度と格納された enum 値を比較する。
- enum 値ごとの件数を確認して、
mediumが異常に多くなっていないかを見る。
問題が出たら、リリース 1 が古いフィールドを残しているのでロールバックは簡単です:リリース 1 のロジックを再デプロイし、UI を一時的に priority_text を読むように戻してからマッピングを修正してバックフィルをやり直します。
次の一歩:スキーマ進化を反復可能な習慣にする
予測可能なマイグレーションを望むなら、スキーマ変更を短いタスクではなく小さなプロジェクトとして扱ってください。目標はシンプルです:すべての変更は説明しやすく、リハーサルしやすく、誤って壊しにくいこと。
視覚的なデータモデルは有用です。テーブル、関係、フィールドタイプを一目で見られると、スクリプトだけでは見落としがちな影響(安全なデフォルトのない必須フィールド、古いレコードを孤立させる関係など)に気づきやすくなります。API、画面、レポート、バックグラウンドジョブなど「誰が依存しているか」を簡単に確認しましょう。
既に使われているフィールドを変更する場合は、短期の遷移期間と重複するフィールドを優先してください。例:phone_e164 を追加して phone_raw を 1〜2 リリース残す。遷移中は新しいフィールドがあればそれを読み、なければ古いフィールドにフォールバックし、書き込みは両方に行い、完全にバックフィルされたら古い方を削除します。
環境の運用規律が、良い意図を安全なリリースに変えます。開発、ステージング、本番を揃えておきつつ、同一視しないでください。
- 開発:再生成後にバックエンドがクリーンに起動し、基本的なフローが動くかを確認する。
- ステージング:本番に近いデータで完全なマイグレーション計画を実行し、主要クエリ、レポート、インポートを検証する。
- 本番:ロールバック計画、明確な監視、そして合格すべき少数のチェックが揃っているときにデプロイする。
移行計画は短くても実際のドキュメントにしてください。何が変わるか、順序、バックフィル方法、検証方法、ロールバック方法を含め、本番に触れる前にテスト環境でエンドツーエンドで実行しましょう。
AppMaster を使うなら、Data Designer を活用して視覚的にモデルを検討し、再生成でバックエンドコードをスキーマと一致させてください。変化を早く繰り返せても、各変更に既存の本番データのための計画がある習慣が、予測可能性を保つ鍵です。


