使用列表更新实体时出现乐观并发异常<saleComments>Entity Framework Core 中的关系

问题描述 投票:0回答:1

我正在尝试使用 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>
关系的实体,而不会遇到并发问题?

c# .net concurrency entity-framework-core ef-core-5.0
1个回答
0
投票

问题可能与您尝试更新评论的方式有关,此外您正在执行的一些不必要的步骤也可能会导致 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
的调用即可。

这有望帮助解决您的并发问题。

© www.soinside.com 2019 - 2024. All rights reserved.