Skip to main content

第28章:Projectionって何?(読みやすい形を別で作る)🔎✨

この章のゴール🎯💕

  • 「イベントの履歴」から、画面や検索に使いやすい“読み取り用の形”を作れるようになる🪄
  • 書き込み(Command側)と読み取り(Query側)を分ける理由が、ふわっとじゃなく説明できるようになる😊
  • 最小の Projection(読みモデル)C#で動く形に落とすところまでやる💻✨

1) Projectionってなに?🧁🔎

イベントソーシングでは「真実」は イベント(出来事の履歴) だよね📜✨ でも…イベント列って、そのままだと 画面表示や検索に向いてないことが多いの🥺💦

そこで登場するのが Projection(投影) 🎥✨ ざっくり言うと:

  • イベント列(事実の履歴) → 見たい形に 集計・整形読み取り用データ(Read Model) を作る
  • この「イベントから読みやすい形を作る処理」や、その結果できた「読み取り用の形」をまとめて Projection と呼ぶことが多いよ🧠✨

「イベントからビュー(見やすい形)を materialize(具体化)する」って説明は、Microsoftのパターン解説にも出てくる考え方だよ📚✨ (Microsoft Learn)


2) なんで必要なの?(Projectionが欲しくなる瞬間)😵‍💫➡️😊

たとえば「カート」のイベントがこう積まれてるとするね🛒📮

  • CartCreated
  • ItemAdded (りんご, 1, 120円)
  • ItemAdded (バナナ, 2, 200円)
  • QuantityChanged (バナナ, 3)
  • ItemRemoved (りんご)

ここで画面が欲しいのは例えば👇みたいなやつ:

  • カート一覧:合計金額 / 商品数 / 最終更新日時
  • カート詳細:商品ごとの数量 / 小計 / 合計
  • 検索:「バナナを含むカート」だけ出して!

でもイベント列を毎回ぜんぶ読んで毎回計算するのって、だるいし遅いし事故りやすい😇💦 だから イベントから“表示に最適な形”を作って保存しておく(=Projection)と楽になるの🎀

この「書き込みモデル(厳密)と読みモデル(表示最適)」を分ける話は、CQRSの定番の考え方としても整理されてるよ📌 (Microsoft Learn)


3) 書き込みモデル🛡️ vs 読みモデル👀(ここ超大事!)✨

読み書きの分離

書き込みモデル(Write Model)📮🧱

  • 不変条件を守る(例:数量がマイナスは禁止❌)
  • Command → Decide → Event を作る場所
  • “正しさ最優先”💎

読みモデル(Read Model / Projection)🔎🍱

  • 画面・検索・一覧のために 読みやすく整形する場所
  • 正しさ(ルール)は基本的に書き込み側で保証して、読み側は 表示最適に寄せる🌷
  • “速さ・使いやすさ優先”⚡

MicrosoftのCQRS解説でも「イベントが読みモデルの入力になって、クエリのためのスナップ(ビュー)を作れる」って趣旨が書かれてるよ🧠✨ (Microsoft Learn)


4) Projection設計のコツ:イベントから作るんじゃなく、画面から逆算🪞✨

初心者がやりがちなミス: 「イベントを見て、なんとなくProjection作る」😵‍💫

おすすめは逆! 画面(Query)→ 必要な項目 → 形(Read Model)→ 更新方法 の順だよ💡

手順(テンプレ)🧩

  1. 画面・APIで欲しいものを列挙🖥️📝
  2. それに合わせた Read Model を1つ決める🍱
  3. どのイベントで、どの項目が変わるか対応表を作る🔁
  4. Projector(イベントを受けてRead Modelを更新する処理)を書く🧰

5) ミニ演習:画面に必要な項目を列挙してみよう🖥️💗

読みモデルの設計イメージ

お題:カート一覧(1行で見たい)📋✨

最低限、何が欲しい?👇

  • CartId
  • TotalItems(商品数)
  • TotalPrice(合計)
  • LastUpdatedAt

お題:カート詳細(中身を見たい)🔎🛒

  • CartId
  • Items[](商品名、単価、数量、小計)
  • TotalPrice
  • LastUpdatedAt

この2つは「欲しいクエリ」が違うから、Read Modelも別でOKだよ💞 (Projectionは “質問ごとに形が違っていい” のが強み🌟)


6) 最小実装:イベントを受けてProjectionを更新する⚡🧪

ここでは「カート一覧用Projection」だけを最小で作るよ📦✨ (永続化はまだ気にしないでOK! まず動かすのが勝ち🏆)

6-1. イベント定義(例)📮

using System;

public interface IDomainEvent
{
Guid CartId { get; }
DateTimeOffset OccurredAt { get; }
}

// 例:作成
public sealed record CartCreated(Guid CartId, DateTimeOffset OccurredAt) : IDomainEvent;

// 例:商品追加
public sealed record ItemAdded(
Guid CartId,
string Sku,
int Quantity,
int UnitPriceYen,
DateTimeOffset OccurredAt
) : IDomainEvent;

// 例:数量変更
public sealed record QuantityChanged(
Guid CartId,
string Sku,
int NewQuantity,
int UnitPriceYen,
DateTimeOffset OccurredAt
) : IDomainEvent;

// 例:商品削除
public sealed record ItemRemoved(Guid CartId, string Sku, DateTimeOffset OccurredAt) : IDomainEvent;

6-2. Read Model(カート一覧の1行)📋

using System;

public sealed class CartSummaryReadModel
{
public Guid CartId { get; init; }
public int TotalItems { get; set; }
public int TotalPriceYen { get; set; }
public DateTimeOffset LastUpdatedAt { get; set; }
}

6-3. Projector(イベント→Read Model更新)🔁🧰

