はじめに
この記事では、設計段階でDBの状態遷移図を先に作るべき理由と、UPDATE が多い業務システムでどのように運用可能なデータモデルへ落とし込むかを整理します。
特に、イミュータブルデータモデルの利点を認めつつ、データ量や運用負荷の現実から UPDATE を前提に設計する場面を対象にします。
なぜ状態遷移図が最初に必要なのか
テーブル設計をカラム定義から始めると、後で「このデータはいつ、誰が、どの条件で更新してよいのか」が不明確になりやすいです。
この曖昧さは、実装段階で次の問題として表面化します。
- 想定外の
UPDATEが混ざる - 画面やバッチごとに更新ルールが分岐する
- 不整合の調査で原因追跡が難しくなる
状態遷移図を先に作ると、更新可能な経路と禁止すべき経路を設計時点で合意できます。
結果として、UPDATE 文の責務が明確になり、実装の迷いと不具合が減ります。
UPDATE が一番面倒になる理由
INSERT は新規追加、DELETE は削除として比較的責務が単純です。
一方で UPDATE は、既存状態との関係を常に意識する必要があります。
- 遷移前の状態チェックが必要
- 同時更新の競合を考慮する必要がある
- 監査や再現のために変更履歴を残す必要がある
- 業務ルール変更の影響を受けやすい
つまり、UPDATE はSQL構文の問題ではなく、状態管理そのものの問題です。
イミュータブルモデルは有効だが万能ではない
イミュータブルデータモデルは、更新を追加操作へ置き換えるため、監査性と再現性の面で強力です。
ただし実務では次の制約にぶつかります。
- データ量の増加による保管コスト
- 参照クエリの複雑化
- JOINが多くなりパフォーマンスが悪化しやすい
- 最新状態の算出コスト
- 保守担当者の学習コスト
そのため、全領域を完全イミュータブルにするより、履歴を残すべき領域と UPDATE で管理する領域を分ける設計が現実的です。
ユーザー仮登録と本登録の例で見る必要性
たとえばユーザー管理で、仮登録 -> 本登録 -> 退会 のような状態を UPDATE で管理する場合、状態遷移図がないとデータの変化を追いにくくなります。
仮登録の時点では、メールアドレスは存在しても、社内ユーザーIDや所属情報はまだ払い出されていないことがあります。
このように「状態ごとに存在するデータが違う」前提を図で明示しておかないと、実装側で不整合が起きやすくなります。
- どの操作で
仮登録から本登録へ変わるのか - メール未確認のまま本登録へ進めるのか
- 本登録時に社内ユーザーIDをいつ払い出すのか
- 本登録後にどの機能が利用可能になるのか
- 退会時に関連テーブルをどう更新するのか
ここが曖昧だと、別機能の設計時に「このユーザーテーブルは今どの状態を前提にすべきか」が分からなくなります。
結果として、認可、通知、課金、監査ログなど周辺機能で前提不一致が起きやすくなります。
イミュータブルで全面管理しないのであれば、少なくとも状態遷移図で どの状態からどの状態へ、何を条件に変わるか を固定しておくことが重要です。
状態遷移図のサンプル
状態遷移図は、Mermaidで十分です。テキストで管理できるため、レビューや差分確認がしやすくなります。
この程度の粒度でも、読者は「どの操作で、どの状態へ移るか」を短時間で理解できます。
状態遷移だけでは足りない場合はカラム変化表を作る
個人的に一番効くのは、状態遷移図に加えて「遷移ごとのカラム変化」を表にすることです。
状態が分かっても、実装で本当に迷うのは「どのカラムを、どのタイミングで、どう変えるか」だからです。
例えば、ユーザーの 仮登録 -> 本登録 を扱うなら次のように整理します。
| 遷移 | status | employee_id | email_verified_at | activated_at | updated_by |
|---|---|---|---|---|---|
| 新規作成 | NULL -> pre_registered |
NULL |
NULL |
NULL |
system |
| メール確認 | pre_registered -> pre_registered |
NULL |
NULL -> 時刻 |
NULL |
system |
| 本登録完了 | pre_registered -> active |
NULL -> 採番値 |
変更なし | NULL -> 時刻 |
operator/api |
| 停止 | active -> suspended |
変更なし | 変更なし | 変更なし | operator |
| 退会 | active/suspended -> withdrawn |
変更なし | 変更なし | 変更なし | operator |
この表があると、次の判断が速くなります。
-
NULLを許容する期間がどこまでか - 本登録時に必須で埋まるカラムは何か
- どの遷移で監査項目を更新すべきか
- どのカラムは更新禁止にすべきか
状態遷移図は「流れ」、カラム変化表は「実装ルール」です。
この2つをセットで持つと、他機能設計時にもテーブルの動きが読み取りやすくなります。
実務で使える設計方針
1. 先に状態と遷移を定義する
まず業務状態を列挙し、遷移可能な経路だけを定義します。
- 例:
draft -> submitted -> approved -> archived - 逆戻り可能かどうかを明記する
- 手動更新可能な状態を限定する
2. 遷移ごとに更新責務を固定する
各遷移について、更新主体と更新対象を明示します。
- どのAPIやバッチが更新するか
- どのカラムを更新可能とするか
- 遷移時に必須の監査項目(更新者、更新時刻、理由)
3. 不正な遷移をSQLで防ぐ
アプリ側の分岐だけに頼らず、WHERE 条件で遷移前状態を拘束します。
UPDATE orders
SET status = 'approved', approved_by = :user_id, approved_at = NOW()
WHERE id = :id
AND status = 'submitted';
この形にしておくと、同時更新や想定外状態への上書きを防ぎやすくなります。
4. 履歴テーブルを併用する
完全イミュータブルが難しい場合でも、重要遷移だけ履歴を残す方式は取りやすいです。
- 本体テーブル: 最新状態を保持
- 履歴テーブル: 状態変更イベントを追記
この分離で、参照性能と監査性のバランスを取りやすくなります。
よくあるアンチパターン
画面ごとに勝手にUPDATEする
画面Aは status だけ更新、画面Bは status と approved_at を更新、バッチは status と reason を更新、のように更新責務が分散するパターンです。
この状態になると、同じ遷移でも更新項目が揺れ、監査や不具合調査で整合が取れなくなります。
対策は、状態遷移単位で更新責務を固定することです。
「どの遷移で、どのカラムを、どの条件で更新するか」を1か所に定義し、画面はそのルールを呼び出すだけにします。
最小構成の状態遷移図テンプレート
設計レビューでは、少なくとも次を1枚にまとめると実装しやすくなります。
- 状態一覧
- 許可される遷移矢印
- 遷移トリガー(API、バッチ、手動)
- 遷移ガード条件
- 更新カラム
- 監査ログ要否
図がなくても実装は進みますが、UPDATE が増えるほど設計の前提が失われます。
状態遷移図は、実装者間での共通言語として効きます。
まとめ
UPDATE が多いシステムほど、先に状態遷移を設計する価値が高くなります。
イミュータブルモデルは有効な選択肢ですが、データ量と運用コストの制約があるため、現実にはハイブリッド設計が必要です。
設計段階で状態遷移図を作っておけば、UPDATE 文は「場当たり的な修正」ではなく「許可された遷移の実装」として管理できるようになります。