トランザクションの操作

Note

EF6 以降のみ - このページで説明する機能、API などは、Entity Framework 6 で導入されました。 以前のバージョンを使用している場合、一部またはすべての情報は適用されません。

このドキュメントでは、EF6 でトランザクションを使用する方法について、トランザクションを簡単に操作できるように EF5 以降に追加された機能も含めて説明します。

EF の既定の動作

Entity Framework のすべてのバージョンで、SaveChanges() を実行してデータベースに対して挿入、更新、または削除するたびに、フレームワークによって、トランザクションでその操作がラップされます。 このトランザクションは、操作を実行するのに十分な時間だけ続いた後、完了します。 このような別の操作を実行すると、新しいトランザクションが開始されます。

EF6 以降の Database.ExecuteSqlCommand() は、既定では、まだ存在していなかった場合、トランザクションでコマンドがラップされます。 このメソッドのオーバーロードがあり、必要に応じてこの動作をオーバーライドできます。 また、EF6 では、ObjectContext.ExecuteFunction() などの API を介したモデルに含まれるストアド プロシージャを実行しても同じことが実行されます (ただし、現時点では、既定の動作をオーバーライドすることはできません)。

どちらの場合も、トランザクションの分離レベルは、データベース プロバイダーが既定の設定と見なす分離レベルです。 既定では、たとえば、SQL Server では、これは READ COMMITTED です。

Entity Framework では、トランザクションでクエリはラップされません。

この既定の機能は多数のユーザーに適しており、その場合、EF6 で何か別のことを行う必要はありません。いつものようにコードを記述するだけです。

ただし、ユーザーによっては、トランザクションをより詳細に制御する必要があります。これについては、以降のセクションで説明します。

API のしくみ

EF6 より前では、Entity Framework により、データベース接続を開くこと自体が要求されました (既に開いている接続が渡された場合は、例外がスローされました)。 開いた接続でのみトランザクションを開始できるため、これは、ユーザーが複数の操作を 1 つのトランザクションにラップできる唯一の方法が、TransactionScope を使用するか、ObjectContext.Connection プロパティを使用して、返される EntityConnection オブジェクトで直接 Open()BeginTransaction() の呼び出しを開始することを意味しました。 さらに、基礎となるデータベース接続上で独自にトランザクションを開始していた場合、データベースに対する API 呼び出しは失敗します。

Note

閉じた接続のみを受け入れるという制限は、Entity Framework 6 で削除されました。 詳細については、「接続管理」を参照してください。

EF6 以降、フレームワークで次の機能が提供されました。

  1. Database.BeginTransaction(): ユーザーが既存の DbContext 内で自分でトランザクションを開始して完了するためのより簡単な方法 – 複数の操作を同じトランザクションに結合することで、すべてをコミットするか、すべてを 1 つとしてロールバックできます。 また、トランザクションの分離レベルをユーザーがより簡単に指定できるようにします。
  2. Database.UseTransaction(): DbContext で、Entity Framework の外部で開始されたトランザクションを使用できるようにします。

同じコンテキスト内で複数の操作を 1 つのトランザクションに結合する

Database.BeginTransaction() には、2 つのオーバーライドがあります。1 つは明示的な IsolationLevel を受け取り、もう 1 つは引数を取らず、基になるデータベース プロバイダーからの既定の IsolationLevel を使用します。 どちらのオーバーライドからも、基になるストア トランザクションでコミットとロールバックを実行する Commit() および Rollback() メソッドを提供する DbContextTransaction オブジェクトが返されます。

DbContextTransaction は、コミットまたはロールバックされた後に破棄されることを意味します。 これを実現する 1 つの簡単な方法として、using(…) {…} 構文を使用します。これにより、using ブロックが完了すると、Dispose() が自動的に呼び出されます。

using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Data.SqlClient;
using System.Linq;
using System.Transactions;

namespace TransactionsExamples
{
    class TransactionsExample
    {
        static void StartOwnTransactionWithinContext()
        {
            using (var context = new BloggingContext())
            {
                using (var dbContextTransaction = context.Database.BeginTransaction())
                {
                    context.Database.ExecuteSqlCommand(
                        @"UPDATE Blogs SET Rating = 5" +
                            " WHERE Name LIKE '%Entity Framework%'"
                        );

                    var query = context.Posts.Where(p => p.Blog.Rating >= 5);
                    foreach (var post in query)
                    {
                        post.Title += "[Cool Blog]";
                    }

                    context.SaveChanges();

                    dbContextTransaction.Commit();
                }
            }
        }
    }
}

