DbContext.AttachRange() 在这种情况下如何工作

how DbContext.AttachRange() works in this scenario

我看到一本书有这样的代码:

public class Order 
{
   public int OrderID { get; set; }
   public ICollection<CartLine> Lines { get; set; }
   ...
}

public class CartLine
{
   public int CartLineID { get; set; }
   public Product Product { get; set; }
   public int Quantity { get; set; }
}

//Product class is just a normal class that has properties such as ProductID, Name etc

并且在订单存储库中,有一个 SaveOrder 方法:

public void SaveOrder(Order order)
{
   context.AttachRange(order.Lines.Select(l => l.Product));
   if (order.OrderID == 0)
   {
       context.Orders.Add(order);
   }
   context.SaveChanges();
}

书上说:

在数据库中存储订单对象时。当用户的购物车数据从会话存储中反序列化时,JSON 包会创建新的未知对象 Entity Framework 核心,然后尝试将所有对象写入数据库。对于 Product 对象,这意味着 Entity Framework Core 尝试写入已存储的对象,这会导致错误。为了避免这个问题,我通知 Entity Framework Core 对象存在并且不应存储在数据库中,除非它们被修改

我很困惑,有两个问题:

Q1-为什么写入已经存储的对象会出错,​​从底层数据库的角度来看,它只是一个update SQL语句将所有列修改为当前值?我知道它通过不改变任何东西并重写所有东西来做不必要的工作,但它不应该在数据库级别抛出任何错误?

Q2-为什么我们不对 CartLine 做同样的事情:

context.AttachRange(order.Lines.Select(l => l.Product));
context.AttachRange(order.Lines);

防止CartLine对象存储在数据库中就像我们对Product对象所做的那样?

好的,所以这会很长:

第一个问题:

在Entity Framework(核心或"old"6)中,有"Change tracking"这个概念。 DbContext class 能够跟踪您对数据所做的所有更改,然后通过 SQL 语句(INSERT、UPDATE、DELETE)将其应用到数据库中。要了解为什么它会在您的案例中引发错误,您首先需要了解 DbContext / 更改跟踪的实际工作原理。让我们举个例子:

public void SaveOrder(Order order)
{
   context.AttachRange(order.Lines.Select(l => l.Product));
   if (order.OrderID == 0)
   {
       context.Orders.Add(order);
   }
   context.SaveChanges();
}

在此方法中,您会收到一个包含 LinesProductsOrder 实例。假设此方法是从某个 Web 应用程序调用的,这意味着您 没有从数据库加载订单实体。 这就是所谓的 Disconected Scenario

它是 "disconnected",因为您的 DbContext 不知道它们的存在。当您执行 context.AttachRange 时,您实际上是在告诉 EF: 这里由我控制,而且我 100% 确定这些实体已经存在于数据库中。请暂时注意它们!,

让我们再次使用您的代码:假设它是一个新的 Order(因此它将进入您的 if 那里)并且您删除了代码的 context.AttachRange 部分。一旦代码到达 AddSaveChanges,这些事情就会在 DbContext:

内部发生
  1. 将调用DetectChanges方法
  2. 它将尝试在其当前图表
  3. 中找到所有实体Order, Lines and Products
  4. 如果找不到它们,它们将作为 要插入的新记录添加到 "pending changes"

然后你继续调用SaveChanges,它会像书上告诉你的那样失败。为什么?假设选择的 Products 是:

Id: 1, "Macbook Pro"
Id: 2, "Office Chair"

DbContext 查看实体但不知道它们时,它会将它们添加到状态为 Added 的待定更改中。当您调用 SaveChanges 时,它会根据这些产品在模型中的当前状态为这些产品发出 INSERT 语句。由于 ID 的 1 and 2 已存在于数据库中,因此操作失败,主键冲突。

这就是为什么在这种情况下您必须调用 Attach(或 AttachRange)。这有效地告诉 EF 实体存在于数据库中,并且不应尝试再次插入它们。它们将以 Unchanged 的状态添加到上下文中。 Attach 通常用于您之前没有从 dbContext 加载实体的情况。

第二题:

这对我来说很难访问,因为我不知道那个级别的 context/model,但这是我的猜测:

您不需要对 Cartline 执行此操作,因为对于每个订单,您可能都想插入新的 Order line。想想在亚马逊买东西。您将产品放入购物车,它会生成一个 Order,然后是 Order Lines,构成该订单的东西。

如果您随后要更新现有订单并向其中添加更多商品,那么您会 运行 进入同一期。在将现有 CartLines 保存到数据库之前,您必须先加载它们,或者像您在此处所做的那样调用 Attach

希望它更清楚一点。我已经回答了一个类似的问题,我在其中提供了更多细节,所以也许阅读它也有更多帮助: