繁体   English   中英

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

[英]The instance of entity type cannot be tracked because another instance with the same key value for is already being tracked

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

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

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

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

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

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

复杂的答案是,当更新实体引用时,包括子 collections 和多对一引用在内的所有实体引用,您需要检查 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以搜索本地跟踪缓存,而不是命中数据库。

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

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();
}

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

选择订单:

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

其中 _mapperConfig 是一个 Automapper 配置,它告诉 Automapper 如何将 Order 转换为 OrderViewModel。 这可以遵循约定或可选地包含映射规则来为订单及其相关详细信息构建平面视图 model。 ProjectTo与 EF 的IQueryable一起使用,在实体图中构建 SQL SELECT 语句,以仅返回填充视图 model 所需的数据。 这比使用需要预先加载所有相关实体的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,但通常我建议只将可以更新的字段打包到专用视图 model 中。 “魔术”在 Automapper 配置中,它控制从视图 model 复制回实体的字段。 如果可以包含诸如 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>(...)

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

这样做的缺点是通过网络来回发送整个实体和潜在的相关实体的开销,加上为了“安全”免受篡改,需要更多的努力将 go 放入映射器配置或显式复制允许的值.

我在使用 EF Core 和 Blazor 服务器时遇到了同样的问题。 将服务集合中的 scope 切换为“Transient”并使用 ServiceScopeFactory 进行查询/更新就可以了。 您将在下面看到我正在使用 Blazor 样式的依赖注入,但构造函数注入对于 IServiceScopeFactory 的工作方式仍然相同

            [Inject]
        IServiceScopeFactory _serviceScopeFactory { get; set; }
private async Task UpdateItem(GridCommandEventArgs args)
        {
            var utilityItem = (EntityModelSample)args.Item;
            using (var scope1 = _serviceScopeFactory.CreateScope())
            {
                var dbContext = scope1.ServiceProvider.GetService<SampleDbContext>();
                dbContext.Update(utilityItem);
                await dbContext.SaveChangesAsync();
            }

            LoadData();
        }

在启动代码中:

builder.Services.AddDbContext<InternalUtilitiesDbContext>(option => option.UseSqlServer(connectionString), ServiceLifetime.Transient);

此代码解决了您的问题:: builder.Services.AddDbContext(option => option.UseSqlServer(connectionString), ServiceLifetime.Transient);

ServiceLifetime.Transient

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM