Entity Framework のコンテキストにおいて、トランザクションは、既定では SaveChanges() を実行したときに暗黙的に使用されます。
要件によっては、トランザクションのスコープを明示的に制御したいケースも出てくるでしょう。
EF6 ではトランザクション操作のために DbContext.Database.BeginTransaction
/UseTransaction
メソッドが導入され、TransactionScope
よりも推奨されるようになりました。(Microsoft Docs 解説)
ここでは、トランザクションの明示的なスコープ制御を EF6 の Model/Database First で行う例を示します。
※EF4.1 以降で TransactionScope
クラスや DbTransaction.BeginTransaction
メソッドを使用する例については、「トランザクションのスコープ制御(EF4.1~:Model/Database First)」をご覧ください。
※Code First での制御については「トランザクションのスコープ制御(EF6:Code First)」をご覧ください。
複数回の SaveChanges をまたぐトランザクション
Database.BeginTransaction
でトランザクションを開始し、その中で SaveChanges
した変更をまとめて Commit
します。
// コンテキスト
using (var context = new NorthwindEntities())
{
// トランザクション開始
using (var transaction = context.Database.BeginTransaction())
{
// 1つめの SaveChanges()
var product = await context.Products.SingleAsync(p => p.ProductID == 1).ConfigureAwait(false);
product.ProductName = "New Product Name";
await context.SaveChangesAsync().ConfigureAwait(false);
// 2つめの SaveChanges()
var employee = await context.Employees.SingleAsync(e => e.EmployeeID == 1).ConfigureAwait(false);
employee.Title = "New Title";
await context.SaveChangesAsync().ConfigureAwait(false);
// まとめてコミット
transaction.Commit();
}
}
複数のコンテキストをまたぐトランザクション
あらかじめ接続を開いておいて BeginTransaction
メソッドでトランザクションを開始し、その中で複数のコンテキストを操作、SaveChanges
した後にまとめて Commit
します。
Database/Model First では、コンテキストのコンストラクタには EntityConnection
を渡す必要があります。
コンテキストを Dispose
しても接続が破棄されないよう、contextOwnsConnection
引数には false
を指定します。
トランザクションは Database.UseTransaction
メソッドでコンテキストに渡して共用します。
// 接続準備
var workspace = NorthwindEntities.GetMetadataWorkspace();
using (var entityConnection1 = new EntityConnection("name=NorthwindEntities"))
using (var sqlConnection = entityConnection1.StoreConnection)
using (var entityConnection2 = new EntityConnection(workspace, sqlConnection, false))
{
// あらかじめ接続を開いておく。
sqlConnection.Open();
// トランザクション開始
using (var transaction = sqlConnection.BeginTransaction())
{
// 1つ目のコンテキストを操作する。
using (var context = new NorthwindEntities(entityConnection1, false))
{
context.Database.UseTransaction(transaction);
var product = await context.Products.SingleAsync(p => p.ProductID == 1).ConfigureAwait(false);
product.ProductName = "New Product Name";
await context.SaveChangesAsync().ConfigureAwait(false);
}
// 別の EntityConnection を使って2つ目のコンテキストを操作する。
// ※同じ EntityConnection を使用すると InvalidOperationException が発生する。
using (var context = new NorthwindEntities(entityConnection2, false))
{
context.Database.UseTransaction(transaction);
var employee = await context.Employees.SingleAsync(e => e.EmployeeID == 1).ConfigureAwait(false);
employee.Title = "New Title";
await context.SaveChangesAsync().ConfigureAwait(false);
}
// まとめてコミット
transaction.Commit();
}
}
// コンテキストの部分クラス
public partial class NorthwindEntities : DbContext
{
/// <summary>
/// コンストラクタ。
/// </summary>
/// <param name="existingConnection">コンテキストで使用する接続。</param>
/// <param name="contextOwnsConnection">false を指定すると、コンテキストが Dispose されたときに接続を Dispose しない。</param>
public NorthwindEntities(DbConnection existingConnection, bool contextOwnsConnection)
: base(existingConnection, contextOwnsConnection)
{
}
/// <summary>
/// メタデータワークスペースを取得する。
/// </summary>
/// <returns></returns>
public static MetadataWorkspace GetMetadataWorkspace()
{
using (var context = new NorthwindEntities())
{
var objectContext = ((IObjectContextAdapter)context).ObjectContext;
return objectContext.MetadataWorkspace;
}
}
}
それぞれのコンテキストには別々の EntityConnection
を渡す必要があります。
2つ目以降の EntityConnection
を作成するときに必要となる MetadataWorkspace
オブジェクトは、コンテキストが実装する IObjectContextAdapter
インターフェイスの ObjectContext
プロパティから取得できます。