第05章:トランザクション超入門(“全部成功 or 全部失敗”)🔒🍙
今日のゴール 🎯✨
- Commit(確定) と Rollback(取り消し) の感覚をつかむ😺
- 「同じトランザクションに入れる」がどういう意味か、体で覚える💪
- Outboxで一番大事な「DB更新とOutbox追加を“同時に確定”」の土台を作る🏗️📦
1) トランザクションってなに?🍙🔒
トランザクションは、ひとことで言うと👇
複数のDB操作を “ひとまとまり” にして、 全部できたら確定(Commit)/途中で失敗したら全部なかったこと(Rollback) にする仕組み✨
たとえば「注文を作る」って処理は、実はDBの中で色々やりたいよね?🛒
- Orders に注文行を追加する🧾
- OutboxMessages に「注文が作られたよ」メッセージを追加する📦📩
この2つ、片方だけ成功すると事故ります😱 だから、**“2つまとめて成功 or 2つまとめて失敗”**にしたい → それがトランザクション🔒🍙
2) Commit / Rollback をゲームで覚える🎮✨
- Commit(コミット) = セーブ完了💾✅
- Rollback(ロールバック) = セーブせずに終了、なかったことにする🔙💥
「最後までノーミスならセーブ」 「途中でバグったらロードして戻す」 この感覚がそのままDB版になったイメージだよ〜😺✨
3) 「同じトランザクションに入れる」ってどういうこと?🧠🔗

