如何通过分配新集合来更新多对多集合? [英] How to update a Collection in Many-Many by assigning a new Collection?

查看:24
本文介绍了如何通过分配新集合来更新多对多集合?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

在entity framework core 2.0中,PostCategory之间有很多关系(绑定类是PostCategory).>

当用户更新一个 Post 时,整个 Post 对象(连同它的 PostCategory 集合)被发送到服务器,这里我想重新分配新收到的集合 PostCategory(用户可以通过添加新类别和删除一些类别来显着更改此集合).

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

var post = await dbContext.Posts.Include(p => p.PostCategories).ThenInclude(pc => pc.Category).SingleOrDefaultAsync(someId);post.PostCategories = ...一些新的集合...;//<<<dbContext.Posts.Update(post);等待 dbContext.SaveChangesAsync();

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

<块引用>

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

如何在不出现此异常的情况下有效地重建新集合(或简单地分配一个新集合)?

更新

此链接中的答案似乎与什么有关我想要,但它是一个好的和有效的方法吗?有没有更好的办法?

更新 2

我的帖子(编辑覆盖其值)是这样的:

公共异步任务<发布>GetPostAsync(Guid postId){返回等待 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);}

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

var writerId = User.GetUserId();var category = 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();等待 postService.UpdatePostAsync(post);//检查 Update4 中的实现.

更新 4:

