使用事务Working with Transactions

备注

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

本文档介绍如何在 EF6 中使用事务,包括自 EF5 后添加的增强功能,以便轻松处理事务。

默认情况下,什么是 EFWhat EF does by default

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

从 EF6 开始, ExecuteSqlCommand () 在默认情况下会在事务中包装命令(如果尚未存在)。 此方法有一些重载,允许你根据需要重写此行为。 此外,通过ExecuteFunction () 等 api,在模型中包含的存储过程的执行也是相同的(但在重写默认行为时不能 EF6)。

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

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

此默认功能适用于很多用户,如果没有,则无需在 EF6 中执行任何其他操作;只需像往常一样编写代码。

但是,某些用户需要更好地控制其事务–以下各节将对此进行介绍。

Api 的工作原理How the APIs work

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

备注

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

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

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

将多个操作合并为同一上下文中的一个事务Combining several operations into one transaction within the same context

BeginTransaction () 具有两个替代,其中一个将使用显式IsolationLevel ,另一个不采用任何参数,并从基础数据库提供程序使用默认的 IsolationLevel。 这两个重写都返回一个处理对象,该对象提供对基础存储区事务执行 commit 和 Rollback 的commit ()rollback () 方法。

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

  1. using System;
  2. using System.Collections.Generic;
  3. using System.Data.Entity;
  4. using System.Data.SqlClient;
  5. using System.Linq;
  6. using System.Transactions;
  7. namespace TransactionsExamples
  8. {
  9. class TransactionsExample
  10. {
  11. static void StartOwnTransactionWithinContext()
  12. {
  13. using (var context = new BloggingContext())
  14. {
  15. using (var dbContextTransaction = context.Database.BeginTransaction())
  16. {
  17. context.Database.ExecuteSqlCommand(
  18. @"UPDATE Blogs SET Rating = 5" +
  19. " WHERE Name LIKE '%Entity Framework%'"
  20. );
  21. var query = context.Posts.Where(p => p.Blog.Rating >= 5);
  22. foreach (var post in query)
  23. {
  24. post.Title += "[Cool Blog]";
  25. }
  26. context.SaveChanges();
  27. dbContextTransaction.Commit();
  28. }
  29. }
  30. }
  31. }
  32. }

备注

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

将现有事务传递到上下文Passing an existing transaction to the context

有时,您想要在范围内更广泛的事务,其中包括对同一数据库的操作,而不是在 EF 的外部。 若要实现此目的,您必须打开连接并自行启动事务,然后告诉 EF a),以使用已打开的数据库连接,使用 b)在该连接上使用现有事务。

若要执行此操作,必须在上下文类上定义和使用一个构造函数,该构造函数继承自一个 DbContext 构造函数,该构造函数使用 i)一个 contextOwnsConnection 布尔值。

备注

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

  1. using (var conn = new SqlConnection("..."))
  2. {
  3. conn.Open();
  4. using (var context = new BloggingContext(conn, contextOwnsConnection: false))
  5. {
  6. }
  7. }

而且,您必须自行启动事务(如果您想要避免默认设置,则包括 IsolationLevel),并让实体框架知道已经在连接上启动了现有事务(请参阅下面的第33行)。

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

  1. using System;
  2. using System.Collections.Generic;
  3. using System.Data.Entity;
  4. using System.Data.SqlClient;
  5. using System.Linq;
  6. using System.Transactions;
  7. namespace TransactionsExamples
  8. {
  9. class TransactionsExample
  10. {
  11. static void UsingExternalTransaction()
  12. {
  13. using (var conn = new SqlConnection("..."))
  14. {
  15. conn.Open();
  16. using (var sqlTxn = conn.BeginTransaction(System.Data.IsolationLevel.Snapshot))
  17. {
  18. var sqlCommand = new SqlCommand();
  19. sqlCommand.Connection = conn;
  20. sqlCommand.Transaction = sqlTxn;
  21. sqlCommand.CommandText =
  22. @"UPDATE Blogs SET Rating = 5" +
  23. " WHERE Name LIKE '%Entity Framework%'";
  24. sqlCommand.ExecuteNonQuery();
  25. using (var context =
  26. new BloggingContext(conn, contextOwnsConnection: false))
  27. {
  28. context.Database.UseTransaction(sqlTxn);
  29. var query = context.Posts.Where(p => p.Blog.Rating >= 5);
  30. foreach (var post in query)
  31. {
  32. post.Title += "[Cool Blog]";
  33. }
  34. context.SaveChanges();
  35. }
  36. sqlTxn.Commit();
  37. }
  38. }
  39. }
  40. }
  41. }

