EF 中的重复条目验证和“已跟踪的实体”错误

Duplicated-entry validation in EF and "entity being already tracked" error

提问人:kkkristo 提问时间:10/8/2023 更新时间:10/9/2023 访问量:23

问:

请在初学者的道路上帮助我,我正在尝试在我的 Customer 类上设置重复条目验证,过滤“电子邮件”或“名称”值。 验证过滤器有效,正在保存新条目,但是在编辑时,我无法更新实体并不断在数据库上出现错误。更新():

System.InvalidOperationException:“无法跟踪实体类型为”Customer“的实例,因为已跟踪另一个具有相同键值的 {'CustomerID'} 的实例。附加现有实体时,请确保仅附加一个具有给定键值的实体实例。

尽管该消息看起来不言自明,但我无法弄清楚要更改什么才能使代码正常工作,同时保留重复条目验证。

我的操作是:

public IActionResult Edit(int? id)
 {
     var editedCustomer = _unitOfWork.Customer.Get(c => c.CustomerID == id);
     CustomerViewModel customerVM = new()
     {
         Customer = editedCustomer
     };
     return View(customerVM);
 }
[HttpPost]
 public IActionResult Edit(CustomerViewModel editedCustomerVM)
 {            
     var result =  _unitOfWork.Customer.GetAll()
         .Where(c => c.CustomerID != editedCustomerVM.Customer.CustomerID)
         .Where(c => c.CustomerEmail == editedCustomerVM.Customer.CustomerEmail)
         .Any();

  if (result)
     {
        var objVM = ReCreateGetView(editedCustomerVM.Customer);
        CreateCustomerAlert(objVM, "Possible duplicated entry existing!");
    return View(objVM);
     }

     _unitOfWork.Customer.Update(editedCustomerVM.Customer);
   ...
   ...

尝试具体化“结果”及其元素,以各种方式将数据从查询移动到内存,但根本没有帮助。删除 Post 方法中的“_unitOfWork”调用会有所帮助,但它显然也排除了预期的反重复验证。

ASP.NET-MVC 实体框架 ASP.NET-Core 验证

评论

0赞 David Browne - Microsoft 10/8/2023
什么?您如何管理其使用寿命?_unitOfWork
0赞 kkkristo 10/8/2023
@David Browne 这是我的存储库模式的工作单元,它注册了范围生存期。与此同时,我将我的“结果”查询更改为:“var result = _unitOfWork.Customer.Any(c => c.CustomerID != editedCustomer.Customer.CustomerID && c.CustomerName == editedCustomer.Customer.CustomerName);',它解决了这个问题。它现在可以正常工作,我只是想知道,究竟是什么导致了同一实例的双重跟踪?它是否过滤了整个“客户”对象集合?

答:

1赞 Steve Py 10/9/2023 #1

当您尝试在 DbContext 已加载并正在跟踪的实体实例上使用时,会导致此问题。Update

 var result =  _unitOfWork.Customer.GetAll()
     .Where(c => c.CustomerID != editedCustomerVM.Customer.CustomerID)
     .Where(c => c.CustomerEmail == editedCustomerVM.Customer.CustomerEmail)
     .Any();

上面的“GetAll()”方法很可能返回的是所有客户的具体化列表,而不是 .这很昂贵,而且完全没有必要,因为 EF DbContext 已作为一个工作单元运行。IQueryable<Customer>()

作为一般规则,我建议避免在控制器和视图之间传递实体。实体表示数据状态,视图模型表示视图状态。EditedCustomerViewModel 应包含允许编辑的值,并且仅包含可以更新的值。接受 Customer 作为视图模型的一部分并调用看起来像是一种简单方便的更新数据的方法,但从某种意义上说,这是危险的,因为它可以允许更改您不打算更改的数据,并且一旦您开始引入导航属性等,它就会变得糟糕。Update()

相反,请考虑在视图模型中跟踪和提供已修改的字段,然后更新看起来更像:

[HttpPost]
 public IActionResult Edit(CustomerViewModel editedCustomerVM)
 {            
     var duplicateEmailAddress = _context.Customers
          .Where(c => c.CustomerID != editedCustomerVm.CustomerID
              && c.EmailAddress == editedCustomerVm.EmailAddress)
          .Any();
     if(duplicateEmailAddress)
     {
         string validationMessage = "Another employee exists for the same Email address.";
         return RedirectToAction("Edit", new {EmployeeID = editedCustomerVM.EmployeeID, validationMessage});
     } 

     var customer =  _context.Customers.Single(c => c.CustomerID == editedCustomerVM.CustomerID);

     // Copy allowed values from the view model into the entity.
     customer.UpdateableValue = editedCustomerVM.UpdatedValue;

     _context.SaveChanges();

     return View(new { CustomerID = editedCustomerVM.CustomerID })
}

这里的主要细节是:当转到 DbContext 检查重复的电子邮件地址时,我们希望确保不会将任何实体加载到跟踪缓存中。我怀疑您拥有的 UoW 包装器正在具体化实体,这将是有问题的,并且在性能方面是浪费的。

如果我们验证并查找重复的电子邮件地址,请返回 RedirectResponse 并将消息作为该响应的一部分传递。“编辑”视图可以查找 validationMessage 并显示类似 Toastr 警报或用户的 what-have-you 等内容。现在,在此示例中,所有更改都将被丢弃,因此“编辑”视图将重新加载所有数据。或者,可以继续将数据从 VM 复制到实体(电子邮件地址除外),保存这些更改,然后返回到带有验证警告的编辑页面。

在更新实体时,我们从 DbContext 加载实体,然后在调用 SaveChanges 之前复制允许的值,而不是使用该方法。 适用于分离的实体,但前提是确保 DbContext 尚未跟踪实体。我不建议使用这种方法来更新数据,因为即使在您确定没有跟踪引用来搞砸事情的最佳情况下,也会为表中的所有字段生成 SQL UPDATE 语句,无论是否有任何更改。改用 EF 更改跟踪器,任何 UPDATE 语句将仅包含实际更改的值,并且仅当实际发生任何更改时才会创建 UPDATE 语句。Update()Update()Update

评论

0赞 kkkristo 10/11/2023
哇,谢谢你的精彩解释。是的,我的 GetAll() 正在返回 ToList()。我读过关于 EF 与工作单元的问题,但自己无法识别哪种方法更好或更合适。UoW 似乎很方便,并且经常实施解决方案,而且大多数(我发现的)教程都是基于 UoW。我现在非常热衷于重新考虑和重构我的代码。我仍然缺乏关于跟踪开始的确切地点和时间的知识,但肯定会挖掘更多信息。感谢您的支持!