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

时间:2018-05-24 23:12:47

标签: c# .net entity-framework .net-core entity-framework-core

在实体框架核心2.0中,PostCategory之间有很多关系(绑定类是PostCategory)。

当用户更新Post时,整个Post对象(及其PostCategory集合)将被发送到服务器,在此我想重新分配新收到的集合{ {1}}(用户可以通过添加新类别和删除某些类别来显着更改此集合。)

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

PostCategory

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

  

System.InvalidOperationException:实体类型的实例' PostCategory'无法跟踪,因为已经跟踪了{' CategoryId',' PostId'}的另一个具有相同键值的实例。

如何有效地重建新集合(或简单地分配新集合)而不会出现此异常?

更新

The answer in this link似乎与我想要的有关,但它是一种优秀而有效的方法吗?有没有更好的方法?

更新2

我的帖子(编辑覆盖其值)如下:

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

更新3(我的控制器中的代码,尝试更新帖子):

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

更新4:

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.

1 个答案:

答案 0 :(得分:4)

使用断开链接实体时的主要挑战是检测并应用添加和删除的链接。而EF Core(截至撰写本文时)几乎没有提供任何帮助。

来自链接的答案是可以的(自定义Except方法对于IMO来说太重了),但是它有一些陷阱 - 必须使用急切/显式加载提前检索现有链接(虽然EF Core 2.1延迟加载可能不是问题),并且新链接应该只填充FK属性 - 如果它们包含引用导航属性,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 / ViewModels。

可以以类似的方式更新类别帖子,并交换相应的选择器。

更新:如果您收到(可能)已修改的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使用单独的数据库查询来进行预先加载相关的收集。由于辅助方法也是如此,因此在从数据库中检索主实体时,无需Include链接相关数据。