我正在尝试使用 Entity Framework Core 更新数据库中的
saleOrder
实体。该实体有一个属性 List<saleComments> Comments
。
我在尝试保存更改时遇到问题,出现乐观并发异常:
数据库操作预计影响 1 行,但实际影响 0 行
这是我正在使用的代码:
public void SaveSaleOrder(saleOrder saleOrder)
{
try
{
_log.WriteToLog($"Attempting to find SaleOrder with PEC_NUM_PEDIDO = {saleOrder.PEC_NUM_PEDIDO}");
var existingSaleOrder = _context.SaleOrders
.Include(s => s.Comments)
.FirstOrDefault(s => s.PEC_NUM_PEDIDO == saleOrder.PEC_NUM_PEDIDO);
if (existingSaleOrder == null)
{
if (saleOrder != null)
{
var result = SaveOrUpdatePayment(saleOrder.PaymentStatus);
saleOrder.PECPG_SEQUENCIA = result;
}
_context.SaleOrders.Add(saleOrder);
_log.WriteToLog("ORDER CREATED.");
}
else
{
var existingEntry = _context.Entry(existingSaleOrder);
var saleOrderEntry = _context.Entry(saleOrder);
foreach (var property in saleOrderEntry.Properties)
{
// Ignore the primary key
if (property.Metadata.IsPrimaryKey())
continue;
// Update the property value
existingEntry.Property(property.Metadata.Name).CurrentValue = property.CurrentValue;
}
// Update comments
UpdateComments(existingSaleOrder.Comments, saleOrder.Comments);
_log.WriteToLog("ORDER UPDATED.");
}
_context.SaveChanges();
}
catch (Exception ex)
{
_log.WriteToLog("Error saving to the database: " + ex.Message);
ProcessConfig.Error += 1;
throw;
}
}
private void UpdateComments(ICollection<saleComments> existingComments, ICollection<saleComments> newComments)
{
// Remove existing comments
foreach (var existingComment in existingComments.ToList())
{
_context.Comments.Remove(existingComment);
}
// Add new comments
foreach (var newComment in newComments)
{
existingComments.Add(newComment);
}
}
private int SaveOrUpdatePayment(salePayment payment)
{
var existingPayment = _context.SalePayments
.FirstOrDefault(p => p.Payment_Method == payment.Payment_Method &&
p.Payment_Description == payment.Payment_Description);
if (existingPayment == null)
{
_context.SalePayments.Add(payment);
_log.WriteToLog("PAYMENT CREATED.");
_context.SaveChanges();
return payment.PECPG_SEQUENCIA;
}
else
{
existingPayment.Payment_Description = payment.Payment_Description;
existingPayment.Payment_Method = payment.Payment_Method;
_context.SalePayments.Update(existingPayment);
_log.WriteToLog("PAYMENT UPDATED.");
_context.SaveChanges();
return existingPayment.PECPG_SEQUENCIA;
}
}
问题详情
尝试将更改保存到数据库时遇到乐观并发异常:
数据库操作预计影响 1 行,但实际影响 0 行
自加载实体以来,数据可能已被修改或删除。
我正在尝试更新现有的
saleOrder
,包括其标量属性和 saleComments
列表。
我正在使用
_context.SaleOrders.Include(s => s.Comments)
加载与 saleOrder
相关的评论。
标量属性正在工作,但我无法更新评论。
问题
什么可能导致此并发异常以及如何解决它?
是否有更有效或更正确的方法来更新具有
List<saleComments>
关系的实体,而不会遇到并发问题?
问题可能与您尝试更新评论的方式有关,此外您正在执行的一些不必要的步骤也可能会导致 EF 崩溃。此外,在使用 DBContext 时,将其视为一个工作单元非常重要,理想情况下应该有一个,并且对于给定操作只有一次对
SaveChanges
的调用。所以像主操作调用的支付处理方法这样的支持方法应该避免调用SaveChanges
。
从 SaveOrder 方法开始:
public void SaveSaleOrder(saleOrder saleOrder)
{
ArgumentNullException.ThrowIfNull(nameof(saleOrder));
try
{
_log.WriteToLog($"Attempting to find SaleOrder with PEC_NUM_PEDIDO = {saleOrder.PEC_NUM_PEDIDO}");
var existingSaleOrder = _context.SaleOrders
.Include(s => s.Comments)
.FirstOrDefault(s => s.PEC_NUM_PEDIDO == saleOrder.PEC_NUM_PEDIDO);
if (existingSaleOrder == null)
{
var result = SaveOrUpdatePayment(saleOrder.PaymentStatus);
saleOrder.PECPG_SEQUENCIA = result;
_context.SaleOrders.Add(saleOrder);
_log.WriteToLog("ORDER CREATED.");
}
else
{
_context.Entry(existingSaleOrder).CurrentValues.SetValues(saleOrder);
UpdateComments(existingSaleOrder, saleOrder);
_log.WriteToLog("ORDER UPDATED.");
}
_context.SaveChanges();
}
catch (Exception ex)
{
_log.WriteToLog("Error saving to the database: " + ex.Message);
ProcessConfig.Error += 1;
throw;
}
}
你所拥有的大部分都是好的。在开始时断言 SaleOrder 不为空检查,该方法永远不应在没有检查的情况下调用,因此这可以避免嵌套空检查,因为当您尝试使用销售订单属性进行记录时,您的代码最终仍会引发
NullReferenceException
。为了更新属性,EF 提供了 SetValues()
方法。 IMO 更好的方法是显式复制值或使用配置的映射器。通常,在更新数据时,您只会期望某些值会发生变化,因此明确地确保代码的读者了解预期会发生的变化,并且仅那些指定的值可以更改。 (减少错误或篡改的风险)对于 UpdateComments,我们将传递两个销售订单引用而不是它们的评论集合。这只是为了表明我们正在修改实体,以便未来的开发人员不会执行诸如过滤传入的集合之类的操作,这会破坏行为。
下次更新评论。添加和删除关联实体时,请避免转储现有项目并从更新的源中重新添加它们。当您从跟踪的集合中删除某个项目时,EF 会指出应该删除该项目,但如果您再次添加相同的项目,可能会出现问题。
下面的示例假设注释是销售订单拥有的实体引用而不是引用,这意味着您以一对多关系添加和删除新注释,而不是以多对关系将销售订单关联到现有注释-许多关系。这假设对于新评论,您正在为评论客户端定义一个 ID。如果新评论有一个未设置的 ID,那么下面的逻辑会有点不同,只需检查插入的 newComments 上的默认 0 或 null PK 即可。
private void UpdateComments(SaleOrder existingSaleOrder, SaleOrder newSaleOrder)
{
var existingCommentIds = existingSaleOrder.Comments.Select(x => x.Id).ToList();
var newCommentIds = newSaleOrder.Comments.Select(x => x.Id).ToList();
var commentIdsToAdd = newCommentIds.Except(existingCommentIds);
var commentIdsToRemove = existingCommentIds.Except(existingCommentIds);
var commentIdsToUpdate = newCommentIds.Intersect(existingCommentIds);
foreach(var id in commentIdsToRemove)
{
var comment = existingSaleOrder.Comments.First(x => x.Id == id);
existingSaleOrder.Comments.Remove(comment);
}
foreach(var id in commentIdsToUpdate)
{
var existingComment = existingSaleOrder.Comments.First(x => x.Id == id);
var updatedComment = newSaleOrder.Comments.First(x => x.Id == id);
existingComment.Comment = updatedComment.Comment;
// .. Any other properties that can be updated...
}
foreach(var id in commentIdsToAdd)
{
var comment = newSaleOrder.Comments.First(x => x.Id == id);
existingSaleOrder.Comments.Add(comment);
}
}
因此更新方法的关键是比较 ID 以查找已添加、删除或可能更新的条目。我们首先删除任何不应再存在的项目,然后在添加任何应添加的注释之前浏览更新列表。这样做的原因只是为了在扩展集合之前先减少集合,以最大程度地减少查找项目的时间。 EF 更改跟踪的好处是,在更新的情况下,如果值实际更改,它只会实际生成
UPDATE
SQL 语句,因此您无需添加检查值是否实际更改或不是。 EF 已经解决了这个问题。
对于 UpdatePayment 方法,这应该几乎没问题,只需删除对
SaveChanges
的调用即可。
这有望帮助解决您的并发问题。