3
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

UPDATE設計を破綻させないDB状態遷移図の作り方

3
Last updated at Posted at 2026-03-03

はじめに

この記事では、設計段階で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は statusapproved_at を更新、バッチは statusreason を更新、のように更新責務が分散するパターンです。
この状態になると、同じ遷移でも更新項目が揺れ、監査や不具合調査で整合が取れなくなります。

対策は、状態遷移単位で更新責務を固定することです。
「どの遷移で、どのカラムを、どの条件で更新するか」を1か所に定義し、画面はそのルールを呼び出すだけにします。

最小構成の状態遷移図テンプレート

設計レビューでは、少なくとも次を1枚にまとめると実装しやすくなります。

  • 状態一覧
  • 許可される遷移矢印
  • 遷移トリガー(API、バッチ、手動)
  • 遷移ガード条件
  • 更新カラム
  • 監査ログ要否

図がなくても実装は進みますが、UPDATE が増えるほど設計の前提が失われます。
状態遷移図は、実装者間での共通言語として効きます。

まとめ

UPDATE が多いシステムほど、先に状態遷移を設計する価値が高くなります。
イミュータブルモデルは有効な選択肢ですが、データ量と運用コストの制約があるため、現実にはハイブリッド設計が必要です。

設計段階で状態遷移図を作っておけば、UPDATE 文は「場当たり的な修正」ではなく「許可された遷移の実装」として管理できるようになります。

3
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?