Skip to main content

.NETにおけるトランザクション管理

.NETアプリケーションにおけるデータ整合性を保つためのトランザクション管理について解説します。System.Transactionsを使用した暗黙的なトランザクションから、ADO.NETやEntity Framework Coreを使用した明示的なトランザクションまで、主要な実装パターンと注意点を網羅します。

トランザクションの基本

トランザクションとは、複数の操作を一つの論理的な作業単位として扱い、**すべて成功するか、すべて失敗するか(All or Nothing)**を保証する仕組みです。ACID特性(Atomicity, Consistency, Isolation, Durability)を満たすことが求められます。

1. TransactionScope (System.Transactions)

System.Transactions.TransactionScope は、.NETで最も推奨される汎用的なトランザクション管理方法です。リソースマネージャー(SQL Serverなど)に対して暗黙的にトランザクションを管理します。

基本的な使い方

using ブロックでスコープを定義し、最後に Complete() を呼び出すことでコミットされます。Complete() が呼ばれずにブロックを抜けると、自動的にロールバックされます。

using System.Transactions;

public void TransferMoney(int fromAccountId, int toAccountId, decimal amount)
{
// TransactionScopeを作成
// デフォルトの分離レベルは Serializable なので注意が必要(後述)
using (var scope = new TransactionScope())
{
// データベース操作1: 出金
_accountRepository.Withdraw(fromAccountId, amount);

// データベース操作2: 入金
_accountRepository.Deposit(toAccountId, amount);

// トランザクションをコミット
scope.Complete();
}
}

非同期コードでの注意点 (Async/Await)

.NET 4.5.1 以降および .NET Core / .NET 5+ では、非同期メソッドで TransactionScope を使用する場合、TransactionScopeAsyncFlowOption.Enabled を指定する必要があります。これを指定しないと、await の後に別のスレッドで処理が再開された際、トランザクションコンテキストが引き継がれず、意図しない動作(トランザクション外での実行など)を引き起こす可能性があります。

public async Task TransferMoneyAsync(int fromAccountId, int toAccountId, decimal amount)
{
// 非同期フローを有効にする
using (var scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled))
{
await _accountRepository.WithdrawAsync(fromAccountId, amount);
await _accountRepository.DepositAsync(toAccountId, amount);

scope.Complete();
}
}

分離レベルの指定

TransactionScope のデフォルトの分離レベルは Serializable(直列化可能)です。これは最も厳格なレベルですが、ロックの競合が発生しやすく、パフォーマンスに影響を与える可能性があります。多くの場合、ReadCommitted が実用的です。

var options = new TransactionOptions
{
IsolationLevel = IsolationLevel.ReadCommitted,
Timeout = TimeSpan.FromMinutes(1)
};

using (var scope = new TransactionScope(TransactionScopeOption.Required, options))
{
// ...
scope.Complete();
}

2. Entity Framework Core トランザクション

EF Core は、SaveChanges() 呼び出し時に自動的にトランザクションを作成・コミット・ロールバックします。しかし、複数の SaveChanges を一つのトランザクションにまとめたい場合は、明示的なトランザクション制御が必要です。

IDbContextTransaction の使用

using var context = new MyDbContext();

// トランザクション開始
using var transaction = context.Database.BeginTransaction();

try
{
context.Users.Add(new User { Name = "Alice" });
context.SaveChanges();

context.Blogs.Add(new Blog { Url = "http://blogs.msdn.com/alice" });
context.SaveChanges();

// コミット
transaction.Commit();
}
catch (Exception)
{
// ロールバック(usingブロックを抜ける際にも自動で行われるが、明示的に書くことも可能)
// transaction.Rollback();
throw;
}

TransactionScope との併用

EF Core は TransactionScope にも対応しています。すでに TransactionScope 内で実行されている場合、EF Core はそのトランザクションに参加します。

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

context.Users.Add(newUser);
await context.SaveChangesAsync();

// 他のDB操作やサービス呼び出し...

scope.Complete();
}

3. ADO.NET トランザクション (SqlTransaction)

SqlConnection などの低レベルAPIを使用する場合、BeginTransaction メソッドを使用します。

using (var connection = new SqlConnection(connectionString))
{
connection.Open();

// トランザクション開始
using (var transaction = connection.BeginTransaction())
{
try
{
var command = connection.CreateCommand();
command.Transaction = transaction; // コマンドにトランザクションを紐付ける必須操作

command.CommandText = "INSERT INTO ...";
command.ExecuteNonQuery();

transaction.Commit();
}
catch
{
transaction.Rollback();
throw;
}
}
}

ベストプラクティスと注意点

1. トランザクションの期間は短く保つ

トランザクション中はデータベースのリソース(ロックなど)を保持し続けます。長時間実行される処理(外部API呼び出しや複雑な計算など)はトランザクションの外に出し、DB操作のみをトランザクション内に含めるようにします。

2. 適切な分離レベルを選択する

  • ReadCommitted: 一般的なWebアプリケーションで推奨。コミットされたデータのみ読み取る。
  • Serializable: 最も安全だが、デッドロックやタイムアウトのリスクが高い。
  • Snapshot: 読み取り一貫性を提供するが、tempdbの使用量が増える(SQL Serverの場合)。

3. 分散トランザクション (MSDTC) の回避

複数の異なるデータベース(例: SQL Server A と SQL Server B)にまたがる TransactionScope は、Windows環境では MSDTC (Microsoft Distributed Transaction Coordinator) に昇格する可能性があります。クラウド環境やコンテナ環境では MSDTC の利用が難しい場合があるため、可能な限り同一DB内でのトランザクションに留めるか、Sagaパターンなどの結果整合性アプローチを検討してください。 ※ .NET Core / .NET 5以降では、Windows以外での分散トランザクションのサポートは限定的です。

4. 接続管理

TransactionScope を使用する場合、スコープ内でデータベース接続を開き、スコープ内で閉じることが推奨されます。これにより、接続が自動的にトランザクションに参加します。

// 良い例
using (var scope = new TransactionScope(...))
{
using (var conn = new SqlConnection(...)) // スコープ内でOpen
{
conn.Open();
// ...
}
scope.Complete();
}