Note

トランザクションを開始するには、基になるストア接続が開いている必要があります。 そのため、Database.BeginTransaction() を呼び出すと、まだ開いていない場合に接続が開かれます。 接続が DbContextTransaction によって開いた場合は、Dispose() が呼び出されたときに閉じます。

既存のトランザクションをコンテキストに渡す

場合によっては、さらに広いスコープで、同じデータベースに対する操作を含むが、完全に EF の完全な外部にあるトランザクションが必要になります。 これを実現するには、接続を開き、自分でトランザクションを開始してから、a) 既に開いているデータベース接続を使用する、b) その接続で既存のトランザクションを使用するように EF に指示する必要があります。

これを行うには、i) 既存の接続パラメーターと ii) contextOwnsConnection ブール値を受け取る DbContext コンストラクターの 1 つから継承するコンストラクターをコンテキスト クラスで定義して使用する必要があります。

Note

このシナリオで呼び出される場合は、contextOwnsConnection フラグを false に設定する必要があります。 これは、完了したときに接続を閉じないように Entity Framework に通知するため重要です (たとえば、下記の 4 行目を参照)。

using (var conn = new SqlConnection("..."))
{
    conn.Open();
    using (var context = new BloggingContext(conn, contextOwnsConnection: false))
    {
    }
}

さらに、トランザクションを自分で開始し (既定の設定を回避する場合は、IsolationLevel を含める)、接続で既に開始されている既存のトランザクションがあることを Entity Framework に通知する必要があります (下記の 33 行目を参照)。

次に、SqlConnection 自体で直接、または DbContext でデータベース操作を自由に実行できます。 このような操作はすべて、1 つのトランザクション内で実行されます。 トランザクションのコミットまたはロールバック、トランザクションでの Dispose() の呼び出し、およびデータベース接続の終了と破棄はご自分で行ってください。 次に例を示します。

using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Data.SqlClient;
using System.Linq;
using System.Transactions;

namespace TransactionsExamples
{
     class TransactionsExample
     {
        static void UsingExternalTransaction()
        {
            using (var conn = new SqlConnection("..."))
            {
               conn.Open();

               using (var sqlTxn = conn.BeginTransaction(System.Data.IsolationLevel.Snapshot))
               {
                   var sqlCommand = new SqlCommand();
                   sqlCommand.Connection = conn;
                   sqlCommand.Transaction = sqlTxn;
                   sqlCommand.CommandText =
                       @"UPDATE Blogs SET Rating = 5" +
                        " WHERE Name LIKE '%Entity Framework%'";
                   sqlCommand.ExecuteNonQuery();

                   using (var context =  
                     new BloggingContext(conn, contextOwnsConnection: false))
                    {
                        context.Database.UseTransaction(sqlTxn);

                        var query =  context.Posts.Where(p => p.Blog.Rating >= 5);
                        foreach (var post in query)
                        {
                            post.Title += "[Cool Blog]";
                        }
                       context.SaveChanges();
                    }

                    sqlTxn.Commit();
                }
            }
        }
    }
}

トランザクションのクリア

Database.UseTransaction() に null を渡すと、現在のトランザクションについて Entity Framework が認識している内容をクリアすることができます。 この操作を行っても、Entity Framework によって、既存のトランザクションはコミットされず、ロールバックもされません。そのため、これがやりたいことであると確信できる場合にのみ慎重に使用してください。

UseTransaction のエラー

次の場合にトランザクションを渡すと、Database.UseTransaction() の例外が表示されます。

  • Entity Framework に既存のトランザクションが既に存在する
  • Entity Framework が TransactionScope 内で既に動作している
  • 渡されたトランザクション内の接続オブジェクトが null である。 つまり、トランザクションが接続に関連付けられていません。通常、これはトランザクションが既に完了していることを示しています
  • 渡されたトランザクションの接続オブジェクトが Entity Framework の接続と一致しない。

他の機能でトランザクションを使用する

このセクションでは、上記のトランザクションと以下との相互作用について詳しく説明します。

  • 接続の弾力性
  • 非同期メソッド
  • TransactionScope トランザクション

接続の回復

新しい接続の回復性機能は、ユーザーが開始したトランザクションでは機能しません。 詳細については、実行戦略の再試行に関する記事をご覧ください。

非同期の概要

