使用 EF Core 5.0。我有一个 SPA 页面,它加载一个 Group
实体及其来自 API 的 Employee
实体集合:
var groupToUpdate = await context.Groups
.Include(g => g.Employees)
.FirstOrDefaultAsync(...);
//Used for UI, list of additional employees for selective adding
var employeeList = await context.Employees.
.Where(...)
.ToListAsync();
然后用户通过 Javascript UI 修改 groupToUpdate
实体,包括一些非导航属性,例如名称/注释。
在同一屏幕上,用户将一些员工添加到组中,从组中删除一些员工,并保留组中的一些现有员工。所有员工都是数据库中具有现有主键的现有实体。到目前为止所做的所有更改都只是针对内存中断开连接的实体。
当用户点击保存时,groupToUpdate
实体被发送到我的后端代码。请注意,我们没有跟踪添加/删除/留下哪些员工,我们只是想让这个 groupToUpdate
完全覆盖旧实体,特别是用新实体替换旧的 Employees
集合.
为了实现这一点,后端代码首先从数据库中再次加载组以开始在上下文中跟踪它。然后我尝试更新实体,包括用新集合替换旧集合:
public async Task UpdateGroupAsync(Group groupToUpdate)
{
var groupFromDb = await context.Groups
.Include(g => g.Employees)
.FirstOrDefaultAsync(...);
// Update non-navigation properties such as groupFromDb.Note = groupToUpdate.Note...
groupFromDb.Employees = groupToUpdate.Employees;
await context.SaveChangesAsync();
}
现在,如果对 Employees
集合的更改是完全替换(删除所有旧的,添加所有新的),则此方法成功。但是只要有一些现有的 Employees
被搁置,EF 核心就会抛出异常:
无法跟踪实体类型 'Employee' 的实例,因为另一个具有键值 ... 的实例已被跟踪
因此,EF Core 似乎尝试同时跟踪 Employee
和 groupFromDb
从数据库中新加载的 groupToUpdate
实体,即使后者仅作为来自断开连接状态的参数。
我的问题是如何以最少的复杂性处理这种更新?是否有必要手动跟踪添加/删除的实体并添加/删除它们而不是尝试替换整个集合?
答案 0 :(得分:1)
您必须指示 ChangeTracker 更新导航集合需要哪些操作。只是替换集合不是正确的方法。
这是有助于自动执行此操作的扩展程序:
context.MergeCollections(groupFromDb.Employees, groupToUpdate.Employees, x => x.Id);
实施:
public static void MergeCollections<T, TKey>(this DbContext context, ICollection<T> currentItems, ICollection<T> newItems, Func<T, TKey> keyFunc)
where T : class
{
List<T> toRemove = null;
foreach (var item in currentItems)
{
var currentKey = keyFunc(item);
var found = newItems.FirstOrDefault(x => currentKey.Equals(keyFunc(x)));
if (found == null)
{
toRemove ??= new List<T>();
toRemove.Add(item);
}
else
{
if (!ReferenceEquals(found, item))
context.Entry(item).CurrentValues.SetValues(found);
}
}
if (toRemove != null)
{
foreach (var item in toRemove)
{
currentItems.Remove(item);
// If the item should be deleted from Db: context.Set<T>().Remove(item);
}
}
foreach (var newItem in newItems)
{
var newKey = keyFunc(newItem);
var found = currentItems.FirstOrDefault(x => newKey.Equals(keyFunc(x)));
if (found == null)
{
currentItems.Add(newItem);
}
}
}