使用事务

注意

仅限 EF6 及更高版本 - 此页面中讨论的功能、API 等已引入实体框架 6。 如果使用的是早期版本,则部分或全部信息不适用。

本文档将介绍在 EF6 中使用事务,包括我们从 EF5 开始添加以便更轻松地使用事务的增强功能。

默认情况下 EF 执行的操作

在所有版本的实体框架中,每当执行 SaveChanges() 以在数据库上插入、更新或删除时,框架都会将该操作包装在事务中。 此事务仅持续足够长的时间以执行操作,然后完成。 执行另一个此类操作时,将启动一个新事务。

从 EF6 Database.ExecuteSqlCommand() 开始,默认情况下,会将命令包装在事务中(如果尚不存在)。 此方法存在重载,使你能够根据需要重写此行为。 此外,在 EF6 中,通过 API(如 ObjectContext.ExecuteFunction())执行模型中包含的存储过程,执行相同的操作(除了目前无法覆盖默认行为)

在任一情况下,事务的隔离级别是数据库提供程序认为其默认设置的任何隔离级别。 例如,默认情况下,在 SQL Server 此为“READ COMMITTED”。

实体框架不会在事务中包装查询。

此默认功能适用于许多用户,如果是这样,则无需在 EF6 中执行任何不同的操作;只需像往常一样编写代码。

但是,有些用户需要更好地控制其事务 - 这将在以下部分中介绍。

API 的工作原理

在 EF6 之前,实体框架坚持打开数据库连接本身(如果传递了已打开的连接,则会引发异常)。 由于事务只能在打开的连接上启动,这意味着用户可以将多个操作包装到一个事务中的唯一方法是使用 TransactionScope 或使用 ObjectContext.Connection 属性,并开始直接在返回的 EntityConnection 对象上调用 Open() 和 BeginTransaction()。 此外,如果你自己在基础数据库连接上启动了事务,则与数据库联系的 API 调用将失败。

注意

实体框架 6 中删除了仅接受已关闭连接的限制。 有关详细信息,请参阅连接管理

从 EF6 开始,框架现在提供:

  1. Database.BeginTransaction():一种更简单的方法,让用户在现有的 DbContext 中自己启动和完成事务 - 允许在同一事务中合并多个操作,因此所有已提交或所有回滚都为一个事务。 它还允许用户更轻松地指定事务的隔离级别。
  2. Database.UseTransaction():它允许 DbContext 使用在实体框架外部启动的事务。

将多个操作合并为同一上下文中的一个事务

Database.BeginTransaction() 有两个重写 – 一个采用显式 IsolationLevel,另一个采用参数并使用基础数据库提供程序的默认 IsolationLevel。 两个重写都返回一个 DbContextTransaction 对象,该对象提供 Commit() 和 Rollback() 方法,这些方法对基础存储事务执行提交和回滚操作

DbContextTransaction 应在提交或回滚后被释放。 实现此目的的一种简单方法是 using(…) {…} 语法,当 using 块完成时,它将自动调用 Dispose()

using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Data.SqlClient;
using System.Linq;
using System.Transactions;

namespace TransactionsExamples
{
    class TransactionsExample
    {
        static void StartOwnTransactionWithinContext()
        {
            using (var context = new BloggingContext())
            {
                using (var dbContextTransaction = context.Database.BeginTransaction())
                {
                    context.Database.ExecuteSqlCommand(
                        @"UPDATE Blogs SET Rating = 5" +
                            " WHERE Name LIKE '%Entity Framework%'"
                        );

                    var query = context.Posts.Where(p => p.Blog.Rating >= 5);
                    foreach (var post in query)
                    {
                        post.Title += "[Cool Blog]";
                    }

                    context.SaveChanges();

                    dbContextTransaction.Commit();
                }
            }
        }
    }
}

注意

开始事务需要基础存储连接处于打开状态。 因此,调用 Database.BeginTransaction() 将打开连接(如果尚未打开)。 如果 DbContextTransaction 打开了连接,则在调用 Dispose() 时将关闭它。

将现有事务传递到上下文

有时,你希望事务的范围更广,并且包括对同一数据库(而不是完全在 EF 外部)的操作。 若要完成此操作,必须自行打开连接并启动事务,然后告诉 EF a) 使用已打开的数据库连接,以及 b) 使用该连接上的现有事务。

为此,必须在上下文类上定义并使用一个构造函数,该构造函数继承自其中一个 DbContext 构造函数,其采用 i) 现有连接参数和 ii) contextOwnsConnection 布尔值。

注意

在这种情况下调用 contextOwnsConnection 标志时,其必须设置为 false。 这很重要,因为它通知实体框架,当它完成连接时,不应该关闭连接(例如,请参阅下面的第 4 行):

using (var conn = new SqlConnection("..."))
{
    conn.Open();
    using (var context = new BloggingContext(conn, contextOwnsConnection: false))
    {
    }
}

此外,必须自己启动事务(如果要避免默认设置,则包括隔离级别),并让实体框架知道连接上已启动现有事务(请参阅下面的第 33 行)。

然后,你可以直接在 SqlConnection 本身或 DbContext 上执行数据库操作。 所有此类操作在一个事务内执行。 你负责提交或回滚事务,并对其调用 Dispose(),以及关闭和释放数据库连接。 例如:

using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Data.SqlClient;
using System.Linq;
using System.Transactions;

