業務システムのステータス設計とテーブル分割は、「業務で呼ばれている伝票の名前と、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つを順に問います。

  1. 最初に受け付けた数量や金額が、後の工程で変わることがあるか。あれば型C
  2. 監査、分析、クレーム対応のために履歴を残す必要があるか。あれば型C
  3. フェーズによって書き込むロールが切り替わるか。切り替わるなら型B
  4. 今後の改修が多いと見込まれるか。多いなら型C、少なければ型A

2つ目の履歴については、現場の担当者が「履歴は要らない」と言っても、それで決めないでください。経理、監査、顧客対応の担当に確認すると、必要だと言われることがよくあります。

例題:修理受付の業務を設計する

家電の修理を受け付ける会社の業務を考えます。流れは次のとおりです。

  1. 受付担当が顧客から修理依頼を受ける
  2. 技術者が製品を見て見積を作る
  3. 顧客が見積を承認する、または断る
  4. 技術者が修理し、作業内容を報告する
  5. 経理が請求書を出し、入金を確認する

業務に出てくる呼び名は「受付票」「見積書」「作業報告」「請求書」の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 で取り組めます。