公共异步任务<发布>UpdatePostAsync(发布帖子){//查找(从数据库加载)现有帖子var existingPost = await dbContext.Posts.SingleOrDefaultAsync(p => p.Id == post.Id);//应用原始属性修改dbContext.Entry(existingPost).CurrentValues.SetValues(post);//应用多对多链接修改dbContext.Set().UpdateLinks(电脑=>pc.PostId, post.Id,电脑=>pc.CategoryId,post.PostCategories.Select(pc => pc.CategoryId));//将所有更改应用到数据库等待 dbContext.SaveChangesAsync();返回现有的帖子;}

解决方案

处理断开链接实体时的主要挑战是检测和应用添加和删除的链接.EF Core(截至撰写本文时)对此几乎没有帮助.

来自链接的答案是可以的(自定义 Except 方法对于它所做的 IMO 来说太重了),但它有一些陷阱 - 必须使用eager 提前检索现有链接/显式加载(尽管使用 EF Core 2.1 延迟加载可能不是问题),并且新链接应该只填充 FK 属性 - 如果它们包含引用导航属性,EF Core 将在调用 添加/AddRange.

不久前我回答了类似但略有不同的问题 - 通用更新 EFCore 连接的方法.以下是答案中自定义泛型扩展方法的更通用和优化的版本:

公共静态类EFCoreExtensions{public static void UpdateLinks(this DbSet dbSet,表达式<Func<TLink,TFromId>>fromIdProperty, TFromId fromId,表达式<Func<TLink,TToId>>toIdProperty, IEnumerabletoIds)其中 TLink :类,新(){//链接 =>link.FromId == fromId表达<Func<TFromId>>fromIdVar = () =>从Id;var filter = Expression.Lambda(Expression.Equal(fromIdProperty.Body, fromIdVar.Body),fromIdProperty.Parameters);var existingLinks = dbSet.AsTracking().Where(filter);var toIdSet = new HashSet(toIds);如果(toIdSet.Count == 0){//新集合为空-删除所有现有链接dbSet.RemoveRange(existingLinks);返回;}//删除新集合中不存在的现有链接var toIdSelector = toIdProperty.Compile();foreach (var existingLink in existingLinks){如果 (!toIdSet.Remove(toIdSelector(existingLink)))dbSet.Remove(existingLink);}//为新集合中的剩余项目创建新链接如果(toIdSet.Count == 0)返回;//toId =>新 TLink { FromId = fromId, ToId = toId }var toIdParam = Expression.Parameter(typeof(TToId), "toId");var createLink = Expression.Lambda>(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.表达式/委托构建的性能影响应该可以忽略不计,如果需要可以缓存.

这个想法是只传递一个链接实体的现有密钥和另一个链接实体的现有密钥列表.因此,根据您要更新的链接实体链接的不同,它的名称会有所不同.

在您的示例中,假设您收到 IEnumerablepostCategories,流程大概是这样的:

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

请注意,此方法允许您更改要求并接受 IEnumerable;postCategoryIds:

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

IEnumerable;postCategories:

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

或类似的 DTO/ViewModel.

分类帖子可以以类似的方式更新,交换相应的选择器.

更新:如果你收到一个(可能)修改过的Post post实体实例,整个更新过程如下:

//查找(从数据库加载)现有帖子var existingPost = await dbContext.Posts.SingleOrDefaultAsync(p => p.Id == post.Id);if (existingPost == null){//处理无效调用返回;}//应用原始属性修改dbContext.Entry(existingPost).CurrentValues.SetValues(post);//应用多对多链接修改dbContext.Set().UpdateLinks(pc => pc.PostId, post.Id,电脑=>pc.CategoryId, post.PostCategories.Select(pc => pc.CategoryId));//将所有更改应用到数据库等待 dbContext.SaveChangesAsync();

请注意,EF Core 使用单独的数据库查询来预先加载相关集合.由于helper 方法的作用相同,因此在从数据库中检索主实体时无需Include 链接相关数据.

In entity framework core 2.0, I have many-many relationship between Post and Category (the binding class is PostCategory).

When the user updates a Post, the whole Post object (with its PostCategory collection) is being sent to the server, and here I want to reassign the new received Collection PostCategory (the user may change this Collection significantly by adding new categories, and removing some categories).

Simplified code I use to update that collection (I just assign completely new 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();

This new collection has objects with the same Id of objects in the previous collection (e.g. the user removed some (but not all) categories). Because of the, I get an exception:

System.InvalidOperationException: The instance of entity type 'PostCategory' cannot be tracked because another instance with the same key value for {'CategoryId', 'PostId'} is already being tracked.

How can I rebuild the new collection (or simply assign a new collection) efficiently without getting this exception?

UPDATE

The answer in this link seems to be related to what I want, but it is a good and efficient method? Is there any possible better approach?

UPDATE 2

I get my post (to edit overwrite its values) like this:

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

UPDATE 3 (The code in my controller, which tries to update the post):

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.

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

解决方案

The main challenge when working with disconnect link entities is to detect and apply the added and deleted links. And EF Core (as of the time of writing) provides little if no help to do that.

The answer from the link is ok (the custom Except method is too heavier for what it does IMO), but it has some traps - the existing links has to be retrieved in advance using the eager / explicit loading (though with EF Core 2.1 lazy loading that might not be an issue), and the new links should have only FK properties populated - if they contain reference navigation properties, EF Core will try to create new linked entities when calling Add / AddRange.

A while ago I answered similar, but slightly different question - Generic method for updating EFCore joins. Here is the more generalized and optimized version of the custom generic extension method from the answer:

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

It uses a single database query to retrieve the exiting links from the database. The overhead are few dynamically built expressions and compiled delegates (in order to keep the calling code simplest as possible) and a single temporary HashSet for fast lookup. The performance affect of the expression / delegate building should be negligible, and can be cached if needed.

The idea is to pass just a single existing key for one of the linked entities and list of exiting keys for the other linked entity. So depending of which of the linked entity links you are updating, it will be called differently.

In you sample, assuming you are receiving IEnumerable<PostCategory> postCategories, the process would be something like this:

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

Note that this method allows you to change the requirement and accept IEnumerable<int> postCategoryIds:

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

or IEnumerable<Category> postCategories:

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

or similar DTOs / ViewModels.

Category posts can be updated in a similar manner, with corresponding selectors swapped.

Update: In case you a receiving a (potentially) modified Post post entity instance, the whole update procedure cold be like this:

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

Note that EF Core uses separate database query for eager loading related collecttions. Since the helper method does the same, there is no need to Include link related data when retrieving the main entity from the database.

这篇关于如何通过分配新集合来更新多对多集合?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