使用事务
事务允许以原子方式处理多个数据库操作。 如果已提交事务,则所有操作都会成功应用到数据库。 如果已回滚事务,则所有操作都不会应用到数据库。
提示
可在 GitHub 上查看此文章的示例。
默认事务行为
默认情况下,如果数据库提供程序支持事务,则会在事务中应用对 SaveChanges
的单一调用中的所有更改。 如果其中有任何更改失败,则会回滚事务且所有更改都不会应用到数据库。 这意味着,SaveChanges
可保证完全成功,或在出现错误时不修改数据库。
对于大多数应用程序,此默认行为已足够。 如果应用程序要求被视为有必要,则应该仅手动控制事务。
控制事务
可以使用 DbContext.Database
API 开始、提交和回滚事务。 以下示例显示了在单个事务中执行的两个 SaveChanges
操作以及一个 LINQ 查询:
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
}
虽然所有关系数据库提供程序都支持事务,但在调用事务 API 时,可能会引发其他提供程序类型或不执行任何操作。
注意
以这种方式手动控制事务的操作与隐式调用的重试执行策略不兼容。 有关详细信息,请参阅连接复原能力。
保存点
如果调用 SaveChanges
且事务已在上下文中进行,则在保存任何数据之前,EF 会自动创建保存点。 保存点是数据库事务中的点,如果发生错误或出于任何其他原因,可能会回滚到这些点。 如果 SaveChanges
遇到任何错误,则会自动将事务回滚到保存点,使事务处于相同状态,就好像从未开始。 这样可以解决问题并重试保存,尤其是在出现乐观并发问题时。
警告
保存点与 SQL Server 的多重活动结果集 (MARS) 不兼容。 当连接上启用了 MARS 时,EF 不会创建保存点,即使 MARS 未处于活动使用状态。 如果在 SaveChanges 过程中出现错误,则该事务可能处于未知状态。
还可以手动管理保存点,就像管理事务一样。 以下示例在事务中创建保存点,并在失败时回滚到该保存点:
using var context = new BloggingContext();
using var transaction = context.Database.BeginTransaction();
try
{
context.Blogs.Add(new Blog { Url = "https://devblogs.microsoft.com/dotnet/" });
context.SaveChanges();
transaction.CreateSavepoint("BeforeMoreBlogs");
context.Blogs.Add(new Blog { Url = "https://devblogs.microsoft.com/visualstudio/" });
context.Blogs.Add(new Blog { Url = "https://devblogs.microsoft.com/aspnet/" });
context.SaveChanges();
transaction.Commit();
}
catch (Exception)
{
// If a failure occurred, we rollback to the savepoint and can continue the transaction
transaction.RollbackToSavepoint("BeforeMoreBlogs");
// TODO: Handle failure, possibly retry inserting blogs
}
跨上下文事务
您还可以跨多个上下文实例共享一个事务。 此功能仅在使用关系数据库提供程序时才可用,因为它需要使用特定于关系数据库的 DbTransaction
和 DbConnection
。
若要共享事务,上下文必须共享 DbConnection
和 DbTransaction
。
允许在外部提供连接
共享 DbConnection
需要在构造上下文时向其中传入连接的功能。
允许在外部提供 DbConnection
的最简单方式是,停止使用 DbContext.OnConfiguring
方法来配置上下文并在外部创建 DbContextOptions
,然后将其传递到上下文构造函数。
提示
DbContextOptionsBuilder
是在 DbContext.OnConfiguring
中用于配置上下文的 API,现在即将在外部使用它来创建 DbContextOptions
。
public class BloggingContext : DbContext
{
public BloggingContext(DbContextOptions<BloggingContext> options)
: base(options)
{
}
public DbSet<Blog> Blogs { get; set; }
}
替代方法是继续使用 DbContext.OnConfiguring
,但接受已保存并随后在 DbContext.OnConfiguring
中使用的 DbConnection
。
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);
}
}
共享连接和事务
现在可以创建共享同一连接的多个上下文实例。 然后使用 DbContext.Database.UseTransaction(DbTransaction)
API 在同一事务中登记两个上下文。
using var connection = new SqlConnection(connectionString);
var options = new DbContextOptionsBuilder<BloggingContext>()
.UseSqlServer(connection)
.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();
context2.Blogs.Add(new Blog { Url = "http://dot.net" });
context2.SaveChanges();
}
// Commit transaction if all commands succeed, transaction will auto-rollback
// when disposed if either commands fails
transaction.Commit();
}
catch (Exception)
{
// TODO: Handle failure
}
使用外部 DbTransactions(仅限关系数据库)
如果使用多个数据访问技术来访问关系数据库,则可能希望在这些不同技术所执行的操作之间共享事务。
以下示例显示了如何在同一事务中执行 ADO.NET SqlClient 操作和 Entity Framework Core 操作。
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 (Exception)
{
// TODO: Handle failure
}
使用 System.Transactions
如果需要跨较大作用域进行协调,则可以使用环境事务。
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 (Exception)
{
// TODO: Handle failure
}
}
还可以在显式事务中登记。
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 (Exception)
{
// TODO: Handle failure
}
}
注意
如果使用异步 API,请确保在 TransactionScope
构造函数中指定 TransactionScopeAsyncFlowOption.Enabled ,以确保环境事务跨异步调用流动。
有关 TransactionScope
和环境事务的详细信息,请参阅此文档。
System.Transactions 的限制
EF Core 依赖数据库提供程序以实现对 System.Transactions 的支持。 如果提供程序未实现对 System.Transactions 的支持,则可能会完全忽略对这些 API 的调用。 SqlClient 支持它。
重要
建议你测试在依赖提供程序以管理事务之前 API 与该提供程序的行为是否正确。 如果不正确,则建议你与数据库提供程序的维护人员联系。
System.Transactions 中的分布式事务支持仅添加到了 Windows 版 .NET 7.0。 尝试在较旧的 .NET 版本或非 Windows 平台上使用分布式事务将失败。
TransactionScope 不支持异步提交/回滚;这意味着同步处置它会阻塞正在执行的线程,直到该操作完成。