次の方法で共有


トランザクションの操作

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

このドキュメントでは、トランザクションの操作を容易にするために EF5 以降に追加した機能強化を含め、EF6 でのトランザクションの使用について説明します。

EF の既定の動作

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

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

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

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

この既定の機能は、多くのユーザーに適しているため、EF6 で何か異なる操作を行う必要がない場合は適しています。コードは、いつもと同じように記述するだけです。

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

API のしくみ

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

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

EF6 以降、フレームワークは次の機能を提供するようになりました。

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

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

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

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

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();
                }
            }
        }
    }
}

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

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

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

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

このシナリオで呼び出すときは、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 以降では、 TransactionScopeAsyncFlowOption 列挙体を使用して非同期メソッドを操作するように、TransactionScope が更新されました。

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() アプローチと組み合わせることはできません。
  • 分散トランザクションを MSDTC サービスを介して有効にしていない場合に DDL を発行すると、例外がスローされます。

TransactionScope アプローチの利点:

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

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