トランザクションの使用

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

ヒント

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

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

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

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

自動トランザクションの制御

SaveChangesを呼び出すときに EF がトランザクションを自動的に作成し、ユーザー トランザクションが存在しないかどうかを制御できます。 AutoTransactionBehavior には、次のいずれかの値を選択します。

  • WhenNeeded (既定値): EF は、必要な場合にのみトランザクションを作成します。 たとえば、ほとんどの単一の SQL ステートメントはトランザクションで暗黙的に実行されるため、EF では明示的なトランザクションは作成されません。
  • Always: ユーザー トランザクションが存在しない場合、EF は常にトランザクションを作成します。 これにより、データベースラウンドトリップが追加され、パフォーマンスが低下する可能性があります。
  • Never: EF はトランザクションを自動的に作成しません。

たとえば、 SaveChangesを呼び出す前に常にトランザクションを作成するように EF を構成します。

context.Database.AutoTransactionBehavior = AutoTransactionBehavior.Always;

Always は、アプリケーション コードがユーザー トランザクションを使用しないときに呼び出される IDbTransactionInterceptor トランザクション作成コールバック SaveChanges 依存している場合に便利です。

警告

AutoTransactionBehavior.Neverは注意して使用してください。 SaveChanges複数のコマンドを実行する必要があり、エラーが発生した場合、以前のコマンドは既にコミット済みであり、部分的な変更はデータベースに残されている可能性があります。

トランザクションの制御

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

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

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

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

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

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

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

注

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

セーブポイント

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

警告

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

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

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

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

    await transaction.CreateSavepointAsync("BeforeMoreBlogs");

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

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

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

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

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

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

外部からの接続を許可する

DbConnection を共有するには、接続を構築するときにコンテキストに接続を渡す機能が必要です。

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

ヒント

DbContextOptionsBuilder は、コンテキストを構成するために DbContext.OnConfiguring で使用した 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);
await using var transaction = await context1.Database.BeginTransactionAsync();
try
{
    context1.Blogs.Add(new Blog { Url = "http://blogs.msdn.com/dotnet" });
    await context1.SaveChangesAsync();

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

        var blogs = await context2.Blogs
            .OrderBy(b => b.Url)
            .ToListAsync();

        context2.Blogs.Add(new Blog { Url = "http://dot.net" });
        await context2.SaveChangesAsync();
    }

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

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

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

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

using var connection = new SqlConnection(connectionString);
await connection.OpenAsync();

await using var transaction = (SqlTransaction)await connection.BeginTransactionAsync();
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))
    {
        await context.Database.UseTransactionAsync(transaction);
        context.Blogs.Add(new Blog { Url = "http://blogs.msdn.com/dotnet" });
        await context.SaveChangesAsync();
    }

    // Commit transaction if all commands succeed, transaction will auto-rollback
    // when disposed if either commands fails
    await transaction.CommitAsync();
}
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);
    await connection.OpenAsync();

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

        // 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" });
            await context.SaveChangesAsync();
        }

        // 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))
        {
            await context.Database.OpenConnectionAsync();
            context.Database.EnlistTransaction(transaction);

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

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

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

注

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

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

System.Transactions の制限事項

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

    重要

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

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

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