它正在使用 Entity Framework Core 来更新数据库。
dbContextTransaction.Commit();
工作正常,之后是一些文件操作,结果很糟糕。然后它会抛出一个错误,因此它尝试使用 dbContextTransaction.Rollback();
回滚,但结果是:
此SqlTransaction已完成;已经不能用了。
DbContext dbContext = scope.ServiceProvider.GetService<DbContext>();
IDbContextTransaction dbContextTransaction = dbContext.Database.BeginTransaction();
try
{
IOrderDao orderDao = scope.ServiceProvider.GetService<IOrderDao>();
IItemSoldPriceDao itemSoldPriceDao = scope.ServiceProvider.GetService<IItemSoldPriceDao>();
ItemSoldPrice itemSoldPrice = new ItemSoldPrice
{
...
};
itemSoldPriceDao.AddItemSoldPrice(itemSoldPrice);
order.SoldPriceCaptured = true;
dbContext.SaveChanges();
dbContextTransaction.Commit();
//... some other file operation throws out error
throw new Exception("aaa");
}
catch (Exception ex)
{
CommonLog.Error("CaptureSoldPrice", ex);
dbContextTransaction.Rollback();
}
事务提交后就不能回滚了?
使用实体框架时,仅当您想要将 DbContext 操作的成功或失败与 DbContext 范围之外的其他操作链接起来时,才需要显式事务。 DbContext 中 SaveChanges
之前的所有操作都已分组到事务中。因此,例如在 DbContext 中跨两个或多个表保存实体不需要设置显式事务,如果 EF 无法保存其中一个或另一个,它们将一起提交或回滚。
Commit()
调用应该是本质上形成工作单元的最后一个操作。这将是确定事务范围内的所有操作是否成功的最后一个操作。因此,作为一般规则,所有操作,无论是基于数据库、基于文件等,都应该注册并侦听事务的成功或失败。
使用事务的示例:假设我们有一个系统,通过两个单独的 DbContext 访问两个数据库。一种是跟踪订单并拥有客户记录的订单系统,另一种是跟踪客户信息的 CRM。当我们接受新客户的新订单时,我们会检查 CRM 系统中是否有客户,如果是新客户,则在两个系统中创建客户记录。using (var orderContext = new OrderDbContext())
{
var transaction = orderContext.Database.BeginTransaction();
try
{
var order = new Order
{
// TODO: Populate order details..
}
if(customerDetails == null && registerNewCustomer) // Where customerDetails = details loaded if an existing customer logged in and authenticated...
{
using(var crmContext = new CrmDbContext())
{
crmContext.Database.UseTransaction(transaction);
var customer = new Customer
{
UserName = orderDetails.CustomerEmailAddress,
FirstName = orderDetails.CustomerFirstName,
LastName = orderDetails.CustomerLastName,
Address = orderDetails.BillingAddress
};
crmContext.Customers.Add(customer);
crmContext.SaveChanges();
var orderCustomer = new Orders.Customer
{
CustomerId = customer.CustomerId,
FirstName = customer.FirstName,
LastName = customer.LastName
}
orderContext.Customers.Add(orderCustomer);
}
}
order.CustomerId = crmContext.Customers
.Select(c => c.CustomerId)
.Single(c => c.UserName == customerDetails.UserName);
orderContext.Orders.Add(order);
orderContext.SaveChanges();
transaction.Commit();
}
catch(Exception ex)
{
// TODO: Log exception....
transaction.Rollback();
}
}
订单数据库客户只是 CRM 客户的一个薄包装,我们将在其中获取所有客户详细信息。 CRM 客户管理与订单客户记录相关的客户 ID。这绝不是生产代码类型示例,而只是概述事务在使用时如何协调多个操作。
通过这种方式,如果在任何时候出现任何异常,例如在创建并保存新的客户记录之后,所有保存的更改都将回滚,我们可以检查任何记录的异常详细信息以及记录的值,以确定发生了什么错了。
在处理 DbContext 操作和其他操作的组合时,我们可能希望在失败时支持回滚流程,然后我们可以利用像
TransactionScope
包装器这样的结构。然而,应该谨慎使用,并且仅在您明确需要结合这些操作的情况下使用,而不是尝试使用该模式作为所有操作的标准。在大多数情况下,您不需要显式交易。
// create transaction
await using var transaction = await _db.Database.BeginTransactionAsync();
try
{
// do some database updates
await DatabaseUpdate();
// commit transaction
await transaction.CommitAsync();
// call third party API
await _externalService.CallThirdPartyAsync();
}
catch (Exception ex)
{
// rollback
await transaction.RollbackAsync();
throw;
}
问题
如果提交事务后(例如,在第三方 API 调用期间)抛出异常,就会出现此问题。在 catch 块中,代码尝试回滚已提交的事务。这将导致错误,因为事务上下文在成功提交后不再有效。
这里是抛出的异常 - 由
ZombieCheck
可知:
This SqlTransaction has completed; it is no longer usable.
at Microsoft.Data.SqlClient.SqlTransaction.ZombieCheck()
at Microsoft.Data.SqlClient.SqlTransaction.Rollback()
at System.Data.Common.DbTransaction.RollbackAsync(CancellationToken cancellationToken)
解决方案
要解决这个问题,您应该将事务提交移至 try 块的最后一行。这可确保仅在所有操作(包括第三方 API 调用)成功时才提交事务。如果任何操作失败,事务将被适当回滚。这是修改后的代码:
await using var transaction = await _db.Database.BeginTransactionAsync();
try
{
// Do some database updates
await DatabaseUpdate();
// Call third party API
await _externalService.CallThirdPartyAsync();
// Commit transaction
await transaction.CommitAsync();
}
catch (Exception ex)
{
// Rollback
await transaction.RollbackAsync();
throw;
}
希望有帮助。