「Read Model専用の更新処理」って感じだよ😊✨

using System;
using System.Collections.Generic;

public sealed class CartSummaryProjector
{
private readonly Dictionary<Guid, CartSummaryReadModel> _store = new();

// 取得(Query側が使うイメージ)
public CartSummaryReadModel? Find(Guid cartId)
=> _store.TryGetValue(cartId, out var rm) ? rm : null;

// ここが「Projection更新」
public void Apply(IDomainEvent ev)
{
switch (ev)
{
case CartCreated e:
_store[e.CartId] = new CartSummaryReadModel
{
CartId = e.CartId,
TotalItems = 0,
TotalPriceYen = 0,
LastUpdatedAt = e.OccurredAt
};
break;

case ItemAdded e:
{
var rm = Ensure(e.CartId, e.OccurredAt);
rm.TotalItems += e.Quantity;
rm.TotalPriceYen += e.Quantity * e.UnitPriceYen;
rm.LastUpdatedAt = e.OccurredAt;
break;
}

case QuantityChanged e:
{
// ⚠️ 本当は「以前の数量」を知らないと差分計算できない
// → ここが “一覧Projectionだけだと情報が足りない” 例!
// 今回は最小章なので「詳細Projectionで数量を持つ」前提にして、ここは割愛/例示にする✨
var rm = Ensure(e.CartId, e.OccurredAt);
rm.LastUpdatedAt = e.OccurredAt;
break;
}

case ItemRemoved e:
{
var rm = Ensure(e.CartId, e.OccurredAt);
rm.LastUpdatedAt = e.OccurredAt;
break;
}

default:
throw new NotSupportedException($"Unknown event type: {ev.GetType().Name}");
}
}

private CartSummaryReadModel Ensure(Guid cartId, DateTimeOffset at)
{
if (_store.TryGetValue(cartId, out var rm)) return rm;

// 最小実装:作成イベントが来る前提だけど、保険で作っちゃう
rm = new CartSummaryReadModel { CartId = cartId, LastUpdatedAt = at };
_store[cartId] = rm;
return rm;
}
}

🔥ここで大事ポイント

QuantityChangedItemRemoved を正しく一覧に反映するには、「その商品の現在数量」みたいな状態が必要になることが多いのね。 だから実務ではよく👇のどっちかをやるよ💡

  • 詳細Projection(Itemsを持つ)を作って、そこから合計も作る
  • もしくは 一覧Projectionの内部に最小の状態(SKU→数量)を持たせる

Projectionは「質問に答えるために、必要な最小状態を持ってOK」って感覚が大事だよ🍬✨ (イベントは真実、Projectionは“答えを作るための計算結果”)


7) テストで「Projectionが更新される」を確認🧪✅

Projectionはバグると画面が壊れるので、軽いテストがめちゃ効くよ〜!💕

using System;
using Xunit;

public class CartSummaryProjectorTests
{
[Fact]
public void Apply_creates_and_updates_summary()
{
var projector = new CartSummaryProjector();
var cartId = Guid.NewGuid();
var t1 = DateTimeOffset.Parse("2026-02-01T10:00:00+09:00");
var t2 = DateTimeOffset.Parse("2026-02-01T10:01:00+09:00");

projector.Apply(new CartCreated(cartId, t1));
projector.Apply(new ItemAdded(cartId, "APPLE", 2, 120, t2));

var rm = projector.Find(cartId)!;

Assert.Equal(cartId, rm.CartId);
Assert.Equal(2, rm.TotalItems);
Assert.Equal(240, rm.TotalPriceYen);
Assert.Equal(t2, rm.LastUpdatedAt);
}
}

8) Projectionあるある落とし穴🚧🥺(先に知っておくと強い)

① 読みモデルにビジネスルールを入れちゃう😇

  • ルールは基本 Write Model側で守る
  • 読み側は「見せ方」中心🎀

② 更新漏れ(イベント追加したのにProjection更新し忘れ)🙃

  • イベント追加=Projectionの Apply も見直しが必要✅
  • 「イベント追加したら、Projectionとテストもセット」って癖づけると事故減るよ🧸✨

③ 同じイベントを2回食べると壊れる(重複)🔁💥

  • 実務では チェックポイント(どこまで処理したか) を持って重複を避けることが多いよ
  • 「最後に処理した位置(position)」の考え方は、投影の定番テクとしてよく出てくるよ📌 (Kurrent - event-native data platform)

9) AI活用プロンプト例🤖✨(丸投げじゃなく“設計を早くする”)

画面→Projection設計を出してもらう🪄

  • 「カート一覧画面に必要な項目はこれ。イベントはこれ。 どんなRead Modelが良い?フィールド案と更新ルール案を出して!」

Apply漏れをレビューしてもらう🔍

  • 「このProjectorのswitchを見て、更新漏れ・矛盾・将来バグりそうな点を指摘して!」

テスト観点を増やす🧪

  • 「成功ケース2つ、失敗(または未対応イベント)ケース1つのテスト案をGiven-When-Thenで提案して!」

10) 2026年時点のメモ🗒️✨(“今の最新版”)

  • **.NET 10 はLTS(長期サポート)**で、サポート期限も明示されてるよ📅 (Microsoft for Developers)
  • Visual Studio 2026 は 2026年1月時点でも更新が出ていて、リリースノートがまとまってる📌 (Microsoft Learn)
  • VS Code 側も 2026年1月の更新内容が公開されてるよ🧩 (Visual Studio Code)

まとめ🍓✨

  • Projection=イベントから「見たい答えの形」を作る仕組み🔎
  • 設計は イベント起点じゃなく、画面(クエリ)起点がラク🖥️💕
  • 最小でも Projector(Apply)+Read Model+テスト1本 があると一気に安心する🧪✅