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

par*_*e89 3 c# entity-framework

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

附加现有实体时,请确保仅附加一个具有给定键值的实体实例。

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

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

我通过调用一个调用表的视图解决了这个问题。为什么会出现这种情况?如何在不创建额外视图的情况下解决问题?

Ste*_* Py 18

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

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

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

public void UpdateOrder(Order order)
{
    context.Update(order);

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

    context.SaveChanges();
}
Run Code Online (Sandbox Code Playgroud)

看起来简单干净,但当 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();
}
Run Code Online (Sandbox Code Playgroud)

该示例检查本地跟踪缓存中是否有匹配的订单并转储所有跟踪的实例。这里的关键是.Local使用 DbSet 搜索本地跟踪缓存,而不是访问 DB。

变得更复杂的是 Order 包含其他实体引用,例如 OrderLines,或对 Customer 的引用等。在处理分离实体时,您需要检查整个对象图以查找跟踪的引用。

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();
}
Run Code Online (Sandbox Code Playgroud)

正如您所看到的,这很快就会变得复杂和麻烦,因为您需要检查每个参考资料。因此,避免传递分离的或序列化的实体会更简单。使用视图模型可以为性能带来许多好处并简化此类问题。与 AutoMapper 或支持投影的类似映射器结合使用可以使视图模型的操作变得非常简单:

选择订单:

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

其中 _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();
}
Run Code Online (Sandbox Code Playgroud)

orderVM 可以是返回的 OrderViewModel,但通常我建议仅将可更新的字段打包到专用视图模型中。“魔力”在于 AutoMapper 配置,它控制哪些字段从视图模型复制回实体。如果可以包含子数据,例如 OrderLines 等,在这种情况下,您需要确保这些子实体.Include在数据库提取中预先加载/w。在这种情况下, 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();
}
Run Code Online (Sandbox Code Playgroud)

这确保我们只更改允许更改的内容,并避免跟踪引用的整个崩溃。在我们的映射器配置中,我们实际上有一个条目,例如:

cfg.CreateMap<Order, Order>(...)
Run Code Online (Sandbox Code Playgroud)

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

这样做的缺点是通过线路来回发送整个实体及其可能相关的实体的开销,而且为了“安全”地防止篡改,需要付出更多的努力来进行映射器配置或显式复制允许的值。

  • 这是一个令人惊奇的答案。 (3认同)