sqlite 实体框架-更新错误-无法跟踪实体类型的示例,因为已在跟踪具有该键值的另一个示例

jgzswidk  于 2023-10-23  发布在  SQLite
关注(0)|答案(2)|浏览(178)

我们目前正在使用Entity Framework和SQLite。在我们的场景中,
1.多个工作线程
1.没有组成模式(由于治理原因)
1.我们正在为每个数据库事务创建一个新的DbContext
样本代码:

public int Update(IEnumerable<MyChanges> changes)
{
    using (var dbContext = new MyDbContext())
    { 
        dbContext.TypeChanges.UpdateRange(changes);  // <-- Exception
        return dbContext.SaveChanges();
    }
}

“已跟踪实体”是共享成员示例,其形式为:
ChangeA.ChildChangeB.Child相同。这个子对象发生异常,它已经存在于数据库中,并且没有被修改。其他线程也可以使用这个子示例。
在这个非常小的函数中,我们得到一个异常:
System.InvalidOperationException:无法跟踪实体类型的示例,因为已在跟踪具有该键值的另一个示例。附加现有实体时,请确保仅附加一个具有给定键值的实体示例。
注意:主键是一个字符串值,不是自动生成的
我不明白出了什么问题。
我的猜测是:一个不同的线程当前也在跟踪该示例(这可能在我们的设置中),并且不允许在多个线程中跟踪该实体,即使它们使用MyDbContext的不同示例。
但是如果这是不允许的,你怎么能让多个线程处理你的数据呢?数据结构始终是深层次的,并且彼此拥有。
问:有没有办法在多个线程(不同的上下文)中跟踪一个实体,让最后一个事务获胜?这对我们来说完全足够了。
谢谢你的每一个小提示。我在这一点上已经卡了很长时间了。我想,我完全误解了EF的一些基本概念。
最好的问候,裁缝
1.使用单例DbContext锁定/监视器(工作,但速度慢,不是一个复杂的解决方案)
1.检查跟踪的实体(在我的示例中为空)
1.使用添加/附加-相同错误

q9yhzks0

q9yhzks01#

这是在创建实体的DbContext范围之外传递实体的一个问题。此错误通常发生在将实体用作DTO/ViewModel并将其发送回Controller时,尤其是在处理对象图时。(实体包含相关实体)当使用实体时,引用就是一切,而序列化和重新组合等事情将打破这一点。使用AsNoTracking()加载数据也可能导致此问题,如果您要重新附加分离的实体图。
让我们举一个例子,我们为一个客户加载两个订单,每个订单引用一个状态查找实体。

Order ID 1 -> Status "Open"
Order ID 2 -> Status "Shipped"

现在我们发送给客户,它的订单要更新。在幕后,我们会有代码,或者依赖EF来计算并大致执行以下操作:

  • Attach Ofder ID 1
  • Attach状态“打开”
  • 将订单ID 1状态设置为已修改
  • Attach Ofder ID 2
  • Attach状态“已发货”
  • 将订单ID 2状态设置为已修改
  • SaveChanges()

……一切似乎都很好。然后我们找到下一个客户,他们又有两个订单,但都是“打开”的:

Order ID 3 -> Status "Open"
Order ID 4 -> Status "Open"
  • Attach Ofder ID 3
  • Attach状态“打开”
  • 将订单ID 1状态设置为已修改
  • Attach Ofder ID 4
  • Attach状态“打开”
  • !!异常!!

如果我们看得更深一些,你可以看到发生了什么。当我们从EF读取数据时,在第一种情况下,我们会看到这样的内容:

Order ID 1 -> Status "Open" (Reference A)
Order ID 2 -> Status "Shipped" (Reference B)

在第二种情况下:

Order ID 3 -> Status "Open" (Reference A)
Order ID 4 -> Status "Open" (Reference A)

当EF加载多个订单指向同一相关实体的订单时,它们将指向对该跟踪实体的同一引用。您可以使用object.ReferenceEquals(order3.Status, order4.Status);验证这一点
现在,当您试图反序列化实体或重新组合实体时(例如在控制器窗体POST操作中),问题就出现了。我们得到的订单3和4将是:

Order ID 3 -> Status "Open" (Reference D)
Order ID 4 -> Status "Open" (Reference E)

调用object.ReferenceEquals(order3.Status, order4.Status);将返回false。我们附加了Order 3的Status,这样DbContext就跟踪了一个键为“Open”的状态,但是当我们附加Order 4的状态时,“Open”已经被跟踪了。
当你想将实体附加到DbContext时,你应该总是先检查跟踪缓存,然后如果示例没有被跟踪则附加,或者如果它们被跟踪则替换对被跟踪实体的引用。所以看一下保存订单的例子,我们需要修改一下:

  • Attach Ofder ID 3
  • 检查跟踪缓存(.Local)的状态“打开”
  • 如果找到,请将引用替换为现有状态
  • 如果未找到,Attach状态“打开”
  • 将订单ID 1状态设置为已修改
  • Attach Ofder ID 4
  • 检查跟踪缓存(.Local)的状态“打开”
  • 如果找到,请将引用替换为现有状态
  • 如果未找到,Attach状态“打开”
  • 将订单ID 2状态设置为已修改
  • SaveChanges()

在代码中,类似于:

foreach (var order in orders)
{
    if(_context.Orders.Local.Any(o => o.Id == order.Id)
        throw new InvalidOperation("Somehow an order was already loaded/updated, duplicate reference.");
    var existingStatus = _context.Statuses.Local.FirstOrDefault(s => s.Id == order.Status.Id);
    if (existingStatus != null)
         order.Status = existingStatus;
    else
         _context.Attach(order.Status);

    _context.Attach(order);
    _context.Entry(order).State = EntityState.Modified;
}
_context.SaveChanges();

就我个人而言,我不建议将分离的实体发送到它们被读取的DbContext范围之外。虽然它可能看起来比创建专用的DTO/ViewModel更简单,但它会带来类似上述场景的复杂性,导致更大的不必要数据块被传递,导致重新组合不包含足够信息的问题,从而导致无意的编辑或错误,风险暴露太多关于系统内部的细节给第三方,由于错误或恶意行为者而导致意外操作的风险,并且如果使用延迟加载,则存在显著的性能问题的风险。
但是如果使用分离的实体,则需要在附加任何实体之前始终检查跟踪的引用并处理该场景。

ny6fqffe

ny6fqffe2#

DBContext不是线程安全的。如果你想在多个线程上处理同一个数据库,你必须创建多个DBContext示例。一个DBContext只能用于一个工作单元,例如:如果你点击UI上的一个按钮,会发生的一切(一个事务)。
如果你的函数需要多线程,你应该考虑并行计算,并在同一个线程上一次性将所有内容保存到数据库。如果数据不必保存在同一个事务中,只要在每次需要时启动一个新的DBContext即可。
如果这不是问题所在,你是否检查了 changes 中的每个实体是否都有不同的主键?

相关问题