前述のセクションで説明した方法では、非同期クエリと保存メソッドを操作するために、それ以上のオプションや設定は必要ありません。 ただし、非同期メソッド内で何を行うかによって、トランザクションの実行時間が長くなり、デッドロックやブロッキングが発生してアプリケーション全体のパフォーマンスが低下する可能性があることに注意してください。

TransactionScope トランザクション

EF6 より前は、スコープのより大きなトランザクションを提供するために推奨さたのは、TransactionScope オブジェクトを使用する方法でした。

using System.Collections.Generic;
using System.Data.Entity;
using System.Data.SqlClient;
using System.Linq;
using System.Transactions;

namespace TransactionsExamples
{
    class TransactionsExample
    {
        static void UsingTransactionScope()
        {
            using (var scope = new TransactionScope(TransactionScopeOption.Required))
            {
                using (var conn = new SqlConnection("..."))
                {
                    conn.Open();

                    var sqlCommand = new SqlCommand();
                    sqlCommand.Connection = conn;
                    sqlCommand.CommandText =
                        @"UPDATE Blogs SET Rating = 5" +
                            " WHERE Name LIKE '%Entity Framework%'";
                    sqlCommand.ExecuteNonQuery();

                    using (var context =
                        new BloggingContext(conn, contextOwnsConnection: false))
                    {
                        var query = context.Posts.Where(p => p.Blog.Rating > 5);
                        foreach (var post in query)
                        {
                            post.Title += "[Cool Blog]";
                        }
                        context.SaveChanges();
                    }
                }

                scope.Complete();
            }
        }
    }
}

SqlConnection と Entity Framework はどちらもアンビエントの TransactionScope トランザクションを使用するため、一緒にコミットされます。

.NET 4.5.1 以降、TransactionScope は、TransactionScopeAsyncFlowOption 列挙体を使用して非同期メソッドでも動作するように更新されました。

using System.Collections.Generic;
using System.Data.Entity;
using System.Data.SqlClient;
using System.Linq;
using System.Transactions;

namespace TransactionsExamples
{
    class TransactionsExample
    {
        public static void AsyncTransactionScope()
        {
            using (var scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled))
            {
                using (var conn = new SqlConnection("..."))
                {
                    await conn.OpenAsync();

                    var sqlCommand = new SqlCommand();
                    sqlCommand.Connection = conn;
                    sqlCommand.CommandText =
                        @"UPDATE Blogs SET Rating = 5" +
                            " WHERE Name LIKE '%Entity Framework%'";
                    await sqlCommand.ExecuteNonQueryAsync();

                    using (var context = new BloggingContext(conn, contextOwnsConnection: false))
                    {
                        var query = context.Posts.Where(p => p.Blog.Rating > 5);
                        foreach (var post in query)
                        {
                            post.Title += "[Cool Blog]";
                        }

                        await context.SaveChangesAsync();
                    }
                }
                
                scope.Complete();
            }
        }
    }
}

TransactionScope アプローチにはまだいくつかの制限があります。

  • 非同期メソッドを操作するには、.NET 4.5.1 以降が必要です。
  • 接続が確実に 1 つのみの場合を除き、クラウド シナリオでは使用できません (クラウド シナリオでは分散トランザクションがサポートされていません)。
  • 前述のセクションの Database.UseTransaction() アプローチと組み合わせることはできません。
  • DDL を発行し、MSDTC サービスを介して分散トランザクションを有効にしていない場合は、例外がスローされます。

TransactionScope アプローチの利点は次のとおりです。

  • 特定のデータベースに対して複数の接続を作成する、または、同じトランザクション内の別のデータベースへの接続を使用して 1 つのデータベースに接続を結合する場合、ローカル トランザクションは分散トランザクションに自動的にアップグレードされます (注: これを動作させるには、MSDTC サービスが、分散トランザクションを許可するように構成されている必要があります)。
  • コーディングが容易です。 トランザクションをアンビエントにし、明示的に制御するのではなく、バックグラウンドで暗黙的に処理したい場合は、TransactionScope アプローチの方が適している可能性があります。

要約すると、上記の新しい Database.BeginTransaction() および Database.UseTransaction() API を使用すると、ほとんどのユーザーは、TransactionScope アプローチが必要なくなります。 もしも TransactionScope の使用を続ける場合は、上記の制限事項に注意してください。 可能であれば、前述のセクションで説明した方法を使用することをお勧めします。