FIG-117

CQRSとイベントソーシング — 残高を書かずに、明細を再生する

分散システム 2026.10.02 公開 読了 約11分

銀行口座の残高をデータベースに保存するとき、ふつうは「残高 700円」という今の値を上書きしていきます。シンプルですが、上書きした瞬間に前の値と、なぜそうなったかは消えてしまいます。

イベントソーシングは逆の発想です。残高を保存せず、「入金した」「出金した」という出来事(イベント)だけを、順番に書き足していく。今の残高は、その記録を最初から再生して計算します。通帳の明細だけを持っておき、残高は明細を足し算して出すイメージです。図1で、出来事を積み上げ、時間をさかのぼってみてください。

EVENT SOURCING — 今の値ではなく、出来事を保存する。今の値は再生して作る
📜 イベントの記録(書き足すだけ)
    🗃 ふつうのDB(上書き)
    0円 前の値は残っていない
    ▶ イベントソーシング(再生した結果)
    0円 記録を最初から足し算した値
    図1 — 記録は消さず、書き換えもしない。取り消しも「取り消した」という新しいイベントとして書き足す

    消さずに、書き足す

    「↩ 直前の出金を取り消す」を押しても、記録の中の出金は消えません。代わりに「出金を取り消した(+300円)」という新しい出来事が書き足されます。会計の帳簿で、間違えた行を消しゴムで消さず、赤字で打ち消しの行を書くのと同じです。

    記録が全部残っているので、スライダーで3件目の時点の残高のように過去を正確に再現できます。ふつうのDBにはこれができません。「なぜ今700円なのか」を説明できるのは、記録を持っている側だけです。お金や在庫のように経緯の説明が求められるデータで、とくに役に立ちます。

    ただし、イベントが何百万件もたまると、毎回最初から再生するのは大変です。そこで「1000件目の時点の残高は○○円」というスナップショットをときどき保存し、そこから先だけを再生します。

    書く係と、読む係を分ける — CQRS

    イベントの記録は「書き足す」のは得意ですが、「今の残高を見せて」「大きな出金だけ一覧にして」といった読み出しには向いていません。毎回再生していたら遅すぎます。そこで、書き込み(コマンド)と読み出し(クエリ)を別の係に分けるのがCQRSです。読む係は、イベントが届くたびに画面ごとに読みやすい形の表(読み取りモデル)を作っておきます。図2で試してください。

    CQRS — 書く係はイベントを書き足すだけ。読む係が、画面ごとの表を作っておく
    ✍️ 書く係(コマンド)
    📜 イベントの記録
      👀 読む係(読み取りモデル)
      残高の画面 0円 最新
      入金・出金の合計 +0 / −0 最新
      大きな出金(500円以上)の一覧 — 最新
      図2 — 読む係が表を作り直すまでに、約1.2秒かかるものとしている

      書いた直後は、読む係の表にまだ反映されていない瞬間があります(「反映待ち」)。少し待てば必ず追いつく、という性質を結果整合性と呼びます。そのかわり、読む係は画面に合わせた形で表を持てるので速く、あとから新しい画面が必要になっても、記録を最初から再生すれば過去の分までそろった表が作れます。

      用語ミニ辞書
      イベントソーシング
      今の状態ではなく、状態を変えた出来事を順番に保存し、再生して今の状態を作る方式。
      イベント
      「入金した」のように、すでに起きた出来事。あとから書き換えない。
      打ち消しイベント
      過去の出来事を取り消すために書き足す、逆向きの出来事。
      スナップショット
      ある時点の状態を保存したもの。再生をそこから始めて時間を短くする。
      CQRS
      Command Query Responsibility Segregation。書き込みと読み出しを別のモデルに分ける設計。
      読み取りモデル
      画面や用途に合わせて、イベントから作っておく読みやすい形のデータ。
      結果整合性
      一瞬ずれることはあっても、少し待てば必ず同じ内容にそろう性質。

      まとめ

      イベントソーシングは、今の値ではなく出来事の記録を正とし、再生して今を作る方式です。消さずに書き足すので、過去を再現でき、経緯を説明できます。CQRSは、その記録から読む係が画面ごとの表を作ることで、書き込みと読み出しをそれぞれ得意な形にします。代わりに、反映までの一瞬のずれと、仕組みの複雑さを受け入れることになります。