業務システムのステータス設計とテーブル分割は、「業務で呼ばれている伝票の名前と、DBのテーブルは1対1ではない」という前提から始めます。受付票・見積書・作業報告・請求書が業務に出てきたら、それが同じ1つのデータの状態違いなのか、別々のデータなのかを判断します。この記事では、そのときに使う3つの型と、どれを選ぶかの判断基準を、修理受付の業務を例に説明します。
なぜステータスの設計が業務システムの要になるのか
業務システムの画面やAPIは、ほとんどが「いまどの状態のデータに、誰が何をできるか」で動いています。一覧画面の絞り込み、ボタンの表示・非表示、通知の送信、集計の対象。どれもステータスを条件にしています。
そのため、ステータスの切り方が業務と合っていないと、影響が広い範囲に出ます。現場では「受付」「対応中」「完了」のような単純な流れだけでなく、差し戻し、保留、取り消し、再依頼といった後戻りの流れが並行して起きます。最初にこれらを洗い出さずに作ると、リリース後に「この状態に戻したい」という要望が出るたびにステータスを継ぎ足すことになり、条件分岐が増え続けます。
3つの型
テーブルの持ち方は、大きく3つの型に分けられます。
| 型 | 構成 | 向いている場面 |
|---|---|---|
| A | 1テーブルにステータス列を持たせ、値を書き換えて遷移させる | 小規模、流れが一直線、属性がほとんど変わらない |
| B | 業務のイベントごとにテーブルを分ける | 履歴が必須、フェーズごとに書き手のロールがはっきり分かれる |
| C | ヘッダ、明細、イベントログに分け、必要な部分だけ別テーブルにする | 多くの業務システムでの標準的な選択 |
型ごとの長所と短所
| 観点 | A:1テーブル | B:イベントごとに分割 | C:ヘッダ+明細+ログ |
|---|---|---|---|
| 履歴 | 上書きで消える | テーブルごとに残る | イベントログにすべて残る |
| 参照の速さ | 1回のSELECTで済む | JOINが必要 | ヘッダの参照+必要な分だけJOIN |
| 同時更新の衝突 | 同じ行に集中する | 分散する | 分散する |
| ロールごとの権限 | 行単位での制御が必要 | テーブル単位で分けられる | テーブル単位で分けられる |
| 不具合の影響範囲 | 広い | 狭い | 層ごとに限られる |
| 設計の手間 | 少ない | 中程度 | 多い |
型Aは作るのが速い反面、履歴が消え、全フェーズの項目が1つのテーブルに集まって列が増えていきます。型Bは履歴と権限の分離に強い反面、テーブルの数が増えて全体像を読みにくくなります。型Cは設計の手間がかかりますが、後からの変更に最も耐えやすい型です。迷ったら型Cを選んでおくと、後で困ることが少なくなります。
どの型を選ぶかの判断基準
次の4つを順に問います。
- 最初に受け付けた数量や金額が、後の工程で変わることがあるか。あれば型C
- 監査、分析、クレーム対応のために履歴を残す必要があるか。あれば型C
- フェーズによって書き込むロールが切り替わるか。切り替わるなら型B
- 今後の改修が多いと見込まれるか。多いなら型C、少なければ型A
2つ目の履歴については、現場の担当者が「履歴は要らない」と言っても、それで決めないでください。経理、監査、顧客対応の担当に確認すると、必要だと言われることがよくあります。
例題:修理受付の業務を設計する
家電の修理を受け付ける会社の業務を考えます。流れは次のとおりです。
- 受付担当が顧客から修理依頼を受ける
- 技術者が製品を見て見積を作る
- 顧客が見積を承認する、または断る
- 技術者が修理し、作業内容を報告する
- 経理が請求書を出し、入金を確認する
業務に出てくる呼び名は「受付票」「見積書」「作業報告」「請求書」の4つです。これを全部1つのテーブルに入れてステータスで切り替える(型A)と、見積の金額を後から修正したときに、当初の見積額が上書きされて消えます。顧客から「最初の見積と違う」と言われたときに確かめられません。また、受付担当・技術者・経理が同じ行を更新するので、権限の制御が行単位の条件分岐になります。
そこで、判断基準を当てはめます。見積額は修理中に変わりうる、クレーム対応で履歴が要る、書き手のロールはフェーズごとに変わる。したがって型Cを選びます。
-- 修理依頼の本体(現在の状態を持つ)
CREATE TABLE repair_requests (
request_id BIGINT PRIMARY KEY,
customer_id BIGINT NOT NULL,
product_model VARCHAR(100) NOT NULL,
received_at TIMESTAMP NOT NULL,
current_status VARCHAR(20) NOT NULL -- 受付済 / 見積待ち / 承認待ち / 修理中 / 修理完了 / 請求済 / 入金済 / 取消済
);
-- 見積(版ごとに行を追加し、上書きしない)
CREATE TABLE estimates (
estimate_id BIGINT PRIMARY KEY,
request_id BIGINT NOT NULL REFERENCES repair_requests(request_id),
version INT NOT NULL,
amount INT NOT NULL,
created_by BIGINT NOT NULL,
created_at TIMESTAMP NOT NULL,
UNIQUE (request_id, version)
);
-- 作業報告(技術者だけが書く)
CREATE TABLE work_reports (
report_id BIGINT PRIMARY KEY,
request_id BIGINT NOT NULL REFERENCES repair_requests(request_id),
work_detail TEXT NOT NULL,
completed_at TIMESTAMP NOT NULL
);
-- 請求(経理だけが書く)
CREATE TABLE invoices (
invoice_id BIGINT PRIMARY KEY,
request_id BIGINT NOT NULL REFERENCES repair_requests(request_id),
billed_amount INT NOT NULL,
billed_at TIMESTAMP NOT NULL,
paid_at TIMESTAMP
);
-- 状態遷移のログ(追記のみ)
CREATE TABLE request_events (
event_id BIGINT PRIMARY KEY,
request_id BIGINT NOT NULL REFERENCES repair_requests(request_id),
from_status VARCHAR(20),
to_status VARCHAR(20) NOT NULL,
actor_role VARCHAR(20) NOT NULL,
reason TEXT,
occurred_at TIMESTAMP NOT NULL
);
見積は版を重ねて保存するので、当初の金額も最終の金額も残ります。作業報告と請求はテーブルを分けたので、書き込めるロールをテーブル単位で決められます。ステータスの変化はすべて request_events に残るため、「いつ、誰が、なぜ取り消したか」を後から追えます。
状態遷移を業務フローと揃える
ステータスの値と遷移は、業務フローの「次に何が起きうるか」をそのまま写します。
| 現在のステータス | 起きうる遷移 | 遷移させるロール |
|---|---|---|
| 受付済 | 見積待ち、取消済 | 受付担当 |
| 見積待ち | 承認待ち | 技術者 |
| 承認待ち | 修理中、取消済、見積待ち(再見積) | 受付担当(顧客の回答を受けて) |
| 修理中 | 修理完了、見積待ち(追加費用が判明) | 技術者 |
| 修理完了 | 請求済 | 経理 |
| 請求済 | 入金済 | 経理 |
「修理中から見積待ちに戻る」のような後戻りは、設計者だけで考えると抜けやすい遷移です。現場の担当者と一緒にこの表を確認し、実際の運用で起きることを全部書き出します。表のどの行にも書かれていない遷移は、システムで禁止します。可能ならアプリ側だけでなく、DBの制約やトリガでも遷移のルールを守らせると、直接データを書き換えたときにも状態が壊れにくくなります。
ステータスの名前のつけ方
| よい例 | 理由 |
|---|---|
| 受付済、請求済、入金済 | 何が終わったかがわかる |
| 修理中、配送中 | 何をしている最中かがわかる |
| 承認待ち、入金待ち | 誰の何を待っているかがわかる |
| 取消済、差戻済 | 例外の流れも名前で区別できる |
避けたい名前もあります。ST01 のような記号は業務と対応が読めません。「完了」「OK」だけでは何の完了かがわかりません。「保留」「その他」「エラー」は、扱いに困ったデータが何でも入る受け皿になり、そこに溜まったデータで業務が止まります。例外は専用のテーブルや別の流れで扱います。
よくある失敗
- 金額や数量を上書きする。当初の見積の根拠が消え、経理の確認やクレーム対応で困ります。版を重ねるか、イベントログに変更前後の値を残します
- 「保留」「その他」を作る。例外がすべてそこに集まり、誰も処理しないまま残ります
- 新しいフェーズを全部ステータスの追加で済ませる。年月が経つとステータスの種類が膨らみ、条件分岐が読めなくなります。新しいフェーズが別のロールの仕事なら、テーブルを追加することを考えます
- 1テーブルの列が増え続ける。全フェーズの項目が混ざったテーブルは読めず、どの列がいつ埋まるのかもわかりません。ヘッダ、明細、イベントの3層に分けます
- 請求済のあとも更新できる。締めた後の金額が変わってしまいます。一定のステータス以降は更新を禁止する制約を入れます
この記事のまとめ
- 業務上の伝票の呼び名とDBのテーブルは1対1ではない。同じデータの状態違いか、別のデータかを判断する
- 型は3つ。1テーブル+ステータス列、イベントごとの分割、ヘッダ+明細+イベントログ
- 金額や数量が変わる、履歴が要る、改修が多いなら型C。迷ったら型C
- 遷移は業務フローと揃え、後戻りの流れを現場と一緒に洗い出す
- ステータス名は業務の言葉で、何が終わったか・何を待っているかがわかる名前にする
ステータス設計はロードマップのステップ6「進めて作る」で扱う技術です。ロールとステータスの組み合わせを表で洗い出す方法は俯瞰思考の記事、設計の前提になる業務の流れはASIS業務フローの記事、作ったあとの確認は動作確認の基本で扱っています。判断フローと設計例の全体はFDEガイドの「ステータス管理とテーブル分割」で読めます。スライド・演習問題・テストは lecture.iplot.jp で取り組めます。
