メインコンテンツまでスキップ

第66章:CQRS(コマンドクエリ責務分離)の超入門🚦✨

CQRS(コマンドクエリ責務分離)の超入門

〜「読み込みはもっと自由でいい」ってどういうこと?〜📚💡


1. まず最初に:CQRSは“分けるだけ”です✂️🙂

CQRSは、難しい魔法じゃなくて…

  • Command(コマンド):データを“変える” 📥🛠️(登録・更新・削除)
  • Query(クエリ):データを“読むだけ” 👀📤(一覧・詳細・検索)

この2つを 分けて考える だけの考え方です✨


2. なんで分けるの?(1人開発だと特に効く)💪👩‍💻

✅ よくある悩み

「一覧画面を速くしたいからSQLをいじったら、更新処理が壊れた…😇」 「読み込み用に便利な形にしたら、ドメインのルールがぐちゃぐちゃ…🌀」

✅ CQRSのうれしさ

  • 書き込み(Command)は“ルール重視”でガチガチに守る🛡️
  • 読み込み(Query)は“画面に最適”で自由に作る🪽
  • AIに頼む時も、指示がブレにくくなる🤖✨ (“更新ロジック”と“一覧取得”が混ざるとAIも事故りやすい💥)

3. CommandとQueryの違い(ここだけ覚えればOK)🧠🌟

Command(変更する)🛠️

  • 目的:状態を変える(登録・更新・削除)
  • 成功/失敗が大事(バリデーション・ビジネスルール)
  • できれば「集約」や「ドメインルール」を通す

Query(読むだけ)👀

  • 目的:画面に必要なデータを返す
  • 副作用なし(読んだだけで何かが変わらない)
  • 速さ・見やすさ優先でOK JOINしてもいい、集計してもいい、DTO直でいい🙆‍♀️✨

4. “読み込みは自由でいい”の具体例🍱📌

たとえば「注文」の一覧画面で…

  • ドメイン(書き込み側)は「注文(Order)」をちゃんと守って設計する🛡️
  • 一覧表示は「注文日・合計金額・顧客名」が欲しいだけ👀

このとき Query は、 Order集約を全部復元してから計算… みたいなことしなくてOKです🙅‍♀️💦 DBから画面用DTOをズバッと取ってきてOK✨(軽い!速い!)


5. 最小CQRSをC#で作ってみよう(MediatRなし版)🧩✨

ここでは Command/Queryを「クラスで分ける」だけ の最小構成でいきます🙂 (MediatRの話は次章の楽しみに🎁)

想定:シンプルな「メモ」アプリ📝

  • メモを追加する(Command)
  • メモ一覧を表示する(Query)

6. ざっくり構成(イメージ)🗂️

  • Application

    • Commands(書き込み)
    • Queries(読み込み)
  • Infrastructure

    • DbContext / DBアクセス
  • Web

    • APIエンドポイント

7. 実装例:Command(追加する)➕🛠️

DTO(入力)

public sealed record CreateNoteRequest(string Title, string Body);

Command(意図を表す)

public sealed record CreateNoteCommand(string Title, string Body);

結果(戻り値)

public sealed record CreateNoteResult(Guid NoteId);

Handler(書き込み処理)

using Microsoft.EntityFrameworkCore;

public sealed class CreateNoteHandler
{
private readonly AppDbContext _db;

public CreateNoteHandler(AppDbContext db) => _db = db;

public async Task<CreateNoteResult> HandleAsync(CreateNoteCommand command, CancellationToken ct)
{
// ⭐ここが「ルールを守る場所」:最低限のバリデーション
if (string.IsNullOrWhiteSpace(command.Title))
throw new ArgumentException("Title is required.");

var note = new Note
{
Id = Guid.NewGuid(),
Title = command.Title.Trim(),
Body = command.Body ?? "",
CreatedAtUtc = DateTime.UtcNow
};

_db.Notes.Add(note);
await _db.SaveChangesAsync(ct);

return new CreateNoteResult(note.Id);
}
}

8. 実装例:Query(一覧を読む)📄👀

Query側は 画面に最適なDTO を返すのがコツです✨

一覧用DTO

public sealed record NoteListItemDto(Guid Id, string Title, DateTime CreatedAtUtc);

Query(意図)

public sealed record GetNoteListQuery(int Take = 50);

Query Handler(読み込み処理:自由に最適化OK🪽)

