代码之家  ›  专栏  ›  技术社区  ›  Mohammed Noureldin Reedwanul Islam

如何通过分配新集合来更新多个集合中的集合?

  •  3
  • Mohammed Noureldin Reedwanul Islam  · 技术社区  · 8 年前

    在实体框架核心2.0中,我与 Post 和 Category (绑定类为 PostCategory )

    当用户更新 柱 整个 柱 对象(及其 后分类 collection)正在发送到服务器,在此我要重新分配新接收的集合 后分类 (用户可以通过添加新类别和删除某些类别来显著更改此集合)。

    我用于更新该集合的简化代码(我只分配了完全新的集合):

    var post = await dbContext.Posts
        .Include(p => p.PostCategories)
        .ThenInclude(pc => pc.Category)
        .SingleOrDefaultAsync(someId);
    
    post.PostCategories = ... Some new collection...; // <<<
    dbContext.Posts.Update(post);
    await dbContext.SaveChangesAsync();
    

    此新集合中的对象具有上一个集合中对象的相同ID(例如,用户删除了一些(但不是全部)类别) . 因为,我得到一个例外:

    System.InvalidOperationException:无法跟踪实体类型“PostCategory”的实例,因为已跟踪另一个对“CategoryID”、“PostID”具有相同键值的实例。

    如何在不获取此异常的情况下高效地重新生成新集合(或简单地分配一个新集合)?

    更新

    The answer in this link 似乎和我想要的有关,但这是一个好的和有效的方法?有没有可能更好的方法?

    更新2

    我得到我的帖子(编辑覆盖它的值)如下:

    public async Task<Post> GetPostAsync(Guid postId)
    {
        return await dbContext.Posts
            .Include(p => p.Writer)
                .ThenInclude(u => u.Profile)
            .Include(p => p.Comments)
            .Include(p => p.PostCategories)
                .ThenInclude(pc => pc.Category)
            .Include(p => p.PostPackages)
                .ThenInclude(pp => pp.Package)
            //.AsNoTracking()
            .SingleOrDefaultAsync(p => p.Id == postId);
    }
    

    更新3(我的控制器中尝试更新日志的代码):

    var writerId = User.GetUserId();
    var categories = await postService.GetOrCreateCategoriesAsync(
        vm.CategoryViewModels.Select(cvm => cvm.Name), writerId);
    
    var post = await postService.GetPostAsync(vm.PostId);
    post.Title = vm.PostTitle;
    post.Content = vm.ContentText;
    
    post.PostCategories = categories?.Select(c => new PostCategory { CategoryId = c.Id, PostId = post.Id }).ToArray();
    
    await postService.UpdatePostAsync(post); // Check the implementation in Update4.
    

    更新4:

    public async Task<Post> UpdatePostAsync(Post post)
    {
        // Find (load from the database) the existing post
        var existingPost = await dbContext.Posts
            .SingleOrDefaultAsync(p => p.Id == post.Id);
    
        // Apply primitive property modifications
        dbContext.Entry(existingPost).CurrentValues.SetValues(post);
    
        // Apply many-to-many link modifications
        dbContext.Set<PostCategory>().UpdateLinks(
            pc => pc.PostId, post.Id,
            pc => pc.CategoryId,
            post.PostCategories.Select(pc => pc.CategoryId)
        );
    
        // Apply all changes to the db
        await dbContext.SaveChangesAsync();
    
        return existingPost;
    }
    
    1 回复  |  直到 8 年前
        1
  •  4
  •   Ivan Stoev    8 年前

    使用断开链接实体时的主要挑战是检测和应用添加和删除的链接。而ef core(在写作的时候)几乎没有帮助。

    链接的答案是正常的(自定义 Except 方法对于IMO来说太重了),但它有一些陷阱-必须使用热切/显式加载提前检索现有链接(尽管EFcore2.1延迟加载可能不是问题),并且新链接应该只填充FK属性-如果它们包含Reference导航属性,ef core将在调用时尝试创建新的链接实体 Add / AddRange .

    不久前,我回答了类似但略有不同的问题- Generic method for updating EFCore joins 。下面是来自答案的自定义通用扩展方法的更通用和优化版本:

    public static class EFCoreExtensions
    {
        public static void UpdateLinks<TLink, TFromId, TToId>(this DbSet<TLink> dbSet,
            Expression<Func<TLink, TFromId>> fromIdProperty, TFromId fromId,
            Expression<Func<TLink, TToId>> toIdProperty, IEnumerable<TToId> toIds)
            where TLink : class, new()
        {
            // link => link.FromId == fromId
            Expression<Func<TFromId>> fromIdVar = () => fromId;
            var filter = Expression.Lambda<Func<TLink, bool>>(
                Expression.Equal(fromIdProperty.Body, fromIdVar.Body),
                fromIdProperty.Parameters);
            var existingLinks = dbSet.AsTracking().Where(filter);
    
            var toIdSet = new HashSet<TToId>(toIds);
            if (toIdSet.Count == 0)
            {
                //The new set is empty - delete all existing links 
                dbSet.RemoveRange(existingLinks);
                return;
            }
    
            // Delete the existing links which do not exist in the new set
            var toIdSelector = toIdProperty.Compile();
            foreach (var existingLink in existingLinks)
            {
                if (!toIdSet.Remove(toIdSelector(existingLink)))
                    dbSet.Remove(existingLink);
            }
    
            // Create new links for the remaining items in the new set
            if (toIdSet.Count == 0) return;
            // toId => new TLink { FromId = fromId, ToId = toId }
            var toIdParam = Expression.Parameter(typeof(TToId), "toId");
            var createLink = Expression.Lambda<Func<TToId, TLink>>(
                Expression.MemberInit(
                    Expression.New(typeof(TLink)),
                    Expression.Bind(((MemberExpression)fromIdProperty.Body).Member, fromIdVar.Body),
                    Expression.Bind(((MemberExpression)toIdProperty.Body).Member, toIdParam)),
                toIdParam);
            dbSet.AddRange(toIdSet.Select(createLink.Compile()));
        }
    }
    

    它使用单个数据库查询从数据库中检索退出的链接。开销包括几个动态构建的表达式和已编译的委托(为了使调用代码尽可能简单)以及一个临时的 HashSet 用于快速查找。表达式/委托构建的性能影响应该可以忽略不计,并且可以根据需要进行缓存。

    其思想是只为一个链接实体传递一个现有键,为另一个链接实体传递一个现有键列表。因此,根据正在更新的链接实体链接中的哪一个,将以不同的方式调用它。

    在你的样本中,假设你正在接收 IEnumerable<PostCategory> postCategories ,过程如下:

    var post = await dbContext.Posts
        .SingleOrDefaultAsync(someId);
    
    dbContext.Set<PostCategory>().UpdateLinks(pc => 
        pc.PostId, post.Id, pc => pc.CategoryId, postCategories.Select(pc => pc.CategoryId));
    
    await dbContext.SaveChangesAsync();
    

    请注意,此方法允许您更改需求并接受 IEnumerable<int> postCategoryIds :

    dbContext.Set<PostCategory>().UpdateLinks(pc => 
        pc.PostId, post.Id, pc => pc.CategoryId, postCategoryIds);
    

    或 IEnumerable<Category> postCategories :

    dbContext.Set<PostCategory>().UpdateLinks(pc => 
        pc.PostId, post.Id, pc => pc.CategoryId, postCategories.Select(c => c.Id));
    

    或类似的DTO/视图模型。

    分类文章可以以类似的方式更新,并交换相应的选择器。

    更新: 以防您收到(可能)修改的 Post post 实体实例,整个更新过程都是这样的:

    // Find (load from the database) the existing post
    var existingPost = await dbContext.Posts
        .SingleOrDefaultAsync(p => p.Id == post.Id);
    
    if (existingPost == null)
    {
        // Handle the invalid call
        return;
    }
    
    // Apply primitive property modifications
    dbContext.Entry(existingPost).CurrentValues.SetValues(post);
    
    // Apply many-to-many link modifications
    dbContext.Set<PostCategory>().UpdateLinks(pc => pc.PostId, post.Id, 
        pc => pc.CategoryId, post.PostCategories.Select(pc => pc.CategoryId));
    
    // Apply all changes to the db
    await dbContext.SaveChangesAsync();
    

    注意,ef core使用单独的数据库查询来进行与预加载相关的收集。因为helper方法做的是相同的,所以不需要 Include 从数据库中检索主实体时链接相关数据。

    推荐文章