スキーマは最後の防衛線だ
一言でいうと
本番のデータ契約は、アプリケーションのifではなく、PostgreSQLの制約で最後まで守る必要があります。注文APIなら、正の金額、所有者と組織、組織内で一意な冪等キー、作成時刻を、データベースが直接保証しなければなりません。
なぜアプリケーションの検査だけでは足りないのか
書き込みの経路は1つで始まっても、すぐにバッチ、管理ツール、復旧スクリプト、新しいサービスが加わります。最初のAPIが金額を検査していても、別の経路が負の値を入れれば、データはすでに汚染されています。NOT NULL、CHECK、UNIQUE、外部キーは、どのクライアントがアクセスしても同じルールを適用します。特に、冪等キーはグローバルではなく組織の範囲でなければ、異なる顧客が同じキーを使っても衝突しません。そのため、UNIQUE (org_id, idempotency_key)のほうが、ビジネスの境界をより正確に表現します。
PostgreSQL 16のGENERATED ALWAYS AS IDENTITYは、SQLiteの整数の主キーとは異なる、実際の運用の文法です。TIMESTAMPTZは、保存された瞬間を1つの時間軸で比較できるようにします。インデックスは、参照パターンに沿ってorg_id、owner_id、created_atの順序で設計します。マイグレーションはBEGINとCOMMITの間で実行して、途中の状態を公開しないようにします。
現場での検証方法
DDLのテキストに単語があるかどうかだけを見てはいけません。空の分離スキーマにマイグレーションを実際に適用し、正常な行を入れ、同じ組織と同じキーの2行目がunique_violationになるかを確認します。負の金額がcheck_violationになるかも、実行して確かめます。このとき、本番のデータと混ざらないように、一時的なスキーマを作って、終わったら削除します。同じ検証をCIで繰り返せてこそ、「自分のマシンでは動いた」を、デプロイの証拠に変えられます。
実務での判断基準
制約は、エラーを遅らせる障壁ではなく、誤った状態が保存されないようにする契約です。エラーコードは、APIでは意味のある409や400に翻訳しますが、データベースの制約そのものをなくしはしません。ロールバックの戦略も考える必要がありますが、まず、順方向のマイグレーションが、新しい空のDBと既存のデータの条件の両方を満たすことを証明します。次のレッスンでは、この不変条件をトランザクションとリトライの意味に結びつけます。