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

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

问题描述

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

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

当用户更新 Post 时,整个 Post 对象(及其 PostCategory 集合)都将发送到服务器,在这里我想重新分配新收到的收藏集 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();

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

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:无法跟踪实体类型'PostCategory'的实例,因为具有

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?

此链接中的答案似乎与我想要的内容有关,但这是一个很好的选择有效的方法?有什么更好的方法吗?

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?

我明白了(编辑覆盖其值)像这样:

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(我控制器中的代码,试图更新该帖子) :



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.



更新4:



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


推荐答案

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

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.

链接的答案是可以的(自定义 Except 方法对于IMO而言太重了),但它有一些陷阱-必须使用急切/显式加载来预先检索现有链接(尽管对于EF Core 2.1延迟加载可能不是问题),并且新链接应仅填充FK属性-如果它们包含参考导航属性,EF Core将在调用 Add / AddRange 时尝试创建新的链接实体。

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.

前一段时间,我回答了类似但略有不同的问题-用于更新EFCore联接的通用方法。这是答案中自定义通用扩展方法的更为通用和优化的版本:

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

它使用单个数据库查询来检索现有链接从数据库中。开销很少是动态构建的表达式和编译的委托(以使调用代码尽可能简单)和单个临时 HashSet 以便快速查找。表达式/委托构建对性能的影响应该可以忽略不计,并且可以在需要时进行缓存。

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.

在您的示例中,假设您收到的是 IEnumerable< PostCategory> postCategories ,过程将如下所示:

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

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

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

IEnumerable< Category" postCategories

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

或类似的DTO / ViewModel。

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

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

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天全站免登陆