实体类型的实例无法被追踪,因为已经有另一个具有相同键值的实例正在被追踪。

3
实体类型'AssegnazioneLotto'的实例无法被跟踪,因为另一个具有相同键值{'Id_AssegnazioneLotto'}的实例已经被跟踪。在附加现有实体时,请确保只附加了一个具有给定键值的实体实例。考虑使用'DbContextOptionsBuilder.EnableSensitiveDataLogging'来查看冲突的键值。
当我们从表中调用数据并更新它时,会遇到此错误。通过调用一个调用表的视图来解决此问题。为什么会发生这种情况?如何在不创建其他视图的情况下解决此问题?

你必须意识到哪些实体是被跟踪的。在适当的情况下使用 .AsNoTracking()。 - H H
如果你需要帮助,你必须发布代码。 - Serge
没有看到代码是不可能回答这个问题的。没有通用的答案可以始终修复此异常。 - Gert Arnold
3个回答

16
最简单的答案是:不要在它们被读取的范围之外传递实体。传递视图模型(POCO对象而不是实体),并在更新时获取实体以复制预期的值。
复杂的答案是,在更新实体引用时,包括子集合和多对一引用,您需要检查DbContext是否正在跟踪匹配的引用,并将引用替换为已跟踪的实体,或告诉DbContext在附加之前放弃已跟踪的引用。
例如,接受分离或反序列化的“实体”的更新方法。有时可以使用,但有时会出现问题。
public void UpdateOrder(Order order)
{
    context.Update(order);

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

    context.SaveChanges();
}

看起来简单干净,但当 DbContext 实例可能已经跟踪到匹配的订单实例时就会出错。如果是这种情况,你会收到异常通知。
安全检查:
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或对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();
}

如您所见,随着您需要检查每个引用,这很快就变得复杂而繁琐。因此,避免在实体之间传递分离或序列化的实体更加简单。使用视图模型不仅可以提高性能,还可以简化此类问题。与AutoMapper或类似的支持投影的映射器结合使用,可以使视图模型的操作非常简单:
选择订单:
var orders = context.Orders.Where(/* suitable conditions */)
    .ProjectTo<OrderViewModel>(_mapperConfig)
    .ToList();

其中,_mapperConfig是AutoMapper的一个配置,告诉AutoMapper如何将Order转换为OrderViewModel。它可以遵循约定或可选地包含映射规则,以构建展平的视图模型,包括Order及其相关细节。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 或这样的数据,那么您需要确保这些子实体在 DB 获取时使用 eager loaded /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>(...)

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

这样做的缺点是,需要在网络上传输整个实体及其可能相关的实体,而且为了避免篡改,需要在映射器配置或明确允许的值之间进行更多的努力。


3
这是一个惊人的答案。 - NovaJoe

0
我在使用EF Core和Blazor Server时遇到了同样的问题。将服务集合中的范围切换为“瞬态”,并为查询/更新使用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);

-3
这段代码可以解决你的问题: builder.Services.AddDbContext(option => option.UseSqlServer(connectionString), ServiceLifetime.Transient);

ServiceLifetime.Transient


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接