提问人:Flu 提问时间:11/17/2023 最后编辑:ℍ ℍFlu 更新时间:11/18/2023 访问量:67
上下文。SaveChangesAsync() 未按预期工作
context.SaveChangesAsync() doesn't work as expected
问:
我在使用 EF 更新数据库时遇到问题。 在我的应用程序中,我有 Orders 和 OrderPositions。OrderPosition 可以是 FoodPosition 或 ReservationPosition。因此,我的 Order 类中有两个列表,一个用于 FoodPositions,一个用于 ReservationPositions。
我的问题是使用FoodPositions和ReservationPositions更新订单。 当我想在以下行中保存对数据库的更改时:
await context.SaveChangesAsync();
此行是 UpdateAsync 方法的一部分:
private async Task UpdateAsync(Order order)
{
Order contextOrder = context.Set<Order>()
.Include(o => o.FoodPositions)
.Include(o => o.ReservationPositions)
.Single(e => e.Id == order.Id);
contextOrder.FoodPositions = order.FoodPositions;
contextOrder.ReservationPositions = order.ReservationPositions;
contextOrder.Status = order.Status;
contextOrder.Cost = order.Cost;
contextOrder.CostCenter = order.CostCenter;
context.Update(contextOrder);
await context.SaveChangesAsync();
}
它不会完成该行,而是返回到调用方法的位置。
这是我的数据库上下文中用于绑定外键的代码:
modelBuilder.Entity<FoodPosition>()
.HasOne<Order>()
.WithMany(e => e.FoodPositions);
modelBuilder.Entity<ReservationPosition>()
.HasOne<Order>()
.WithMany(e => e.ReservationPositions);
当我尝试在没有这两行的情况下更新订单时:
contextOrder.FoodPositions = order.FoodPositions;
contextOrder.ReservationPositions = order.ReservationPositions;
它工作得很好,并且它通过 saveChangesAsync 行。
我没有收到任何错误,它只是没有按照我想要的方式工作。因为它不会按预期保存更新的订单以及所有更新的属性值和新的 OrderPositions。
也许用新的 OrderPositions 替换 OrderPositions 会有一些冲突,它不知道如何在数据库中处理它。
答:
从有关更改跟踪的文档
实体实例在以下情况下将被跟踪:
- 从对数据库执行的查询返回
- 通过 Add、Attach、Update 或类似方法显式附加到 DbContext
- 检测为连接到现有跟踪实体的新实体
因此,当您的初始查询返回 contextOrder 时,会被跟踪,但不会被跟踪。这两个对象可能共享相同的 Id,因此当您运行时,它有两个具有相同 ID 的不同跟踪对象,即异常所说的内容。这与异步与否无关。contextOrder.FoodPositions
order.FoodPositions
.Update
您可以尝试添加 .AsNoTracking
() 来确保在显式执行此操作之前不会跟踪实体,但请注意,当您这样做时,将在对象图上进行搜索,并且所有找到的具有现有键的实体将被跟踪为“已修改”,而没有键的实体(即 0)将被跟踪为“已添加”。.Update(..)
还可以显式设置单个实体的跟踪状态。contextOrder.Entry(myEntity).State = EntityState.Modified
您可能还需要考虑上下文对象的生存期。我最熟悉的方法是确保上下文是短暂的。也就是说,你创建你的上下文,将它用于你需要做的任何事情,并处理它。如果你的上下文是长期存在的,你需要更密切地关注哪些实体被跟踪,哪些实体没有被跟踪,以及处于什么状态。
从数据库中获取实体的 ID 并对实体进行更改后,只需调用 context。SaveChangesAsync() 方法将更改传播到数据库。
private async Task UpdateAsync(Order order)
{
// ...
contextOrder.Cost = order.Cost;
contextOrder.CostCenter = order.CostCenter;
await context.SaveChangesAsync();
}
更新分离的聚合实体需要特别小心。在 EF 中,引用就是一切,如果 DbContext 未跟踪您提供的引用,则会遇到异常或重复数据情况。如果它已经在跟踪同一行的另一个引用,则再次出现异常。
首先,在加载跟踪引用 (existingOrder) 时,您不使用 . 用于附加一个分离的实体,并告诉 EF 将整个事物视为已修改。这不适用于引用的其他实体,因此需要单独完成这些操作。我不建议使用,因为它效率低下,即使实际上什么都没有改变,也会将所有内容标记为已更改,并且您“信任”传入的数据,因此在许多情况下(例如 Web 应用程序)这会使您的系统受到篡改。相反,加载跟踪的实体,复制允许的值,然后调用 .Update
Update
Update
SaveChanges()
下一个问题是处理导航属性、FoodPositions 和 ReservationPositions。对于导航属性,您需要考虑这些属性是表示关联还是更“拥有”的类型关系。例如,这些是多对多关系,其中订单与一个或多个现有 Food 或 FoodPositions 相关联,还是 FoodPositions 创建并被视为订单的一对多子级?相同的基本规则适用于这两种情况,但它们的处理方式略有不同,因为在保存 Order 之前,DbContext 需要跟踪任何关联的内容。
我建议用于更新关联的典型代码结构如下所示:(将代码应用于 FoodPositions,您可以将其应用于 ReservationPositions。这假设拥有更多的一对多关系)
private async Task UpdateAsync(Order order)
{
Order contextOrder = await context.Set<Order>()
.Include(o => o.FoodPositions)
.Include(o => o.ReservationPositions)
.SingleAsync(e => e.Id == order.Id);
var existingFoodPositionIds = contextOrder.FoodPositions
.Select(x => x.Id)
.ToList();
var updatedFoodPositionIds = order.FoodPositions
.Select(x => x.Id)
.ToList();
var foodPositionIdsToAdd = updatedFoodPositionIds
.Except(existingFoodPositionIds);
var foodPositionIdsToRemove = existingFoodPositionIds
.Except(updatedFoodPositionIds);
if (foodPositionIdsToRemove.Any())
{
var foodPositionsToRemove = contextOrder.FoodPositions
.Where(x => foodPositionIdsToRemove.Contains(x.Id))
.ToList();
foreach(var foodPosition in foodPositionsToRemove)
contextOrder.FoodPositions.Remove(foodPosition);
}
if (foodPositionIdsToAdd.Any())
{
var foodPositionsToAdd = order.FoodPositions
.Where(x => foodPositionIdsToAdd.Contains(x.Id))
.ToList();
foreach(var foodPosition in foodPositionsToAdd)
contextOrder.FoodPositions.Add(foodPosition);
}
contextOrder.Status = order.Status;
contextOrder.Cost = order.Cost;
contextOrder.CostCenter = order.CostCenter;
await context.SaveChangesAsync();
}
从本质上讲,我们比较列表以查找需要添加或删除的项目,然后继续将它们添加或删除到跟踪的订单中。这可能涉及相当多的代码,因此我可能会将其分解为一个简单的方法来调用(即 updateOrderFoodPositions(order, contextOrder)),而不是增加 UpdateOrder 以涵盖所有内容。
您必须自己关心导航集合,它们在分离时不会自动更新。
我创建了通用方法,可以帮助您简化这种情况。使用此方法可以更新集合属性。MergeCollections
private async Task UpdateAsync(Order order)
{
Order contextOrder = context.Set<Order>()
.Include(o => o.FoodPositions)
.Include(o => o.ReservationPositions)
.Single(e => e.Id == order.Id);
CollectionHelpers.MergeCollections(context, contextOrder.FoodPositions, order.FoodPositions, e => e.Id);
CollectionHelpers.MergeCollections(context, contextOrder.ReservationPositions, order.ReservationPositions, e => e.Id);
contextOrder.Status = order.Status;
contextOrder.Cost = order.Cost;
contextOrder.CostCenter = order.CostCenter;
await context.SaveChangesAsync();
}
和实施:
public class CollectionHelpers
{
public static void MergeCollections<TItem, TKey>(
DbContext context,
ICollection<TItem> destination,
ICollection<TItem> source,
Func<TItem, TKey> keyFunc)
where TItem: notnull
where TKey : notnull
{
MergeCollections(destination, source, keyFunc, keyFunc, e => e,
(d, s) => context.Entry(d).CurrentValues.SetValues(s),
d => context.Remove(d)
);
}
public static void MergeCollections<TItem, TKey>(
ICollection<TItem> destination,
ICollection<TItem> source,
Func<TItem, TKey> keyFunc,
Action<TItem, TItem> onUpdate,
Action<TItem> onDelete)
where TKey : notnull
{
MergeCollections(destination, source, keyFunc, keyFunc, e => e, onUpdate, onDelete);
}
public static void MergeCollections<TDestination, TSource, TDestinationKey>(
ICollection<TDestination> destination,
ICollection<TSource> source,
Func<TDestination, TDestinationKey> destinationKeyFunc,
Func<TSource, TDestinationKey> sourceKeyFunc,
Func<TSource, TDestination> insertFactory,
Action<TDestination, TSource> onUpdate,
Action<TDestination> onDelete)
where TDestinationKey : notnull
{
// shortcuts
if (source.Count == 0)
{
while (destination.Count > 0)
{
var first = destination.First();
destination.Remove(first);
onDelete(first);
}
return;
}
if (destination.Count == 0)
{
foreach(var s in source)
destination.Add(insertFactory(s));
return;
}
var sourceDic = source.ToDictionary(sourceKeyFunc);
foreach (var d in destination.ToList())
{
var key = destinationKeyFunc(d);
if (sourceDic.TryGetValue(key, out var foundSource))
{
onUpdate(d, foundSource);
sourceDic.Remove(key);
}
else
{
destination.Remove(d);
onDelete(d);
}
}
if (sourceDic.Count > 0)
{
foreach(var s in sourceDic.Values)
destination.Add(insertFactory(s));
}
}
}
评论
await
UpdateAsync
contextOrder
Modified
context.Update
Added
Modified
order
UpdateAsync
contect.Update(order)
Update
Added
Modified
contextOrder
ID