使用事务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

共享 DbConnection 需要在构造上下文时向其中传入连接的功能。Sharing 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,但接受已保存并随后在 DbContext.OnConfiguring 中使用的 DbConnectionAn 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.

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 (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 }))
{
    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 (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 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. SqlClient for .NET Core 从 2.1 及以上版本开始支持 System.Transactions。SqlClient for .NET Core does support it from 2.1 onwards. 如果你尝试使用此功能,SqlClient for .NET Core 2.0 会抛出异常。SqlClient for .NET Core 2.0 will throw an exception if 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 实现不包括对分布式事务的支持,因此不能使用 TransactionScopeCommittableTransaction 来跨多个资源管理器协调事务。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 CommittableTransaction to coordinate transactions across multiple resource managers.