トランザクションの使用

トランザクションは、複数のデータベース操作をアトミックな方法で処理することを可能にします。 トランザクションがコミットされる場合は、すべての操作がデータベースに正常に適用されます。 トランザクションがロールバックされる場合、データベースに適用される操作はありません。

ヒント

この記事のサンプルは GitHub で確認できます。

既定のトランザクションの動作

既定では、データベース プロバイダーがトランザクションをサポートしている場合は、SaveChanges への 1 回の呼び出しに含まれるすべての変更がトランザクションに適用されます。 いずれかの変更が失敗した場合、トランザクションはロールバックされ、変更は、データベースにまったく適用されません。 つまり、SaveChanges は、完全に成功するか、エラーが発生した場合はデータベースを未変更のままにすることが保証されます。

ほとんどのアプリケーションでは、この既定の動作で十分です。 アプリケーションの要件を満たすために必要であると考えた場合にのみ、トランザクションを手動で制御する必要があります。

トランザクションを制御する

トランザクションは、DbContext.Database API を使用して、開始、コミット、およびロールバックできます。 次の例は、単一のトランザクションで実行される 2 つの SaveChanges 操作と LINQ クエリを示しています。

using var context = new BloggingContext();
using var transaction = context.Database.BeginTransaction();

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

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

    var blogs = context.Blogs
        .OrderBy(b => b.Url)
        .ToList();

    // Commit transaction if all commands succeed, transaction will auto-rollback
    // when disposed if either commands fails
    transaction.Commit();
}
catch (Exception)
{
    // TODO: Handle failure
}

すべてのリレーショナル データベース プロバイダーではトランザクションがサポートされていますが、他のプロバイダーの種類では、トランザクション API が呼び出されたときに例外がスローされたり、何も行われなかったりすることがあります。

Note

この方法での手動によるトランザクションの制御は、暗黙的に呼び出された実行戦略の再試行と互換性がありません。 詳細については、接続の回復性に関する記事を参照してください。

セーブポイント

SaveChanges が呼び出され、そのコンテキストでトランザクションが既に進行中である場合、EF ではデータを保存する前に自動的に "セーブポイント" が作成されます。 セーブポイントとは、エラーが発生した場合やその他の理由で、後でロールバックする可能性のある、データベース トランザクション内のポイントです。 SaveChanges でエラーが発生した場合、トランザクションは自動的にセーブポイントにロールバックされ、開始されていない場合と同じ状態でトランザクションが残ります。 これにより、オプティミスティック同時実行制御に関する問題が発生した場合は特に、問題を修正して保存を再試行できる可能性があります。

警告

セーブポイントは、SQL Server の複数のアクティブな結果セット (MARS) と互換性がありません。 MARS がアクティブに使用されていない場合でも、接続で MARS が有効になっているときは、EF によってセーブポイントは作成されません。 SaveChanges の間にエラーが発生した場合、トランザクションは不明な状態のままになる可能性があります。

また、トランザクションの場合と同様に、セーブポイントを手動で管理することもできます。 次の例では、トランザクション内にセーブポイントを作成し、障害発生時にそこへロールバックします。

using var context = new BloggingContext();
using var transaction = context.Database.BeginTransaction();

try
{
    context.Blogs.Add(new Blog { Url = "https://devblogs.microsoft.com/dotnet/" });
    context.SaveChanges();

    transaction.CreateSavepoint("BeforeMoreBlogs");

    context.Blogs.Add(new Blog { Url = "https://devblogs.microsoft.com/visualstudio/" });
    context.Blogs.Add(new Blog { Url = "https://devblogs.microsoft.com/aspnet/" });
    context.SaveChanges();

    transaction.Commit();
}
catch (Exception)
{
    // If a failure occurred, we rollback to the savepoint and can continue the transaction
    transaction.RollbackToSavepoint("BeforeMoreBlogs");

    // TODO: Handle failure, possibly retry inserting blogs
}

クロスコンテキスト トランザクション

複数のコンテキスト インスタンス間でトランザクションを共有することもできます。 この機能は、リレーショナル データベースに固有の DbTransactionDbConnection を使用する必要があるため、リレーショナル データベース プロバイダーを使用する場合にのみ利用できます。

トランザクションを共有するには、コンテキストが DbConnectionDbTransaction の両方を共有する必要があります。

接続の外部提供を可能にする

DbConnection の共有では、コンテキストの構築時に、コンテキストに接続を渡すことができるようにする必要があります。

DbConnection の外部提供を許可する最も簡単な方法は、DbContext.OnConfiguring メソッドを使用するコンテキストの構成を停止し、DbContextOptions を外部で作成し、それをコンテキスト コンストラクターに渡すことです。

ヒント

DbContextOptionsBuilder は、コンテキストを構成する DbContext.OnConfiguring 内で使用される API であり、その API を外部で使用して、DbContextOptions を作成します。

public class BloggingContext : DbContext
{
    public BloggingContext(DbContextOptions<BloggingContext> options)
        : base(options)
    {
    }

    public DbSet<Blog> Blogs { get; set; }
}

別の方法は、DbContext.OnConfiguring を引き続き使用しますが、保存された DbConnection を受け取って DbContext.OnConfiguring で使用することです。

public class BloggingContext : DbContext
{
    private DbConnection _connection;

    public BloggingContext(DbConnection connection)
    {
      _connection = connection;
    }

    public DbSet<Blog> Blogs { get; set; }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder.UseSqlServer(_connection);
    }
}

接続とトランザクションの共有

同じ接続を共有する複数のコンテキスト インスタンスを作成できるようになりました。 次に、DbContext.Database.UseTransaction(DbTransaction) API を使用して、両方のコンテキストを同じトランザクションに参加させます。

