第25章:トランザクション境界の置き場所(結論:アプリ層)🎬🔒
1. 今日のゴール🎯✨
この章が終わると、こんなことができるようになります👇💡
- 「どこでトランザクションを開始・確定(コミット)するか」を迷わなくなる🔒
- SaveChanges の置き場所=境界の置き場所だと腹落ちする😆
- Domain(ドメイン)をキレイに保ったまま、アプリ層で安全に完結できる✅
2. まず超ざっくり:トランザクション境界って何?🧱
トランザクション境界は、ひとことで言うと👇
- 「このユースケース(1回の操作)で、全部成功か全部失敗かを保証する線」🔒✨
たとえば「注文確定」って操作で…
- 注文明細は増えたけど、注文ステータスが変わってない みたいな “半端な状態” が残ると事故です💥😵
だから境界が必要なんだよね、という話です🫶
3. 結論:境界は “アプリ層(ユースケース)” に置く🎬🔒

なぜアプリ層がベスト?🤔✨
**アプリ層(Application Service / UseCase)**は、ユースケースの進行役🎬 だから、ここに境界を置くときれいにまとまります👇
-
その操作(ユースケース)で「何をやるか」を知っている🧠
-
Domain を呼び出して、必要な保存をまとめられる📦
-
「ここで確定(コミット)!」を決められる✅
-
Domain が DB やトランザクションを知りはじめて汚れやすい(依存が逆流)🧽🚫
-
テストがやりにくくなる(DB前提の設計になりがち)🧪💦
4. 最新の前提(超大事な事実だけ)🧠✨
ここだけは「今の最新事情」として押さえると設計がラクになります👇
✅ SaveChanges は “それ単体で” 原則トランザクションになる
EF Core は、基本的に 1回の SaveChanges をトランザクションで包んでくれます(失敗したらロールバック)🔒✅ つまり、ユースケース内で SaveChanges を1回にできるなら、明示トランザクションなしで安全にいける場面が多いです✨ (Microsoft Learn)
✅ いまの主流は .NET 10 + EF Core 10(LTS)
最新の安定(LTS)として .NET 10 と EF Core 10 が提供されています📦🚀 (どちらも 2025年11月リリース、サポートは 2028年11月までの案内) (Microsoft)
5. “置き場所”を一発で決める合言葉📌✨
迷ったらこれだけ覚えてください👇
「SaveChanges はユースケースの最後に1回」 💾✅
- 置き場所:Application Service(UseCase) 🎬
- 理由:それが「1ユースケース=1境界」を形にする最短ルート🌈
6. 正しい形:アプリ層に境界を置いた実装例(最小)☕️📦
ここでは「注文確定 PlaceOrder」を例にします☕️🧁 ポイントは Domain はトランザクションを知らない、アプリ層が締める、です🔒✨
6-1. Domain(トランザクションを知らない)🌳
// Domain: 値オブジェクト例(雑に見えても「不正値を作らせない」が大事)
public readonly record struct OrderId(Guid Value);
public sealed class Order
{
public OrderId Id { get; }
public string Status { get; private set; } = "Draft";
private readonly List<OrderItem> _items = new();
public IReadOnlyList<OrderItem> Items => _items;
public Order(OrderId id)
{
Id = id;
}
public void AddItem(string name, int quantity)
{
if (Status != "Draft") throw new InvalidOperationException("確定後は追加できません");
if (string.IsNullOrWhiteSpace(name)) throw new ArgumentException("商品名が空です");
if (quantity <= 0) throw new ArgumentException("数量が不正です");
_items.Add(new OrderItem(name, quantity));
}
public void Confirm()
{
if (_items.Count == 0) throw new InvalidOperationException("明細が空の注文は確定できません");
Status = "Confirmed";
}
}
public sealed class OrderItem
{
public string Name { get; }
public int Quantity { get; }
public OrderItem(string name, int quantity)
{
Name = name;
Quantity = quantity;
}
}
6-2. アプリ層(ここが境界🎬🔒)
public sealed class PlaceOrderUseCase
{
private readonly OrderDbContext _db;
public PlaceOrderUseCase(OrderDbContext db)
{
_db = db;
}
public async Task<Guid> HandleAsync(string itemName, int quantity, CancellationToken ct = default)
{
// 1) ドメインを組み立てる(ここは純粋に業務ルール)
var order = new Order(new OrderId(Guid.NewGuid()));
order.AddItem(itemName, quantity);
order.Confirm();
// 2) 追加する(Repositoryを挟んでもOK。ここでは最小で DbContext 直)
_db.Orders.Add(order);
// 3) ここで「確定」=このユースケースのトランザクション境界💾✅
await _db.SaveChangesAsync(ct);
return order.Id.Value;
}
}
🔎 ここでの重要ポイント
- 「確定」は最後に1回だけ(SaveChanges 1回)💾✅
- だから 暗黙トランザクションに任せても安全 な場面が多いよ、という話です🔒 (Microsoft Learn)
7. 「明示トランザクション」が必要になるのはどんな時?🔒🧠
基本は SaveChanges 1回でOKなんだけど、例外もあります👇
✅ 明示トランザクションを切る代表例
- SaveChanges が2回以上必要(途中でDBの都合で確定しないといけない等)💾💾
- 同じトランザクション内で、DBコマンド+SaveChanges を混ぜたい(生SQLも含む)🧩
- 分離レベル(IsolationLevel)を変えたい⚖️
- “ここは絶対まとめたい” という強い理由がある🔥
EF Core 公式でも「複数の SaveChanges を同一トランザクションで」みたいな例が出ています📘 (Microsoft Learn)
7-1. 明示トランザクション例(複数 SaveChanges をまとめる)🔒
public async Task<Guid> HandleWithExplicitTxAsync(string itemName, int quantity, CancellationToken ct = default)
{
await using var tx = await _db.Database.BeginTransactionAsync(ct);
try
{
var order = new Order(new OrderId(Guid.NewGuid()));
order.AddItem(itemName, quantity);
_db.Orders.Add(order);
await _db.SaveChangesAsync(ct); // ①
order.Confirm();
await _db.SaveChangesAsync(ct); // ②
await tx.CommitAsync(ct);
return order.Id.Value;
}
catch
{
await tx.RollbackAsync(ct);
throw;
}
}
この形は “やっていい” けど、初心者のうちはまず👇を優先してOKです😊
- SaveChanges を1回に寄せられない? を先に考える💡
8. やりがち事故💥😵(これだけは回避!)
🚫 事故①:Domain の中で SaveChanges しはじめる
- 「Domainメソッド内で DbContext を触る」みたいな状態は、設計が崩れやすいです🧨
- 理由:Domain がインフラ依存になって、保守もテストもキツくなる😇💦
🚫 事故②:Repository が勝手に SaveChanges する
- Repository が毎回 SaveChanges すると、ユースケース単位でまとめられない💔
- 結果:「注文追加は成功したけど、確定は失敗」みたいな半端が出る💥
👉 だから基本は👇
- Repository:出し入れ(Add / Get)まで🏪
- SaveChanges:UseCase の最後に1回💾✅
🚫 事故③:トランザクション中に外部I/O(API呼び出し)をする
- 決済API、メール送信、外部HTTP…をトランザクションの中でやると 遅い/詰まる/リトライで二重実行 など地獄になりがち😱🌪️
このへんは後半の「イベント/Outbox」に繋がります📮✨(第31-32章へ)
9. TransactionScope はどうなの?🌀
TransactionScope(アンビエントトランザクション)は便利だけど、初心者のうちは EF Core の BeginTransaction を基本にすると安全です😊
- EF Core は System.Transactions をプロバイダが対応していないと効かないことがある、など注意点あり⚠️ (Microsoft Learn)
「複数コンテキストをまたぐ」「複数箇所の呼び出しをまとめたい」みたいな強い理由が出たら検討でOKです👌
10. AI(Copilot / Codex)に手伝ってもらうコツ🤖✨
“丸投げ”じゃなくて、レビュー相手として使うのが超おすすめです🫶
✅ そのまま使えるプロンプト例(コピペOK)✍️
- 「このユースケースの SaveChanges の位置は正しい?トランザクション境界の観点で指摘して」🔎
- 「Repository に SaveChanges が混ざってないかチェックして、修正案を出して」🧹
- 「SaveChanges を1回に寄せるリファクタ案を3つ(メリデメ付き)で」📌
- 「明示トランザクションが本当に必要か、根拠付きで判定して」⚖️
11. ミニ演習(10分)⏱️🎀
演習A:SaveChanges を “最後に1回” に直そう💾✅
次の “ダメ例” を、UseCaseの最後に1回へ直してみてね👇
public sealed class BadOrderRepository
{
private readonly OrderDbContext _db;
public BadOrderRepository(OrderDbContext db) => _db = db;
public async Task AddAsync(Order order, CancellationToken ct)
{
_db.Orders.Add(order);
await _db.SaveChangesAsync(ct); // 🚫 ここで確定しちゃう
}
}
ゴール🎯
- Repository から SaveChanges を消す🧹
- UseCase が最後にまとめて SaveChanges 💾✅
12. まとめ(この章の合言葉)📌✨
- トランザクション境界はアプリ層(UseCase)に置く🎬🔒
- SaveChanges はユースケースの最後に1回💾✅
- EF Core は SaveChanges 1回なら暗黙トランザクションで守ってくれる(失敗はロールバック)🔒 (Microsoft Learn)
- 明示トランザクションは「複数 SaveChanges」など 理由がある時だけ👌 (Microsoft Learn)