using Microsoft.EntityFrameworkCore;

public sealed class GetNoteListHandler
{
private readonly AppDbContext _db;

public GetNoteListHandler(AppDbContext db) => _db = db;

public async Task<IReadOnlyList<NoteListItemDto>> HandleAsync(GetNoteListQuery query, CancellationToken ct)
{
// ⭐読み取り専用:AsNoTracking()で軽くする✨
return await _db.Notes
.AsNoTracking()
.OrderByDescending(x => x.CreatedAtUtc)
.Take(Math.Clamp(query.Take, 1, 200))
.Select(x => new NoteListItemDto(x.Id, x.Title, x.CreatedAtUtc))
.ToListAsync(ct);
}
}

ポイント🎯

  • Queryは「Note(ドメイン)」を返さなくてOK🙆‍♀️
  • 画面に必要な形(DTO)に“直で”してOK✨
  • AsNoTracking() は読み込み最適化の定番です🚀

9. Web側(最小APIのイメージ)🌐✨

using Microsoft.AspNetCore.Mvc;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddDbContext<AppDbContext>();
builder.Services.AddScoped<CreateNoteHandler>();
builder.Services.AddScoped<GetNoteListHandler>();

var app = builder.Build();

app.MapPost("/notes", async (
[FromBody] CreateNoteRequest req,
CreateNoteHandler handler,
CancellationToken ct) =>
{
var result = await handler.HandleAsync(new CreateNoteCommand(req.Title, req.Body), ct);
return Results.Created($"/notes/{result.NoteId}", result);
});

app.MapGet("/notes", async (
[FromQuery] int take,
GetNoteListHandler handler,
CancellationToken ct) =>
{
var items = await handler.HandleAsync(new GetNoteListQuery(take == 0 ? 50 : take), ct);
return Results.Ok(items);
});

app.Run();

10. AIに頼むときの“事故らない”お願いの仕方🤖💬✨

✅ Commandを作らせたいとき(ルール重視🛡️)

  • 「Titleは必須」
  • 「CreatedAtUtcはサーバーで付与」
  • 「保存後にIDを返す」
  • 「例外 or Resultパターンどっちで返すか(ここでは例外)」

📝プロンプト例 「CreateNoteCommandとHandlerを作って。Title必須。Trimする。CreatedAtUtcはUTC。SaveChangesしてNoteIdを返す。テストも付けて」

✅ Queryを作らせたいとき(画面最適🪽)

  • 「一覧はDTOで返す」
  • 「AsNoTracking」
  • 「並び順」
  • 「Take上限」

📝プロンプト例 「GetNoteListQueryとHandlerを作って。AsNoTracking。CreatedAtUtc降順。Takeは1〜200に丸める。戻り値はNoteListItemDtoのList」


11. よくある落とし穴😵‍💫(ここだけ注意!)

❌ Queryで更新してしまう

「ついでに閲覧回数を増やす」とかやりがち😇 → それ、QueryじゃなくてCommandに分けよう🛑

❌ Commandが“画面都合”に引っ張られる

「一覧で必要だから、ドメインにも表示用プロパティ増やそ!」 → それ、Query側DTOで解決できること多いよ🙆‍♀️✨


12. ミニ演習🎮📝(30〜60分)

演習A:検索を追加しよう🔎✨

  • GET /notes?keyword=xxx でタイトルに含むものだけ返す
  • Query側だけでOK(Commandは触らない)

ヒント:Where(x => x.Title.Contains(keyword))

演習B:詳細画面用Queryを作ろう📌

  • GET /notes/{id} で詳細DTOを返す
  • DTOは「Title/Body/CreatedAtUtc」

演習C:Commandに“ルール”を1つ足そう🛡️

  • Titleは最大50文字まで 超えたら例外(or Result)にする

13. まとめ🎉

  • CQRSは「更新と参照を分ける」だけ✂️✨
  • Commandは堅く(ルール最優先)🛡️
  • Queryは自由に(画面・速度優先)🪽
  • 1人開発+AI時代だと、分けるだけで事故率がグッと下がる🤖💖

ちなみに最新のC# 14は .NET 10 / Visual Studio 2026 で試せます(2025年11月ごろリリース)。(Microsoft Learn)


次の第67章では、いよいよ MediatRを使うべき?使わないべき? を「1人開発のデバッグしやすさ」でガチ判断していくよ〜😆🔧