注释
EF6 及更高版本 - 实体框架 6 中介绍了本页中讨论的功能、API 等。 如果使用早期版本,某些或全部信息不适用。
本文档将介绍如何在 EF6 中使用事务,包括自 EF5 以来添加的增强功能,以便轻松处理事务。
EF 默认执行的操作
在所有版本的 Entity Framework 中,每当执行 SaveChanges() 在数据库上插入、更新或删除该操作时,框架就会在事务中包装该操作。 此事务仅持续足够长的时间才能执行该操作,然后完成。 每当执行其他此类操作时,都会启动一个新的事务。
从 EF6 Database.ExecuteSqlCommand() 开始,默认情况下,如果尚不存在命令,则会在事务中包装该命令。 此方法有重载版本,允许你在需要时覆盖此行为。 此外,在 EF6 中,通过 ObjectContext.ExecuteFunction() 等 API 执行模型中包含的存储过程,同样会以这种方式执行(目前无法重写默认行为)。
在任何情况下,事务的隔离级别都是数据库提供程序默认设置的隔离级别。 在默认情况下,例如在 SQL Server 上,是 READ COMMITTED。
Entity Framework 不会在事务中包装查询。
此默认功能适用于大量用户,如果是这样,则无需在 EF6 中执行任何不同操作:只需像往常一样编写代码。
但是,某些用户需要更好地控制其事务 - 以下部分对此进行了介绍。
API 的工作原理
在 EF6 版本之前,实体框架坚持自行打开数据库连接(如果传入的是已打开的连接,它会引发异常)。 由于事务只能在打开的连接上启动,这意味着用户将多个操作包装到一个事务中的唯一方法是使用 TransactionScope 或使用 ObjectContext.Connection 属性并开始直接在返回的 EntityConnection 对象上调用 Open() 和 BeginTransaction()。 此外,如果已自行在基础数据库连接上启动事务,则与数据库联系的 API 调用会失败。
注释
实体框架 6 中删除了仅接受已关闭连接的限制。 有关详细信息,请参阅 连接管理。
从 EF6 开始,框架现在提供:
- Database.BeginTransaction():为用户提供了一种更简单的方法,可以在现有的 DbContext 中自行启动和完成事务,允许将多个操作合并到同一事务中,因此所有操作都可以一起提交或回滚。 它还允许用户更轻松地指定事务的隔离级别。
- Database.UseTransaction() :它允许 DbContext 使用在 Entity Framework 外部启动的事务。
在同一上下文中将多个操作合并成一个事务
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))
{
}
}
此外,您必须自行启动事务(包括 IsolationLevel,如果您想避免默认设置),并通知实体框架连接上已经启动了现有事务(请参阅下文的第 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(),以清除 Entity Framework 对当前事务的了解。 实体框架(Entity Framework)在您执行此操作时不会提交或回滚现有事务,因此请谨慎使用,并且仅在确定这正是您想要执行的操作时使用。
UseTransaction 中的错误
如果在以下情况下传递事务,则会看到 Database.UseTransaction() 中的异常:
- Entity Framework 已有一个现有事务
- Entity Framework 已在 TransactionScope 中运行
- 传递的事务中的连接对象为 null。 也就是说,事务不与连接相关联 - 通常是该事务已完成的标志
- 传递的事务中的连接对象与 Entity Framework 的连接不匹配。
将事务与其他功能配合使用
本部分详细介绍了上述事务如何与以下事务交互:
- 连接弹性
- 异步方法
- 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 和 Entity Framework 都将使用周围环境的 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,请注意上述限制。 建议尽可能使用前面部分概述的方法。