これ、言い換えるとこう👇
同じ“お弁当箱”の中に入れる🍙 → ふたを閉める(Commit)まで、外には確定として見せない → こぼしたら箱ごと捨てる(Rollback)
Outboxでは特に👇が重要💡
- 業務テーブル(例:Orders)
- Outboxテーブル(例:OutboxMessages)
この2つを 同じトランザクション に入れることで、 「注文だけ入ったのに通知が残ってない😭」みたいなズレが起きにくくなるよ🛡️📦
4) 先に知っておくと安心:EF Core の “デフォルト安全” 🧯✨
実は EF Core は、基本こう動くよ👇
SaveChanges()1回の中の変更は、トランザクションでまとめて処理される- だから 途中で失敗したら、全部ロールバックされてDBは無傷 🧡
つまり、普通は「SaveChanges() 1回で済むなら」それだけでも安全寄り👍
(※ただし、Outboxでは「複数回SaveChanges」や「生SQL混在」なども出てくるから、手動トランザクションを覚える価値が大きいよ!)(Microsoft Learn)
5) 手を動かそう:Orders と Outbox を “同時に確定” する🏃♀️💨📦
ここではミニ構成で👇をやるよ✨
- Orders に追加🧾
- OutboxMessages に追加📦
- 同じトランザクションで Commit 🔒✅
(この教材のコードは、今の主流の .NET 10 / C# 14 を想定してるよ)(Microsoft)
5-1) NuGet(パッケージ)📦
最低限これ👇
Microsoft.EntityFrameworkCoreMicrosoft.EntityFrameworkCore.SqlServer
5-2) モデル&DbContext を用意 🧩✨
using System.Text.Json;
using Microsoft.EntityFrameworkCore;
public sealed class AppDbContext : DbContext
{
public DbSet<Order> Orders => Set<Order>();
public DbSet<OutboxMessage> OutboxMessages => Set<OutboxMessage>();
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }
}
public sealed class Order
{
public Guid Id { get; set; } = Guid.NewGuid();
public string CustomerEmail { get; set; } = "";
public decimal Amount { get; set; }
public DateTimeOffset CreatedAt { get; set; } = DateTimeOffset.UtcNow;
}
public sealed class OutboxMessage
{
public Guid Id { get; set; } = Guid.NewGuid();
public string Type { get; set; } = "";
public string Payload { get; set; } = "";
public DateTimeOffset OccurredAt { get; set; } = DateTimeOffset.UtcNow;
public static OutboxMessage Create(string type, string payloadJson) =>
new OutboxMessage { Type = type, Payload = payloadJson, OccurredAt = DateTimeOffset.UtcNow };
}
5-3) “同じトランザクション” で2つ書いて Commit 🔒🍙✅
using Microsoft.EntityFrameworkCore;
// 例:SQL Server LocalDB(Windowsで手軽に使いやすい構成)
var connectionString =
"Server=(localdb)\\MSSQLLocalDB;Database=OutboxChapter5Demo;Trusted_Connection=True;TrustServerCertificate=True;";
var options = new DbContextOptionsBuilder<AppDbContext>()
.UseSqlServer(connectionString)
.Options;
await using var db = new AppDbContext(options);
// 章5では “動かして体感” が目的なので、サクッと作る(本番は Migration 推奨)
await db.Database.EnsureCreatedAsync();
await using var tx = await db.Database.BeginTransactionAsync(); // ← ここが「お弁当箱🍙」
try
{
// ① 業務データ(注文)を追加🧾
var order = new Order
{
CustomerEmail = "alice@example.com",
Amount = 1200m
};
db.Orders.Add(order);
// ② Outbox に「起きた事実」を積む📦
var payload = JsonSerializer.Serialize(new
{
orderId = order.Id,
order.CustomerEmail,
order.Amount
});
db.OutboxMessages.Add(OutboxMessage.Create(
type: "OrderCreated.v1",
payloadJson: payload
));
// ③ まとめて保存(ここで Orders + OutboxMessages が一緒にDBへ)🧠✨
await db.SaveChangesAsync();
// ④ 最後に確定!✅
await tx.CommitAsync();
Console.WriteLine("✅ Commit 完了! Orders と OutboxMessages が同時に確定したよ🎉");
}
catch (Exception ex)
{
// 途中で失敗したら全部なかったことにする💥
await tx.RollbackAsync();
Console.WriteLine($"💥 Rollback! 理由: {ex.Message}");
}
✅ これが「同じトランザクションに入れる」の最小形だよ〜!📦🍙
6) Rollback を “体感” する実ufmer3 👀💥
「ほんとに戻るの?」を体験しよ✨
わざと SaveChangesAsync() の後に例外を投げてみるよ👇
await using var tx = await db.Database.BeginTransactionAsync();
try
{
// ①②③は同じ(Orders と OutboxMessages を追加して SaveChanges)
// ...
await db.SaveChangesAsync();
// わざと落とす💥(たとえば通知処理で例外が出た想定)
throw new InvalidOperationException("わざと落としたよ😇");
// await tx.CommitAsync(); ← ここまで到達しない
}
catch
{
await tx.RollbackAsync();
}
// 別コンテキストで確認(トラッキングの影響を避ける)👀
await using var db2 = new AppDbContext(options);
var ordersCount = await db2.Orders.CountAsync();
var outboxCount = await db2.OutboxMessages.CountAsync();
Console.WriteLine($"Orders: {ordersCount}, Outbox: {outboxCount} (両方0なら成功✨)");
ポイント💡
SaveChangesAsync()まで行ってても、Commit してないなら確定じゃない- Rollback すれば、Orders も Outbox も一緒に消える🧹✨
7) TransactionScope っていつ使うの?🧠🔭
EF Core の BeginTransaction() は **「そのDbContext/接続の範囲」**で分かりやすい👍
でも、たとえば👇みたいに 複数の技術をまたぐときに TransactionScope が出てくることがあるよ🧩
- ADO.NET の生SQL + EF Core を同じ取引に入れたい
- 複数のDB操作を「周囲の大きなスコープ」でまとめたい
ただし注意⚠️
async/awaitするなら、AsyncFlowOption を Enabledにしないと “トランザクションが流れない” 事故が起きがち😱(Microsoft Learn)
ミニ例👇
using System.Transactions;
using var scope = new TransactionScope(
TransactionScopeOption.Required,
new TransactionOptions { IsolationLevel = IsolationLevel.ReadCommitted },
TransactionScopeAsyncFlowOption.Enabled // ← asyncで大事!
);
// ここでDB操作(EF Core / ADO.NET など)をまとめる
scope.Complete(); // ← Commit 相当
8) トランザクション “やらかし集” 😵💫🧨
やらかし①:トランザクションが長すぎる 🐢💤
- 例:トランザクション開始 → ユーザー入力待ち → commit
- その間、DBのロックが長引いて、他の処理が詰まる😱 ✅ 対策:DBに触る直前に開始して、サッと確定🏃♀️💨
やらかし②:外部通信(HTTP/Queue/メール)をトランザクション内でやる 📡💥
- 外部は遅い&失敗しやすい → ロックが長引く → 地獄👹 ✅ 対策:DB内で完結(Orders + Outbox)までをトランザクション → 外部送信は「後で」(これがOutboxの気持ちよさ)📦➡️📩
やらかし③:SaveChangesを2回に分けて、片方だけ成功 😭
- 1回目:Orders 保存 ✅
- 2回目:Outbox 保存 💥 → “注文だけある”事故 ✅ 対策:同じトランザクションでまとめる🔒🍙
9) まとめ 🧡✨
- トランザクションは **「全部成功 or 全部失敗」**の安全装置🔒
- Outboxで一番大事なのは 「業務更新 + Outbox追加」を同じ取引で確定📦🍙
- EF Core は
SaveChanges1回なら基本安全だけど、Outboxでは明示トランザクションが強い味方💪(Microsoft Learn) TransactionScopeを使うならTransactionScopeAsyncFlowOption.Enabledを忘れない😇(Microsoft Learn)
チェック問題(ゆるめ)📝😺
- Commit しなかったトランザクションは、どうなる?🔒
- Outboxで「同じトランザクションに入れる」2つのものは何?📦
- トランザクション中にHTTP送信しちゃダメ寄りなのはなぜ?📡💥
(答えは次章以降の実装で、どんどん体に入ってくるよ〜!✨)