基本の14章 · 04

顧客、口座、元帳をなぜ別々に保存するのか

一人のお客さまを複数の記録で表す理由。識別子、関係、金額、残高の正本を、架空の山田さんのデータで理解する。

本文の説明・図・掲載コードだけで読み進められます。出典リンクは、権限のある人向け(非公開GitHub)です。GitHubで閲覧権限がある方だけ開けます。

この記事でわかること

  • ログイン利用者、顧客、預金口座、会計用勘定は目的が異なる
  • 現在残高は仕訳の集計から説明できる派生値として扱う
  • 業務の状態と計上済みの事実を分けると失敗や取消しを追える
この記事の目次
01 / READ THE SYSTEM

一人の山田さんに、複数のIDがある理由

架空の山田さんがMiniBankを使い始めると、まずログイン用の利用者の記録ができます。次に銀行のお客さまとして氏名や本人確認の状態を登録し、審査後に普通預金口座を開きます。見た目は一人の利用者でも、認証、顧客情報、口座契約は別々の出来事です。ログインに成功した人が、必ず開設済みの銀行口座を持っているとは限りません。

認証の利用者にはAuth User ID、customersの行にはCustomer ID、bank_accountsの行にはAccount IDがあります。UUIDという長い識別子は、内部で「どの一件か」を区別するために使います。一方、口座番号は顧客が振込などで扱う表示上の番号です。名前は重複・変更する可能性があるため、氏名だけで口座の持ち主を結び付ける設計にはしません。

customersはauth_user_idで認証利用者とつながり、bank_accountsはcustomer_idで顧客とつながります。これは同じ情報を無意味に三重保存するのではなく、役割の違う記録を参照で結んでいるのです。口座画面も三つの記録を区別して見せ、ログイン認証と銀行の管理状態を混同しないようにしています。

根拠:顧客とAuth利用者の関係(非公開GitHub・要権限)

根拠:口座と顧客の関係(非公開GitHub・要権限)

根拠:三つの記録を分ける画面(非公開GitHub・要権限)

02 / READ THE SYSTEM

預金口座と会計用の勘定も、別の記録になる

顧客の普通預金口座には、口座番号、通貨、利用可能か凍結中かという状態、表示用の残高などがあります。これに対して元帳の勘定は、仕訳の行をどこへ分類するかを決める箱です。山田さんの普通預金口座には、山田さんへの預金債務を記録する勘定が一つ対応します。口座開設の基盤処理は、この二つを同時に作ります。

たとえば架空の口座番号1234567なら、対応する勘定コードはDEP-1234567です。勘定にはaccount_classとnormal_sideがあります。前者は資産・負債などの区分、後者は通常どちら側で増えるかです。顧客預金は銀行から見た負債なので、貸方側で増える設定になります。「お客さまのお金なのに、なぜ負債なのか」は、帳簿を銀行側の視点で書いていると考えると理解できます。

すべての勘定が顧客口座に対応するわけでもありません。現金の相手勘定や手数料収益のように、銀行全体のための勘定もあります。bank_account_idがない勘定を許すのは、このためです。口座一覧だけでは銀行全体の資産や損益を説明できず、元帳の勘定が別に必要になります。

根拠:口座に対応する預金勘定(非公開GitHub・要権限)

根拠:顧客預金と銀行全体の勘定科目(非公開GitHub・要権限)

03 / READ THE SYSTEM

関係を説明するだけでなく、壊れないように制約を置く

データベースの外部キーは「この参照先が実在すること」を確かめる仕組みです。bank_accounts.customer_idなら、存在しない顧客へ口座をぶら下げることを防ぎます。一意制約は「同じ値を二件作らない」という約束です。基盤の個人口座開設では、顧客ごとの口座の重複と、同じ口座に対応する預金勘定の重複を防ぎます。

アプリ側で先に件数を調べるだけでは、二つの依頼がほぼ同時に来たとき、両方が「まだない」と判断することがあります。保存場所にも制約があれば、後から到着した追加を拒否できます。人が読める設計書、サービスの事前確認、DBの強制力は、それぞれ役割が違います。「一口座のはず」と書いてあるだけで、守られるわけではありません。

削除にも境界があります。顧客や口座の参照は、関連記録まで自動的に消す連鎖削除を避ける定義です。顧客のログインを消しただけで会計履歴まで失われては、過去の取引を説明できません。ただし、ここで述べる一人一口座の関係は基盤の個人普通預金の設計です。後から追加された法人や複数法人の領域まで、一つの図で同じ制約だと決め付けないでください。

根拠:連鎖削除を避けた顧客参照(非公開GitHub・要権限)

根拠:顧客・口座・勘定の一意性(非公開GitHub・要権限)

04 / READ THE SYSTEM

一つの取引に、見出しと複数の明細行を持たせる

仕訳は、一つの出来事を借方と貸方に分けた会計記録です。ledger.journal_entriesはその見出しで、取引の種類、説明、発生時刻、元の仕訳への取消し参照などを持ちます。ledger.journal_linesは明細行で、どの勘定に、借方・貸方のどちらで、いくら計上したかを持ちます。見出し一件に複数の行がつく親子関係です。

山田さんが一万円を入金した例なら、見出しは入金一件です。その下に、現金の相手勘定の借方一万円と、山田さんの預金勘定の貸方一万円が並びます。二行あるから二万円を預けたという意味ではありません。同じ一万円という出来事を、増えた資産と増えた負債の二つの側面から記録しています。

constraint journal_lines_exactly_one_side check (
  (debit_amount > 0 and credit_amount = 0) or
  (credit_amount > 0 and debit_amount = 0)
)