namespace TransactionsExamples
{
     class TransactionsExample
     {
        static void UsingExternalTransaction()
        {
            using (var conn = new SqlConnection("..."))
            {
               conn.Open();

               using (var sqlTxn = conn.BeginTransaction(System.Data.IsolationLevel.Snapshot))
               {
                   var sqlCommand = new SqlCommand();
                   sqlCommand.Connection = conn;
                   sqlCommand.Transaction = sqlTxn;
                   sqlCommand.CommandText =
                       @"UPDATE Blogs SET Rating = 5" +
                        " WHERE Name LIKE '%Entity Framework%'";
                   sqlCommand.ExecuteNonQuery();

                   using (var context =  
                     new BloggingContext(conn, contextOwnsConnection: false))
                    {
                        context.Database.UseTransaction(sqlTxn);

                        var query =  context.Posts.Where(p => p.Blog.Rating >= 5);
                        foreach (var post in query)
                        {
                            post.Title += "[Cool Blog]";
                        }
                       context.SaveChanges();
                    }

                    sqlTxn.Commit();
                }
            }
        }
    }
}

清除事务

可以将 null 传递给 Database.UseTransaction() 以清除实体框架对当前事务的了解。 执行此操作时,实体框架既不会提交也不会回滚现有事务,因此请谨慎使用,并且只有在确定这是要执行的操作时才使用。

UseTransaction 中的错误

如果在以下情况下传递事务,你将看到来自 Database.UseTransaction() 的异常:

  • 实体框架已有一个现有事务
  • 实体框架已在 TransactionScope 内运行
  • 传递的事务中的连接对象为 null。 也就是说,事务不与连接相关联 - 通常这是该事务已完成的标志
  • 传递的事务中的连接对象与实体框架的连接不匹配。

将事务与其他功能一起使用

本部分详细介绍了上述事务如何与以下各项交互:

  • 连接复原
  • 异步方法
  • TransactionScope 事务

连接复原

新的连接复原功能不适用于用户启动的事务。 有关详细信息,请参阅重试执行策略

异步编程

前面各部分中概述的方法不需要其他选项或设置即可使用异步查询和保存方法。 但请注意,根据你在异步方法中执行的操作,这可能会导致长时间运行的事务,这反过来又会导致死锁或阻塞,从而不利于整个应用程序的性能。

TransactionScope 事务

在 EF6 之前,提供更大范围事务的推荐方法是使用 TransactionScope 对象:

using System.Collections.Generic;
using System.Data.Entity;
using System.Data.SqlClient;
using System.Linq;
using System.Transactions;

namespace TransactionsExamples
{
    class TransactionsExample
    {
        static void UsingTransactionScope()
        {
            using (var scope = new TransactionScope(TransactionScopeOption.Required))
            {
                using (var conn = new SqlConnection("..."))
                {
                    conn.Open();

                    var sqlCommand = new SqlCommand();
                    sqlCommand.Connection = conn;
                    sqlCommand.CommandText =
                        @"UPDATE Blogs SET Rating = 5" +
                            " WHERE Name LIKE '%Entity Framework%'";
                    sqlCommand.ExecuteNonQuery();

                    using (var context =
                        new BloggingContext(conn, contextOwnsConnection: false))
                    {
                        var query = context.Posts.Where(p => p.Blog.Rating > 5);
                        foreach (var post in query)
                        {
                            post.Title += "[Cool Blog]";
                        }
                        context.SaveChanges();
                    }
                }

                scope.Complete();
            }
        }
    }
}

SqlConnection 和实体框架都将使用环境 TransactionScope 事务,因此可以一起提交。

从 .NET 4.5.1 开始,TransactionScope 已更新为通过使用 TransactionScopeAsyncFlowOption 枚举来使用异步方法:

using System.Collections.Generic;
using System.Data.Entity;
using System.Data.SqlClient;
using System.Linq;
using System.Transactions;

namespace TransactionsExamples
{
    class TransactionsExample
    {
        public static void AsyncTransactionScope()
        {
            using (var scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled))
            {
                using (var conn = new SqlConnection("..."))
                {
                    await conn.OpenAsync();

                    var sqlCommand = new SqlCommand();
                    sqlCommand.Connection = conn;
                    sqlCommand.CommandText =
                        @"UPDATE Blogs SET Rating = 5" +
                            " WHERE Name LIKE '%Entity Framework%'";
                    await sqlCommand.ExecuteNonQueryAsync();

                    using (var context = new BloggingContext(conn, contextOwnsConnection: false))
                    {
                        var query = context.Posts.Where(p => p.Blog.Rating > 5);
                        foreach (var post in query)
                        {
                            post.Title += "[Cool Blog]";
                        }

                        await context.SaveChangesAsync();
                    }
                }
                
                scope.Complete();
            }
        }
    }
}

TransactionScope 方法仍然存在一些限制:

  • 需要 .NET 4.5.1 或更高版本才能使用异步方法。
  • 它不能在云方案中使用,除非确定有一个且只有一个连接(云方案不支持分布式事务)。
  • 它不能与前面各部分的 Database.UseTransaction() 方法结合使用。
  • 如果你发出任何 DDL 并且尚未通过 MSDTC 服务启用分布式事务,它将引发异常。

TransactionScope 方法的优点:

  • 如果你与给定数据库建立多个连接,或者将与一个数据库的连接与同一事务中不同数据库的连接相组合,则它会自动将本地事务升级到分布式事务(注意:必须将 MSDTC 服务配置为允许分布式事务才能正常工作)。
  • 易于编码。 如果希望事务是环境事务,并在后台隐式处理,而不是显式地由你控制,那么 TransactionScope 方法可能更适合你。

总之,有了上面新的 Database.BeginTransaction() 和 Database.UseTransaction() API,对于大多数用户来说,TransactionScope 方法不再是必需的。 如果继续使用 TransactionScope,请注意上述限制。 建议尽可能改用前面各部分中概述的方法。