using var connection = new SqlConnection(connectionString);
var options = new DbContextOptionsBuilder<BloggingContext>()
    .UseSqlServer(connection)
    .Options;

using var context1 = new BloggingContext(options);
using var transaction = context1.Database.BeginTransaction();
try
{
    context1.Blogs.Add(new Blog { Url = "http://blogs.msdn.com/dotnet" });
    context1.SaveChanges();

    using (var context2 = new BloggingContext(options))
    {
        context2.Database.UseTransaction(transaction.GetDbTransaction());

        var blogs = context2.Blogs
            .OrderBy(b => b.Url)
            .ToList();
            
        context2.Blogs.Add(new Blog { Url = "http://dot.net" });
        context2.SaveChanges();
    }

    // Commit transaction if all commands succeed, transaction will auto-rollback
    // when disposed if either commands fails
    transaction.Commit();
}
catch (Exception)
{
    // TODO: Handle failure
}

外部 DbTransactions の使用 (リレーショナル データベースのみ)

複数のデータ アクセス テクノロジを使用してリレーショナル データベースにアクセスしている場合、これらの異なるテクノロジによって実行される操作の間でトランザクションを共有できます。

次の例は、同じトランザクション内で ADO.NET SqlClient 操作と Entity Framework Core 操作を実行する方法を示しています。

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

using var transaction = connection.BeginTransaction();
try
{
    // Run raw ADO.NET command in the transaction
    var command = connection.CreateCommand();
    command.Transaction = transaction;
    command.CommandText = "DELETE FROM dbo.Blogs";
    command.ExecuteNonQuery();

    // Run an EF Core command in the transaction
    var options = new DbContextOptionsBuilder<BloggingContext>()
        .UseSqlServer(connection)
        .Options;

    using (var context = new BloggingContext(options))
    {
        context.Database.UseTransaction(transaction);
        context.Blogs.Add(new Blog { Url = "http://blogs.msdn.com/dotnet" });
        context.SaveChanges();
    }

    // Commit transaction if all commands succeed, transaction will auto-rollback
    // when disposed if either commands fails
    transaction.Commit();
}
catch (Exception)
{
    // TODO: Handle failure
}

System.Transactions の使用

大規模なスコープで調整を行う必要がある場合は、アンビエント トランザクションを使用できます。

using (var scope = new TransactionScope(
           TransactionScopeOption.Required,
           new TransactionOptions { IsolationLevel = IsolationLevel.ReadCommitted }))
{
    using var connection = new SqlConnection(connectionString);
    connection.Open();

    try
    {
        // Run raw ADO.NET command in the transaction
        var command = connection.CreateCommand();
        command.CommandText = "DELETE FROM dbo.Blogs";
        command.ExecuteNonQuery();

        // Run an EF Core command in the transaction
        var options = new DbContextOptionsBuilder<BloggingContext>()
            .UseSqlServer(connection)
            .Options;

        using (var context = new BloggingContext(options))
        {
            context.Blogs.Add(new Blog { Url = "http://blogs.msdn.com/dotnet" });
            context.SaveChanges();
        }

        // Commit transaction if all commands succeed, transaction will auto-rollback
        // when disposed if either commands fails
        scope.Complete();
    }
    catch (Exception)
    {
        // TODO: Handle failure
    }
}

明示的なトランザクションに参加することもできます。

using (var transaction = new CommittableTransaction(
           new TransactionOptions { IsolationLevel = IsolationLevel.ReadCommitted }))
{
    var connection = new SqlConnection(connectionString);

    try
    {
        var options = new DbContextOptionsBuilder<BloggingContext>()
            .UseSqlServer(connection)
            .Options;

        using (var context = new BloggingContext(options))
        {
            context.Database.OpenConnection();
            context.Database.EnlistTransaction(transaction);

            // Run raw ADO.NET command in the transaction
            var command = connection.CreateCommand();
            command.CommandText = "DELETE FROM dbo.Blogs";
            command.ExecuteNonQuery();

            // Run an EF Core command in the transaction
            context.Blogs.Add(new Blog { Url = "http://blogs.msdn.com/dotnet" });
            context.SaveChanges();
            context.Database.CloseConnection();
        }

        // Commit transaction if all commands succeed, transaction will auto-rollback
        // when disposed if either commands fails
        transaction.Commit();
    }
    catch (Exception)
    {
        // TODO: Handle failure
    }
}

Note

非同期 API を使用している場合は、必ず TransactionScope コンストラクターで TransactionScopeAsyncFlowOption.Enabled を指定して、アンビエント トランザクションが非同期呼び出し間で確実に流れるようにしてください。

TransactionScope とアンビエント トランザクションの詳細については、このドキュメント参照してください。

System.Transactions の制限

  1. EF Core は、System.Transactions に対するサポートの実装をデータベース プロバイダーに依存しています。 プロバイダーが System.Transactions のサポートを実装していない場合、これらの API への呼び出しは、完全に無視される可能があります。 SqlClient ではこれがサポートされています。

    重要

    この API に依存してトランザクションを管理する前に、お使いのプロバイダーで API が正常に動作することをテストすることをお勧めします。 そうでない場合は、データベース プロバイダーの保守管理者に連絡することが推奨されます。

  2. System.Transactions での分散トランザクションのサポートが .NET 7.0 for Windows にのみ追加されました。 以前のバージョンの .NET または Windows 以外のプラットフォームで分散トランザクションを使用しようとすると失敗します。

  3. TransactionScope では非同期コミット/ロールバックがサポートされていません。つまり、それを破棄すると、操作が完了するまで実行中のスレッドが同期的にブロックされます。