清除事务Clearing up the transaction

您可以将 null 传递给 UseTransaction ()以清除当前事务实体框架的知识。 当你执行此操作时,实体框架不会提交或回滚现有事务,因此请谨慎使用,仅在你确定要执行的操作时使用。

UseTransaction 中的错误Errors in UseTransaction

如果在以下情况传递事务,则会看到来自 UseTransaction ()的异常:

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

将事务与其他功能一起使用Using transactions with other features

本部分详细说明了上述事务与的交互方式:

  • 连接复原
  • 异步方法
  • TransactionScope 交易

连接复原Connection Resiliency

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

异步编程Asynchronous Programming

前面几节中所述的方法不需要更多选项或设置即可使用异步查询和保存方法。 但请注意,根据您在异步方法中所执行的操作,这可能会导致长时间运行的事务,进而导致死锁或阻塞,导致整个应用程序的性能不佳。

TransactionScope 交易TransactionScope Transactions

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

  1. using System.Collections.Generic;
  2. using System.Data.Entity;
  3. using System.Data.SqlClient;
  4. using System.Linq;
  5. using System.Transactions;
  6. namespace TransactionsExamples
  7. {
  8. class TransactionsExample
  9. {
  10. static void UsingTransactionScope()
  11. {
  12. using (var scope = new TransactionScope(TransactionScopeOption.Required))
  13. {
  14. using (var conn = new SqlConnection("..."))
  15. {
  16. conn.Open();
  17. var sqlCommand = new SqlCommand();
  18. sqlCommand.Connection = conn;
  19. sqlCommand.CommandText =
  20. @"UPDATE Blogs SET Rating = 5" +
  21. " WHERE Name LIKE '%Entity Framework%'";
  22. sqlCommand.ExecuteNonQuery();
  23. using (var context =
  24. new BloggingContext(conn, contextOwnsConnection: false))
  25. {
  26. var query = context.Posts.Where(p => p.Blog.Rating > 5);
  27. foreach (var post in query)
  28. {
  29. post.Title += "[Cool Blog]";
  30. }
  31. context.SaveChanges();
  32. }
  33. }
  34. scope.Complete();
  35. }
  36. }
  37. }
  38. }

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

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

  1. using System.Collections.Generic;
  2. using System.Data.Entity;
  3. using System.Data.SqlClient;
  4. using System.Linq;
  5. using System.Transactions;
  6. namespace TransactionsExamples
  7. {
  8. class TransactionsExample
  9. {
  10. public static void AsyncTransactionScope()
  11. {
  12. using (var scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled))
  13. {
  14. using (var conn = new SqlConnection("..."))
  15. {
  16. await conn.OpenAsync();
  17. var sqlCommand = new SqlCommand();
  18. sqlCommand.Connection = conn;
  19. sqlCommand.CommandText =
  20. @"UPDATE Blogs SET Rating = 5" +
  21. " WHERE Name LIKE '%Entity Framework%'";
  22. await sqlCommand.ExecuteNonQueryAsync();
  23. using (var context = new BloggingContext(conn, contextOwnsConnection: false))
  24. {
  25. var query = context.Posts.Where(p => p.Blog.Rating > 5);
  26. foreach (var post in query)
  27. {
  28. post.Title += "[Cool Blog]";
  29. }
  30. await context.SaveChangesAsync();
  31. }
  32. }
  33. }
  34. }
  35. }
  36. }

对于 TransactionScope 方法仍有一些限制:

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

TransactionScope 方法的优点:

  • 如果与同一事务中的另一个数据库建立了多个连接,则它会自动将本地事务升级到分布式事务,或将连接合并到一个数据库以连接到同一事务中的另一个数据库(注意:您必须MSDTC 服务配置为允许此操作运行的分布式事务。
  • 易于编码。 如果你更愿意在后台隐式处理事务,而不是显式地在控制下进行处理,则 TransactionScope 方法可能更适合你。

总而言之,对于上述新的 BeginTransaction ()和 UseTransaction () Api,大多数用户不再需要 TransactionScope 方法。 如果继续使用 TransactionScope,请注意上述限制。 建议尽可能使用前面部分中所述的方法。