我有一个抽象的
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 更新产品列表时才会出现这种情况。有没有办法告诉实体框架在分配产品列表时不考虑更新、修改和删除?
这不是重新分配相关实体的正确方法:
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 返还此不完整的跟踪引用而不是从加载的新实体时丢失字段数据库。