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

第65章:トランザクション管理 💳🔁✨

トランザクション管理

**複数の集約をいっぺんに更新するときの「事故らない作法」**です😊


まず結論:トランザクションって何?🤔

一言でいうと 「全部成功したら確定✅/途中でコケたら全部なかったことにする🧯」 仕組みです。

たとえば注文確定で…

  • 注文(Order)を「確定」にする ✅
  • 在庫(Inventory)を「引き当て」にする ✅

この2つが セットで必ず同時に成立してほしいですよね? 片方だけ成功すると、次みたいな地獄が起きます…😇🔥

  • 注文は確定したのに在庫が減ってない → 売りすぎ事故💥
  • 在庫は減ったのに注文が確定してない → 在庫だけ消える💥

DDDの超大事ルール:集約は「一貫性の境界」🧱✨

DDDでは基本こう考えます👇

  • **1トランザクションで守れる強い一貫性は、原則「1集約の中」**が安全 💪
  • 複数集約を1回で更新したくなるときは、設計が難しくなるゾーン⚠️

でも!現実は「複数集約を同時に更新したい」場面が普通にあります😊 だからこの章では **“迷わないための判断ルール”**を作ります🎯


迷わない判断ルール🚦✨(これだけ覚えてOK)

ルールA:同じDB・同じDbContextで、同時に確定したい

➡️ DBトランザクションでまとめてOK 😄 (EF Core の BeginTransactionAsync() を使うやつ) (Microsoft Learn)

ルールB:外部API・メール送信・別DBなど「DBの外」が混ざる

➡️ トランザクションで一気に確定しようとしない 🙅‍♀️ 代わりに👇

  • Outbox(送信箱)パターンで「あとで確実に送る」📮✨ (Microsoft Learn)
  • もしくは「後追いで整合」する(イベント駆動・最終的整合性)🕊️

EF Coreでの基本:どう書くのが“安全”?🧷😊

EF Core では DbContext.Database からトランザクションを開始できます。 (Microsoft Learn) **ポイントは「ユースケース(アプリケーションサービス)で囲う」**ことです✨ ドメイン層にトランザクションを持ち込まないのがキレイです🧼


実装例:注文確定(Order + Inventory)を1回で確定する 🛒📦✅

イメージ:

  • Order集約:注文を作って確定にする
  • Inventory集約:在庫を引き当てる
  • 最後にまとめて SaveChanges してコミット🎉
public sealed class PlaceOrderService
{
private readonly ShopDbContext _db;
private readonly IOrderRepository _orders;
private readonly IInventoryRepository _inventories;

public PlaceOrderService(ShopDbContext db, IOrderRepository orders, IInventoryRepository inventories)
{
_db = db;
_orders = orders;
_inventories = inventories;
}

public async Task<Result<OrderId>> PlaceAsync(PlaceOrderCommand cmd, CancellationToken ct)
{
await using var tx = await _db.Database.BeginTransactionAsync(ct);

try
{
// 1) 集約を用意してルールを適用 🧠✨
var order = Order.Create(cmd.CustomerId, cmd.Lines);

var inventory = await _inventories.GetAsync(cmd.Lines.Select(x => x.ProductId).ToList(), ct);
inventory.Reserve(order.Id, cmd.Lines); // 在庫引き当て(ルールは集約内に!)

// 2) 変更を登録 🧾
_orders.Add(order);
_inventories.Update(inventory);

// 3) まとめて保存してコミット 🎉
await _db.SaveChangesAsync(ct);
await tx.CommitAsync(ct);

return Result.Ok(order.Id);
}
catch (Exception ex)
{
// 途中でコケたら全部なかったことにする 🧯
await tx.RollbackAsync(ct);
return Result.Fail<OrderId>($"注文確定に失敗しました: {ex.Message}");
}
}
}

ここがえらいポイント🌟

  • トランザクションの開始・終了が ユースケース1つに対応してる ✅
  • ドメイン(Order/Inventory)の中に EFやトランザクションが出てこない
  • 例外が起きたらロールバックで 「中途半端」を残さない

よくある地雷💣(ここだけは避けて!)

