代码之家  ›  专栏  ›  技术社区  ›  thankyoussd Svyatoslav Danyliv

EF Core断开连接,通过完全替换更新集合导航属性

  •  0
  • thankyoussd Svyatoslav Danyliv  · 技术社区  · 3 年前

    使用EF Core 5.0。我有一个SPA页面,加载 Group 实体及其集合 Employee API中的实体:

    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();
    

    然后用户修改 groupToUpdate 实体,包括一些非导航属性,如名称/注释。

    在同一屏幕上,用户将一些员工添加到组中,从组中删除一些员工,并保留组中的一些现有员工。所有员工都是DB中具有现有主键的现有实体。到目前为止所做的所有更改都只是针对内存中断开连接的实体。

    当用户单击保存时 要更新的组 实体被发送到我的后端代码。请注意,我们没有跟踪哪些员工被添加/删除/留下,我们只想让它 要更新的组 完全覆盖旧实体,特别是替换的旧集合 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();
    }
    

    现在,如果更改为 员工 集合是完全替换(删除所有旧的,添加所有新的),此方法成功。但只要有一些 员工 则EF核心抛出异常:

    无法跟踪实体类型“Employee”的实例,因为另一个具有键值的实例。。。已被跟踪

    看来EF Core试图追踪 受雇者 使用从数据库中新加载的实体 groupFromDb 和来自的 要更新的组 ,即使后者仅作为参数从断开状态传入。

    我的问题是,如何以最少的复杂性处理这种更新?是否有必要手动跟踪添加/删除的实体并添加/删除它们,而不是试图替换整个集合?

    0 回复  |  直到 3 年前
        1
  •  4
  •   thankyoussd Svyatoslav Danyliv    3 年前

    您必须指示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);
            }
        }
    }