使用交易

交易可讓系統以不可部分完成的方式處理數個資料庫作業。 如果認可交易,就會對資料庫成功套用所有作業。 如果復原交易,則不會對資料庫套用任何作業。

秘訣

您可以在 GitHub 上檢視此文章的範例 \(英文\)。

預設交易行為

根據預設,如果資料庫提供者支援交易,就會在交易中套用對 SaveChanges 單一呼叫中的所有變更。 如果其中任何一項變更失敗,系統就會復原交易,而不會對資料庫套用任何變更。 這意謂著會保證 SaveChanges 要不就是完全成功,要不就是發生錯誤時讓資料庫維持原封不動。

對大多數應用程式來說,此預設行為已足以應付需求。 您應該只有在應用程式需求認為有必要時,才手動控制交易。

控制交易

您可以使用 DbContext.Database API 來開始、認可及復原交易。 下列範例示範在單一交易中執行的兩 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 時,其他提供者類型可能會擲回或無作業。

注意

以這種方式手動控制交易與隱含叫用的重試執行策略不相容。 如需詳細資訊,請參閱 連線復原

儲存點

叫用且交易已在內容上進行時 SaveChanges ,EF 會在儲存任何資料之前自動建立 儲存點 。 儲存點是資料庫交易內的點,如果發生錯誤,或基於任何其他原因,稍後可能會回復到該點。 如果 SaveChanges 發生任何錯誤,它會自動將交易回復至儲存點,讓交易處於與從未啟動相同的狀態。 這可讓您修正問題並重試儲存,特別是在 開放式平行存取 問題發生時。

警告

儲存點與SQL Server的多個作用中結果集不相容,而且不會使用。 如果在 期間 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,現在您將從外部使用它來建立 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();
    }

    // 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
    }
}

System.Transactions 的限制

  1. EF Core 需倚賴資料庫提供者實作對 System.Transactions 的支援。 如果提供者未實作對 System.Transactions 的支援,則對這些 API 發出的呼叫將可能完全被忽略。 SqlClient 支援它。

    重要

    建議您先測試該 API 是否可與您的提供者正確搭配運作,再倚賴它來管理交易。 如果無法正確搭配運作,建議您與資料庫提供者的維護人員連絡。

  2. System.Transactions 中的分散式交易支援僅新增至適用于 Windows 的 .NET 7.0。 任何嘗試在舊版 .NET 或非 Windows 平臺上使用分散式交易都會失敗。