无法跟踪实体类型的实例,因为已在跟踪具有相同键值的另一个实例

Posted

技术标签:

【中文标题】无法跟踪实体类型的实例,因为已在跟踪具有相同键值的另一个实例【英文标题】:The instance of entity type cannot be tracked because another instance with the same key value for is already being tracked 【发布时间】:2022-01-02 20:02:28 【问题描述】:

无法跟踪实体类型“AssegnazioneLotto”的实例 因为另一个具有相同键值的实例 'Id_AssegnazioneLotto' 已被跟踪。

附加现有实体时,确保只有一个实体实例 附加了给定的键值。

考虑使用“DbContextOptionsBuilder.EnableSensitiveDataLogging” 查看冲突的键值。

当我们从表中调用数据并更新它时,我遇到了这个错误。

我通过调用一个调用表的视图来解决它。 为什么会这样? 在不创建额外视图的情况下如何解决?

【问题讨论】:

您必须清楚跟踪哪些实体。在适当的地方使用 .AsNoTracking()。 如果您需要帮助,您必须发布代码。 【参考方案1】:

最简单的答案:不要在读取范围之外传递实体。传递视图模型(POCO 对象而不是实体)并在更新时获取实体以复制预期值。

复杂的答案是,当更新实体引用时,所有实体引用包括子集合和多对一引用,您需要检查 DbContext 是否正在跟踪匹配引用,或者替换被跟踪实体的引用,或者告诉 DbContext 在附加之前转储被跟踪的引​​用。

例如,接受分离或反序列化“实体”的更新方法。什么有时有效,但在其他时候却很糟糕:

public void UpdateOrder(Order order)

    context.Update(order);

    // OR 
    context.Attach(order);
    context.Entry(order).State = EntityState.Modified;

    context.SaveChanges();

看起来简单而干净,但是当 DbContext 实例可能已经在跟踪匹配的 Order 实例时就会出错。如果是,你会得到那个异常

安全检查:

public void UpdateOrder(Order order)

    var existingOrder = context.Orders.Local.SingleOrDefault(o => o.OrderId == order.OrderId);
    if (existingOrder != null)
        context.Entry(existingOrder).State = EntityState.Detatched;

    context.Update(order);

    // OR 
    context.Attach(order);
    context.Entry(order).State = EntityState.Modified;

    context.SaveChanges();

该示例检查本地跟踪缓存以查找匹配的订单并转储任何跟踪的实例。这里的关键是使用 DbSet 搜索 .Local 以搜索本地跟踪缓存,而不是命中 DB。

更复杂的地方是 Order 包含其他实体引用,例如 OrderLines,或对客户的引用等。在处理分离的实体时,您需要检查整个对象图中的跟踪引用。

public void UpdateOrder(Order order)

    var existingOrder = context.Orders.Local.SingleOrDefault(o => o.OrderId == order.OrderId);
    if (existingOrder != null)
        context.Entry(existingOrder).State = EntityState.Detatched;

    var customer = context.Customers.Local.SingleOrDefault(c => c.CustomerId = order.Customer.CustomerId);
    if (customer != null)
        order.Customer = customer; // Replace our Customer reference with the tracked one.
    else
        context.Attach(order.Customer);

    context.Update(order);

    // OR 
    context.Attach(order);
    context.Entry(order).State = EntityState.Modified;

    context.SaveChanges();

如您所见,这开始变得非常复杂和繁琐,因为您需要检查每个参考。因此,避免传递分离或序列化的实体更简单。使用视图模型为性能和简化此类问题提供了许多好处。再加上 Automapper 或类似的支持投影的映射器,可以让视图模型的操作变得非常简单:

选择订单:

var orders = context.Orders.Where(/* suitable conditions */)
    .ProjectTo<OrderViewModel>(_mapperConfig)
    .ToList();

其中 _mapperConfig 是一个 Automapper 配置,它告诉 Automapper 如何将 Order 转换为 OrderViewModel。这可以遵循约定或可选地包含映射规则来为订单及其相关细节构建平面视图模型。 ProjectTo 与 EF 的 IQueryable 一起在实体图上构建 SQL SELECT 语句,以仅返回填充视图模型所需的数据。这比使用需要预先加载所有相关实体的Map 高效得多。

更新时:

public void UpdateOrder(UpdateOrderViewModel orderVM)

    var order = context.Orders.Single(o => o.OrderId == orderVM.OrderId);
    if (orderVM.RowVersion != order.RowVersion)
        throw new StaleDataException(); // placeholder to handle the situation where the data has changed since our view got the order details.

    var mapper = _mapperConfig.CreateMapper();
    mapper.Map(orderVM, order);
    context.SaveChanges();

orderVM 可以是返回的 OrderViewModel,但通常我会建议仅将可更新的字段打包到专用视图模型中。 “魔术”在 Automapper 配置中,它控制哪些字段从视图模型复制回实体。如果可以包含诸如 OrderLines 之类的子数据,在这种情况下,您需要确保这些子实体在您的数据库提取中急切加载 /w .Include。在这种情况下,Automapper 的 Map 方法是将映射值从源复制到目标的变体,因此值被直接复制到被跟踪的实体实例中。 EF 将根据实际收费的值而不是覆盖整个记录来构建 SQL UPDATE 语句。

您也可以对分离的实体使用相同的技术来避免您的问题。使用 Automapper 的好处是您可以配置哪些值可以从提供的反序列化/分离实体合法复制到真实数据中:

public void UpdateOrder(Order updatedOrder)

    var order = context.Orders.Single(o => o.OrderId == orderVM.OrderId);
    if (updatedOrder.RowVersion != order.RowVersion)
        throw new StaleDataException(); // placeholder to handle the situation where the data has changed since our view got the order details.

    var mapper = _mapperConfig.CreateMapper();
    mapper.Map(updatedOrder, order);
    context.SaveChanges();

这确保我们只更改允许更改的内容,并避免跟踪引用的整个错误。在我们的映射器配置中,我们确实有一个类似的条目:

cfg.CreateMap<Order, Order>(...)

它将包含明确的规则,以忽略跨字段和相关实体的复制,我们不想在更新时复制。

这样做的缺点是通过网络来回发送整个实体及其相关实体的开销,此外,为了“安全”免受篡改,需要在映射器配置或复制过程中付出更多努力明确允许的值。

【讨论】:

这是一个了不起的答案。

以上是关于无法跟踪实体类型的实例,因为已在跟踪具有相同键值的另一个实例的主要内容,如果未能解决你的问题,请参考以下文章

无法跟踪实体类型的实例,因为已经在跟踪具有相同键值对的另一个实例 'Id'

在 asp.net core 中插入/更新记录时出错

递归 linq 表达式以获取非 NULL 父值?

Asp.net core System.InvalidOperationException:无法跟踪实体类型x的实例

附加类型实体失败,因为相同类型的另一个实体已经具有相同的主键值。

附加类型为“”的实体失败,因为另一个实体具有相同的主键值