実際の表の制約は、一行の中では片側だけが正の金額になるようにしています。両方がゼロ、両方が正、負の金額などを拒否するためです。仕訳全体で左右が同額かという確認は、さらに別に必要です。行の正しさと、行を集めた一件の正しさを、分けて守っています。

根拠:仕訳見出し・明細と片側金額の制約(非公開GitHub・要権限)

05 / READ THE SYSTEM

残高は「集計の答え」であり、理由の代わりではない

金額は整数の円として保存します。仕訳行の型はbigintです。小数の近似計算で一円未満の誤差が積み重なるような表し方を避け、必要な丸めや上限は各業務で明示して扱う設計です。ただし整数型というだけで無制限の金額に対応できるわけではありません。v8の事象金額にも上限があり、画面や言語側で正確に扱える範囲を考慮しています。

顧客預金の残高は、対応する勘定の貸方合計から借方合計を引いて求められます。一万円の入金と三千円の出金なら、貸方一万円、借方三千円で七千円です。bank_accounts.cached_balanceは、この結果を読みやすくするために保持する値です。全履歴を毎回集計せず表示できる利点がありますが、元帳と別々に好きな値へ変えてよいわけではありません。

固定ソースの計上基盤には、新しい仕訳の前に既存の残高差異を調べ、差異があれば止める処理があります。新しい取引に紛れて古い不整合を黙って直すと、いつ壊れたかという証拠が消えるからです。元帳が理由を持つ正本で、キャッシュは正本から説明できる派生値、という関係を保っています。

根拠:整数円としての仕訳金額(非公開GitHub・要権限)

根拠:事象金額の上限(非公開GitHub・要権限)

根拠:既存差異の拒否と同一取引内のキャッシュ再計算(非公開GitHub・要権限)

06 / READ THE SYSTEM

依頼中の記録と、計上済みの記録を混ぜない

送金の申請、本人確認、口座凍結などには途中の状態があります。一方、基盤のjournal_entriesには、未計上から計上済みに変わるためのstatus列を置いていません。見出しと行を一つのトランザクションで作り、外から確定後に見える仕訳は計上済みという考え方だからです。初期設計資料の案にstatusという語があっても、実際の表定義を優先して読む必要があります。

TransactionServiceが扱う顧客の入出金明細も、仕訳が記帳された後の行を対象とし、statusはcompletedと説明されています。これは、すべての送金依頼が完了済みという意味ではありません。依頼の状態一覧と、実際に記帳された明細は、対象としている記録が異なります。未完了の依頼が明細にないことだけで、依頼そのものが消えたと判断してはいけません。

v8ではさらにbusiness_eventsが、出来事、使用したルール、仕訳を結び付けます。旧経路の入出金にはfinancial_operationsという業務記録もあります。表が複数あるのは履歴を無駄に増やすためではなく、「依頼」「業務上の結果」「会計の証拠」を別の問いに答えられる形で残すためです。

根拠:未計上状態を持たない基盤の仕訳表(非公開GitHub・要権限)

根拠:計上済み明細の意味(非公開GitHub・要権限)

根拠:新しい事象の記録と旧経路の共存(非公開GitHub・要権限)

07 / READ THE SYSTEM

変更できる記録と、追記だけの記録を分ける

顧客の氏名や管理状態には更新が必要です。そのため基盤の顧客・口座にはupdated_atがあります。しかし仕訳行を後から上書きすると、当時何が起きたかが消えてしまいます。MiniBankでは通常の権限と操作の範囲で、仕訳見出し・明細の更新や削除をトリガーで拒否します。トリガーは、対象の変更の直前などにDBが自動で呼ぶ防御です。

誤りの修正は、元の仕訳を残し、その効果を打ち消す逆仕訳を追加する形になります。帳簿の最終残高が同じでも、最初から正しく入れた履歴と、誤りを認めて直した履歴は区別できます。調査や承認が必要な理由も、この経過から説明できます。

ただし追記専用は、どんな権限の攻撃者にも絶対に改ざんできないという意味ではありません。元の保護定義にも、DB所有者や特権者がトリガーを無効化するような操作は別の脅威として明記されています。この章はソース上の構造を読むもので、公開DBの権限や保存データを検査した結果ではありません。

根拠:追記専用の保護とその限界(非公開GitHub・要権限)

SOURCE NOTES

出典と、確認できた範囲

コードの説明と、稼働環境の検証を分けます

以下のリンクは非公開GitHubへの出典で、閲覧権限が必要です。本文はリンク先を開かなくても読める構成です。固定コード・設計資料を参照していますが、今回、この読み物のために銀行の取引やDB操作、本番試験は実行していません。実装や資料にある検査と、こちらで実施した検証を混同しないようにしています。

  1. 顧客の定義 ↗(非公開GitHub・要権限)
    Authとの参照、状態、削除の境界
  2. 口座の定義 ↗(非公開GitHub・要権限)
    顧客との関係、口座番号、派生残高
  3. 口座と勘定の作成 ↗(非公開GitHub・要権限)
    預金勘定の区分と一意性
  4. 仕訳の定義 ↗(非公開GitHub・要権限)
    見出し・行・整数円・片側金額
  5. 残高差異の保護 ↗(非公開GitHub・要権限)
    元帳からの再計算と差異の拒否
  6. 追記専用保護 ↗(非公開GitHub・要権限)
    通常の更新・削除拒否と権限上の限界
  7. 明細サービス ↗(非公開GitHub・要権限)
    依頼状態と計上済み明細の区別

MiniBankは学習・検証用の銀行アプリです。架空の金額や人物は、仕組みを説明するための例です。

つながる読み物

ここまで読んだ記録を、ひとつ。

自分のペースで。あとから取り消すこともできます。

次の読み物入金と出金は、残高の足し算・引き算だけではない
→