ほとんどの家計簿アプリは、残高をウォレットテーブルの balance フィールドに保存します。支出を 1 件書き込むとき、コードでは 2 つのことを行います。取引を挿入し、次に残高を更新します。この 2 つは同時に成功しなければならず、そうでないと帳簿が合わなくなります。
単一マシンのシングルスレッドであれば問題ありません。しかし同期、インポート、一括消し込み、並行書き込みが絡むと、残高フィールドの維持は終わりのない防御戦になります。変更のたびに「残高は更新されたか、正しく更新されたか、どのコードパスで更新し忘れたか」を考えなければなりません。
発想の転換:残高を導出値にする
複式簿記のやり方では、1 件の記帳はひとつの金額ではなく、Leg のグループです。各 Leg は「袋 + 金額」です。核心の制約はただひとつ。同じ取引内で、通貨ごとの Leg の合計はゼロでなければならない。
01// 一条账目 = 一组求和为零的 Leg02// 午饭 38 元,用招行卡支付:03transaction(term: .expense) {04 leg(from: .asset(.cmbCard), amount: -38.00)05 leg(from: .expense(.dining), amount: +38.00)06} // sum == 0 ✓0708// 余额不存库,永远派生:09// balance = 期初 + Σ 该袋子下所有 Leg残高はこれで、維持すべき状態ではなくクエリの結果になります。取引の書き込みで保証すべきは Leg の合計がゼロであることだけ。これはカーネル層のひとつの検証ゲートで強制できる性質のものです。
袋モデル:11 種類で足りて、膨らまない
資産、負債、証券、暗号資産のポジション、消し込み待ち、分割の中間袋など。11 種類の袋型が、個人記帳で実際に遭遇するシナリオをカバーします。袋型は固定の列挙型で、ユーザーによる独自定義は許しません。これは意図的な収束で、データモデルが要件とともに際限なく膨らむのを防ぎます。
代償は何か
- 残高の照会には集計計算が必要で、単一フィールドの読み取りより遅い(インデックスとキャッシュで補う)
- データ量が増えると集計コストが上がるため、期間ごとに分割して管理する必要がある
- 開発者への要求が高まる。書き込みのたびに正しい Leg の組み合わせを構成する必要があり、数字だけを書き換えることはできない
私たちは「書くのが面倒な」モデルと引き換えに、「残高が合わない」という問題のカテゴリごと排除しました。記帳ツールにとって、この取引は十分に割に合います。