如何告诉实体框架修改数组相关实体?

问题描述 投票:0回答:1

我有一个抽象的

AuditObject
类,其中包含审核字段,我可以在保存对实体的更改之前自动设置。

public abstract class AuditObject
{
    public string CreatedBy { get; set; }
    public string UpdatedBy { get; set; }
}

在保存之前在我的 DbContext 中设置审核字段:

 private void OnBeforeSaving()
 {
       var user = _someService.GetUserName();

       foreach (var entry in ChangeTracker.Entries()) 
       {
           if (entry.State == EntityState.Modified && entry.Entity is AuditObject auditObject) 
           {
               auditObject.UpdatedBy = user;
               entry.Property("CreatedBy").IsModified = false; 
           }
           else if (entry.State == EntityState.Added && entry.Entity is AuditObject auditObject)
           {
               auditObject.CreatedBy = user;
               auditObject.UpdatedBy = user;
           }
       }
 }

我也在与相关实体相关的实体上继承了这一点:

public abstract class Order: AuditObject
{
    public long Id { get; set; }
    public List<OrderProducts> Products { get; set; }
}

public abstract class OrderProducts: AuditObject
{
    public long Id { get; set; }
    public string Name { get; set; }
}

现在,当我在端点上收到更新的订单时,我将产品列表设置为更新的列表。

[HttpPut("{id:long}")]
public async Task<Order> UpdateOrder(long id, Order order)
{
    var currentOrder = await _dbContext.Order
        .Include(x => x.Products)
        .FirstOrDefaultAsync(x => x.Id == order.Id);

    currentOrder.Products = order.Products;
    await _dbContext.SaveChangesAsync();

    return Ok(currentOrder);
}

当我通过更改跟踪器条目进行调试时,实体框架认为存在修改和删除状态,并导致为产品创建新记录。因此,如果我有一个包含 1 个产品(ID 为 1)的现有订单,则第一个更改跟踪器条目会发现 OrderProduct 1 已被修改,并设置 UpdatedBy 并将 CreatedBy 标记为未修改。然后它有一个键 1 的更改跟踪器条目被删除。最终结果是我得到一个例外:

System.InvalidOperationException:该属性 'OrderProduct.CreatedBy' 不能 分配一个由数据库生成的值。存储生成的值可以 仅分配给配置为使用商店生成的属性 价值观。

在初始创建时永远不会出现问题,CreatedBy 和 UpdatedBy 会被设置,并且会创建一个新的 OrderProduct 记录。仅当我使用现有产品 ID 更新产品列表时才会出现这种情况。有没有办法告诉实体框架在分配产品列表时不考虑更新、修改和删除?

c# .net entity-framework-core
1个回答
0
投票

这不是重新分配相关实体的正确方法:

var currentOrder = await _dbContext.Order
    .Include(x => x.Products)
    .FirstOrDefaultAsync(x => x.Id == order.Id);

currentOrder.Products = order.Products;

为了避免此类问题,请首先保护任何/所有集合设置器,以帮助确保将来不会出现此类操作:

public List<OrderProducts> Products { get; protected set; } = new List<OrderProducts>();

初始化集合是个好主意,因此如果我们创建一个新的 Order,那么 Products 集合就可以开始。这仅适用于导航属性的集合,不适用于单个导航属性。

现在,当涉及到更新与订单关联的产品时,幕后有一个名为 OrderProducts 的链接表,其中插入和删除链接记录以反映订单和产品之间的关联。我们永远不想重新初始化一对多或多对多关系的集合。 EF 将“监听”集合中添加和删除的项目,我们不想通过初始化新列表来中断该连接。相反,我们删除需要删除的内容并添加需要添加的内容。在添加时,对于多对多之类的关联关系,添加对 DbContext 跟踪的实体的引用非常重要。在您的情况下,传递到方法中的 order.Products 不一定由您将从中加载当前订单记录并更新的 DbContext 实例跟踪,因此我们不应该将这些分离的实例显式添加到加载的订单中。如果 DbContext 没有跟踪它们,它会将它们视为新实体。 (产品,而不是 OrderProduct 记录)因此,我们不需要接受整个未跟踪产品实体的方法,我们只需传递产品 Id 的集合即可。 但是,考虑到当前代码具有独立的订单和产品,执行此操作的简单方法是:

[HttpPut("{id:long}")] public async Task<Order> UpdateOrder(long id, Order order) { var currentOrder = await _dbContext.Order .Include(x => x.Products) .FirstOrDefaultAsync(x => x.Id == order.Id); var currentProductIds = currentOrder.Products.Select(x => x.Id).ToList(); var updatedProductIds = order.Products.Select(x => x.Id).ToList(); var productIdsToRemove = updatedProductIds.Except(currentProductIds); var productIdsToAdd = currentProductIds.Except(updatedProductIds); if (productIdsToRemove.Any()) { foreach(var productId in productIdsToRemove) { var product = currentOrder.Products.First(x => x.Id == productId); currentOrder.Products.Remove(product); } } if (productIdsToAdd.Any()) { var productsToAdd = await _dbContext.Products.Where(x => productIdsToAdd.Contains(x.Id)).ToListAsync(); foreach(var product in productsToAdd) currentOrder.Products.Add(product); } await _dbContext.SaveChangesAsync(); return Ok(currentOrder); }

基本确定哪些项目需要删除,哪些项目需要添加。对于要删除的项目,请按当前顺序找到它们并将其删除。对于要添加的项目,从 DbContext 加载引用并将其关联。  您可以避免往返 DbContext 来获取要添加的项目,但这通常不值得付出努力。看起来像:

// ... if (productIdsToAdd.Any()) { foreach(var productId in productIdsToAdd) { var existingProduct = _dbContext.Products.Local.FirstOrDefault(x => x.Id == productId); // Check the dbContext tracking cache for the product. Does not hit DB. if (existingProduct == null) { existingProduct = order.Products.First(x => x.Id == productId); // our detached product _dbContext.Attach(existingProduct); } currentOrder.Products.Add(existingProduct); } }

基本上,它的作用是添加产品 ID,我们检查 DbContext 中是否有已跟踪的实例。如果找到,我们将其添加到 currentOrder.Products 中。如果我们不这样做,那么我们可以附加当前未跟踪的,然后添加它。  如果我们只是在 order.Products 上调用 
Attach

,它在大多数情况下都可以工作,但如果 DbContext 碰巧已经在跟踪具有相同 ID 的产品,则会出现异常。这表现为难以重现的情境运行时异常。这种方法的优点是它避免了数据库往返来获取产品。缺点/限制是它假设分离的产品实体是完整的、有效的并且是最新的。在许多情况下,这些项目被反序列化并且不代表完整的实体,因此附加它们可能会导致奇怪的问题,例如当将来的查询尝试读取产品并且 DbContext 返还此不完整的跟踪引用而不是从加载的新实体时丢失字段数据库。

    
© www.soinside.com 2019 - 2024. All rights reserved.