❶ トランザクション中に外部APIを呼ぶ 🌐😱

  • 決済API、メール送信、WebHook… これをトランザクションの中でやると👇
  • トランザクションが長くなる
  • ロックが長引く
  • タイムアウトやデッドロックが増える で、運用がつらくなりがちです😭

➡️ 外部通信は **基本「コミット後」**に寄せましょう😊 どうしても「確実に送る」が必要なら Outbox へ📮✨(後述)

❷ DbContextを並列で使う(Task.WhenAllとか)🏃‍♀️🏃‍♀️💥

同じ DbContext で並列実行は危険です⚠️ EF Core は同一 DbContext の並列操作をサポートしないので、基本は「すぐawait」がお作法です。 (Microsoft Learn)


もう一歩:TransactionScope はいつ使う?🧵🤏

TransactionScope は「スコープに入ってる処理をまとめてトランザクションにする」道具です。 ただし async/awaitと一緒に使うなら設定が必須で、TransactionScopeAsyncFlowOption.Enabled が必要です。 (Microsoft Learn)

using var scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled);

// ここでDB処理など
// await ... (asyncでも流れる)

scope.Complete();

でも初心者のうちは、まずは EF Coreの BeginTransactionAsync をおすすめします😊 (TransactionScopeは状況によって分散トランザクション寄りの話になって急に難しくなるので…😵‍💫)


DBの外と「安全に仲良くする」:Outboxパターン 📮✨

「DB更新」と「イベント送信(メール・メッセージ・通知)」を同時に確実にやりたいとき、 “DBトランザクションだけ”では保証できません

そこで👇

  • DB更新と一緒に Outboxテーブルに“送る予定”を保存する(同じトランザクション)
  • コミット後、別プロセス/バックグラウンドがOutboxを読んで送信する

この考え方が Transactional Outbox です📮✨ (Microsoft Learn)

「DBには入ったのに、送信だけ失敗した…」を減らすための超定番お守りです🧿


AIに頼むときの “いい感じの指示” 💬🤖✨

そのままコピペで使えるやつ置いときます😊💕

① トランザクション境界つき実装を作らせる

  • 「ユースケース層でトランザクション開始」
  • 「ドメイン層にEF依存を混ぜない」
  • 「外部APIはトランザクションの外」 この3点を強めに言うのがコツです🧠✨
注文確定のユースケースをC# + EF Coreで実装して。
条件:
- トランザクションはアプリケーションサービスで BeginTransactionAsync を使って管理
- ドメイン層(集約)に DbContext / EF Core の型を出さない
- 外部API呼び出しはトランザクションの外に出す
- 失敗時はロールバックして Result パターンで返す

② 事故レビューをさせる(かなり効く✨)

このコードのトランザクション境界が危険な点を指摘して。
特に「外部通信」「トランザクションが長い」「DbContext並列」「複数SaveChanges」の観点でレビューして。
改善案も出して。

ミニ演習✍️🎓✨(15〜30分)

お題:購入でポイント付与 🎁⭐

購入確定で👇をやりたい!

  1. Order集約:注文を確定 ✅
  2. Customer集約:ポイント加算 ✅
  3. ついでに「購入完了メール」送信 ✉️

あなたならどうする?(答えは1つじゃないよ😊)

  • ① Order + Customer が同じDBなら

    • DBトランザクションでまとめる✅
  • ② メール送信は

    • コミット後に送る or Outboxに積む📮✨

今日のまとめ🍀✨

  • トランザクションは「全部OKか、全部なかったこと」🔁
  • DDDでは本来「1集約=一貫性の単位」🧱
  • でも現実は複数集約更新がある → だから判断ルール🚦
  • 同一DB内は BeginTransactionAsync が基本で安心 (Microsoft Learn)
  • 外部通信が混ざるなら Outbox で事故を減らす📮✨ (Microsoft Learn)

次の第66章(CQRS超入門)に行く前に、もしよければ✨ 「いま作りたいアプリの“複数集約更新っぽい処理”」を1つ挙げてくれたら、この章のルールで最短ルートの設計に落とし込みますよ😊🧠💖