使用交易Using Transactions

交易可讓系統以不可部分完成的方式處理數個資料庫作業。Transactions allow several database operations to be processed in an atomic manner. 如果認可交易,就會對資料庫成功套用所有作業。If the transaction is committed, all of the operations are successfully applied to the database. 如果復原交易,則不會對資料庫套用任何作業。If the transaction is rolled back, none of the operations are applied to the database.

提示

您可以在 GitHub 上檢視此文章的範例 (英文)。You can view this article's sample on GitHub.

預設交易行為Default transaction behavior

根據預設,如果資料庫提供者支援交易,就會在交易中套用對 SaveChanges() 單一呼叫中的所有變更。By default, if the database provider supports transactions, all changes in a single call to SaveChanges() are applied in a transaction. 如果其中任何一項變更失敗,系統就會復原交易,而不會對資料庫套用任何變更。If any of the changes fail, then the transaction is rolled back and none of the changes are applied to the database. 這意謂著會保證 SaveChanges() 要不就是完全成功,要不就是發生錯誤時讓資料庫維持原封不動。This means that SaveChanges() is guaranteed to either completely succeed, or leave the database unmodified if an error occurs.

對大多數應用程式來說,此預設行為已足以應付需求。For most applications, this default behavior is sufficient. 您應該只有在應用程式需求認為有必要時,才手動控制交易。You should only manually control transactions if your application requirements deem it necessary.

控制交易Controlling transactions

您可以使用 DbContext.Database API 來開始、認可及復原交易。You can use the DbContext.Database API to begin, commit, and rollback transactions. 下列範例示範在單一交易中執行兩個 SaveChanges() 作業和一個 LINQ 查詢。The following example shows two SaveChanges() operations and a LINQ query being executed in a single transaction.

並非所有資料庫提供者都支援交易。Not all database providers support transactions. 呼叫交易 API 時,有些提供者可能會擲回例外狀況或不執行任何作業。Some providers may throw or no-op when transaction APIs are called.

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

跨內容交易 (僅適用於關聯式資料庫)Cross-context transaction (relational databases only)

您也可以跨多個內容執行個體共用交易。You can also share a transaction across multiple context instances. 只有使用關聯式資料庫提供者時才有提供此功能,因為它會要求使用關聯式資料庫資料庫特定的 DbTransactionDbConnectionThis functionality is only available when using a relational database provider because it requires the use of DbTransaction and DbConnection, which are specific to relational databases.

若要共用交易,內容必須同時共用 DbConnectionDbTransactionTo share a transaction, the contexts must share both a DbConnection and a DbTransaction.

允許從外部提供連線Allow connection to be externally provided

必須能夠在建構內容時將連線傳遞給內容,才能共用 DbConnectionSharing a DbConnection requires the ability to pass a connection into a context when constructing it.

若要允許從外部提供 DbConnection,最簡單的方式就是停止使用 DbContext.OnConfiguring 方法來設定內容,然後從外部建立 DbContextOptions 並將其傳遞給內容建構函式。The easiest way to allow DbConnection to be externally provided, is to stop using the DbContext.OnConfiguring method to configure the context and externally create DbContextOptions and pass them to the context constructor.

提示

DbContextOptionsBuilder 是您在 DbContext.OnConfiguring 中用來設定內容的 API,現在您將從外部使用它來建立 DbContextOptionsDbContextOptionsBuilder is the API you used in DbContext.OnConfiguring to configure the context, you are now going to use it externally to create DbContextOptions.

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

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

替代方法是繼續使用 DbContext.OnConfiguring,但接受 DbConnection,這會在儲存後於 DbContext.OnConfiguring 中使用。An alternative is to keep using DbContext.OnConfiguring, but accept a DbConnection that is saved and then used in 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);
    }
}

共用連線和交易Share connection and transaction

您現在可以建立多個共用相同連線的內容執行個體。You can now create multiple context instances that share the same connection. 然後使用 DbContext.Database.UseTransaction(DbTransaction) API 將兩個內容都登錄在同一個交易中。Then use the DbContext.Database.UseTransaction(DbTransaction) API to enlist both contexts in the same transaction.

var options = new DbContextOptionsBuilder<BloggingContext>()
    .UseSqlServer(new SqlConnection(connectionString))
    .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 (僅適用於關聯式資料庫)Using external DbTransactions (relational databases only)

如果您使用多個資料存取技術來存取關聯式資料庫,則可能會想要在這些不同技術所執行的作業之間共用交易。If you are using multiple data access technologies to access a relational database, you may want to share a transaction between operations performed by these different technologies.

下列範例示範如何在同一個交易中執行 ADO.NET SqlClient 作業和 Entity Framework Core 作業。The following example, shows how to perform an ADO.NET SqlClient operation and an Entity Framework Core operation in the same transaction.

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 (System.Exception)
    {
        // TODO: Handle failure
    }
}

使用 System.TransactionsUsing System.Transactions

注意

此功能是 EF Core 2.1 中的新功能。This feature is new in EF Core 2.1.

如果您需要跨較大的範圍進行協調,可以使用環境交易。It is possible to use ambient transactions if you need to coordinate across a larger scope.

using (var scope = new TransactionScope(
    TransactionScopeOption.Required, 
    new TransactionOptions { IsolationLevel = IsolationLevel.ReadCommitted }))
{
    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 (System.Exception)
    {
        // TODO: Handle failure
    }
}

此外,也可以登錄在明確交易中。It is also possible to enlist in an explicit transaction.

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 (System.Exception)
    {
        // TODO: Handle failure
    }
}

System.Transactions 的限制Limitations of System.Transactions

  1. EF Core 需倚賴資料庫提供者實作對 System.Transactions 的支援。EF Core relies on database providers to implement support for System.Transactions. 雖然支援在 .NET Framework 的 ADO.NET 提供者之間很常見,但此 API 是最近才新增至 .NET Core 中,因此支援尚不普遍。Although support is quite common among ADO.NET providers for .NET Framework, the API has only been recently added to .NET Core and hence support is not be as widespread. 如果提供者未實作對 System.Transactions 的支援,則對這些 API 發出的呼叫將可能完全被忽略。If a provider does not implement support for System.Transactions, it is possible that calls to these APIs will be completely ignored. .NET Core 的 SqlClient 從 2.1 版起便不支援它。SqlClient for .NET Core does support it from 2.1 onwards. .NET Core 2.0 的 SqlClient 會在您嘗試使用該功能時擲回例外狀況。SqlClient for .NET Core 2.0 will throw an exception of you attempt to use the feature.

    重要

    建議您先測試該 API 是否可與您的提供者正確搭配運作,再倚賴它來管理交易。It is recommended that you test that the API behaves correctly with your provider before you rely on it for managing transactions. 如果無法正確搭配運作,建議您與資料庫提供者的維護人員連絡。You are encouraged to contact the maintainer of the database provider if it does not.

  2. 從 2.1 版開始,.NET Core 中的 System.Transactions 實作便不包含對分散式交易的支援,因此您無法使用 TransactionScopeCommitableTransaction 來跨多個資源管理員協調交易。As of version 2.1, the System.Transactions implementation in .NET Core does not include support for distributed transactions, therefore you cannot use TransactionScope or CommitableTransaction to coordinate transactions across multiple resource managers.