CQRSとイベントソーシング — 残高を書かずに、明細を再生する
銀行口座の残高をデータベースに保存するとき、ふつうは「残高 700円」という今の値を上書きしていきます。シンプルですが、上書きした瞬間に前の値と、なぜそうなったかは消えてしまいます。
イベントソーシングは逆の発想です。残高を保存せず、「入金した」「出金した」という出来事(イベント)だけを、順番に書き足していく。今の残高は、その記録を最初から再生して計算します。通帳の明細だけを持っておき、残高は明細を足し算して出すイメージです。図1で、出来事を積み上げ、時間をさかのぼってみてください。
消さずに、書き足す
「↩ 直前の出金を取り消す」を押しても、記録の中の出金は消えません。代わりに「出金を取り消した(+300円)」という新しい出来事が書き足されます。会計の帳簿で、間違えた行を消しゴムで消さず、赤字で打ち消しの行を書くのと同じです。
記録が全部残っているので、スライダーで3件目の時点の残高のように過去を正確に再現できます。ふつうのDBにはこれができません。「なぜ今700円なのか」を説明できるのは、記録を持っている側だけです。お金や在庫のように経緯の説明が求められるデータで、とくに役に立ちます。
ただし、イベントが何百万件もたまると、毎回最初から再生するのは大変です。そこで「1000件目の時点の残高は○○円」というスナップショットをときどき保存し、そこから先だけを再生します。
書く係と、読む係を分ける — CQRS
イベントの記録は「書き足す」のは得意ですが、「今の残高を見せて」「大きな出金だけ一覧にして」といった読み出しには向いていません。毎回再生していたら遅すぎます。そこで、書き込み(コマンド)と読み出し(クエリ)を別の係に分けるのがCQRSです。読む係は、イベントが届くたびに画面ごとに読みやすい形の表(読み取りモデル)を作っておきます。図2で試してください。
書いた直後は、読む係の表にまだ反映されていない瞬間があります(「反映待ち」)。少し待てば必ず追いつく、という性質を結果整合性と呼びます。そのかわり、読む係は画面に合わせた形で表を持てるので速く、あとから新しい画面が必要になっても、記録を最初から再生すれば過去の分までそろった表が作れます。
- イベントソーシング
- 今の状態ではなく、状態を変えた出来事を順番に保存し、再生して今の状態を作る方式。
- イベント
- 「入金した」のように、すでに起きた出来事。あとから書き換えない。
- 打ち消しイベント
- 過去の出来事を取り消すために書き足す、逆向きの出来事。
- スナップショット
- ある時点の状態を保存したもの。再生をそこから始めて時間を短くする。
- CQRS
- Command Query Responsibility Segregation。書き込みと読み出しを別のモデルに分ける設計。
- 読み取りモデル
- 画面や用途に合わせて、イベントから作っておく読みやすい形のデータ。
- 結果整合性
- 一瞬ずれることはあっても、少し待てば必ず同じ内容にそろう性質。
まとめ
イベントソーシングは、今の値ではなく出来事の記録を正とし、再生して今を作る方式です。消さずに書き足すので、過去を再現でき、経緯を説明できます。CQRSは、その記録から読む係が画面ごとの表を作ることで、書き込みと読み出しをそれぞれ得意な形にします。代わりに、反映までの一瞬のずれと、仕組みの複雑さを受け入れることになります。