使用 EF Core 更新通用存储库上的父集合和子集合 [英] Update parent and child collections on generic repository with EF Core

查看:65
本文介绍了使用 EF Core 更新通用存储库上的父集合和子集合的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

假设我有一个 Sale 课程:

Say I have a Sale class:

public class Sale : BaseEntity //BaseEntity only has an Id  
{        
    public ICollection<Item> Items { get; set; }
}

还有一个 Item 类:

public class Item : BaseEntity //BaseEntity only has an Id  
{
    public int SaleId { get; set; }
    public Sale Sale { get; set; }
}

和通用存储库(更新方法):

And a Generic Repository (update method):

    public async Task<int> UpdateAsync<T>(T entity, params Expression<Func<T, object>>[] navigations) where T : BaseEntity
    {
        var dbEntity = _dbContext.Set<T>().Find(entity.Id);

        var dbEntry = _dbContext.Entry(dbEntity);

        dbEntry.CurrentValues.SetValues(entity);            

        foreach (var property in navigations)
        {
            var propertyName = property.GetPropertyAccess().Name;

            await dbEntry.Collection(propertyName).LoadAsync();

            List<BaseEntity> dbChilds = dbEntry.Collection(propertyName).CurrentValue.Cast<BaseEntity>().ToList();

            foreach (BaseEntity child in dbChilds)
            {
                if (child.Id == 0)
                {
                    _dbContext.Entry(child).State = EntityState.Added;
                }
                else
                {
                    _dbContext.Entry(child).State = EntityState.Modified;
                }
            }
        }

        return await _dbContext.SaveChangesAsync();
    }

我在更新 Sale 类上的 Item 集合时遇到困难.使用此代码,我设法addmodify 一个Item.但是,当我delete UI 层上的某个项目时,没有任何内容被删除.

I'm having difficulties to update the Item collection on the Sale class. With this code I managed to add or modify an Item. But, when I delete some item on the UI layer, nothing gets deleted.

EF Core 是否可以在使用通用存储库模式时处理这种情况?

Does EF Core have something to deal with this situation, while using a generic repository patter?

更新

似乎是 Items 跟踪丢失了.这是我的包含包含的通用检索方法.

Seems to be that Items tracking is lost. Here is my generic retrieve method with includes.

    public async Task<T> GetByIdAsync<T>(int id, params Expression<Func<T, object>>[] includes) where T : BaseEntity
    {
        var query = _dbContext.Set<T>().AsQueryable();

        if (includes != null)
        {
            query = includes.Aggregate(query,
              (current, include) => current.Include(include));
        }

        return await query.SingleOrDefaultAsync(e => e.Id == id);
    }

推荐答案

显然问题是应用对 断开连接的实体(否则除了调用 SaveChanges 之外,您无需执行任何其他操作),其中包含需要反映添加/删除的集合导航属性/从传递的对象更新项目.

Apparently the question is for applying modifications of disconnected entity (otherwise you won't need to do anything else than calling SaveChanges) containing collection navigation properties which need to reflect the added/removed/update items from the passed object.

EF Core 不提供这种开箱即用的功能.它支持简单的 对于具有自动生成的键的实体,通过 Update 方法 upsert(插入或更新),但它不会检测和删除已删除的项目.

EF Core does not provide such out of the box capability. It supports simple upsert (insert or update) through Update method for entities with auto-generated keys, but it doesn't detect and delete the removed items.

因此您需要自己进行检测.加载现有项目是朝着正确方向迈出的一步.您的代码的问题在于它没有考虑新项目,而是对从数据库中检索到的现有项目进行一些无用的状态操作.

So you need to do that detection yourself. Loading the existing items is a step in the right direction. The problem with your code is that it doesn't account the new items, but instead is doing some useless state manipulation of the existing items retrieved from the database.

以下是相同想法的正确实现.它使用一些 EF Core 内部结构(GetCollectionAccessor() 方法返回的 IClrCollectionAccessor - 两者都需要 using Microsoft.EntityFrameworkCore.Metadata.Internal;)来操作集合,但您的代码已经在使用内部 GetPropertyAccess() 方法,所以我想这应该不是问题 - 如果将来的某个 EF Core 版本发生更改,代码应该进行相应更新.需要集合访问器,因为虽然 IEnumerable 由于协变可用于一般访问集合,但对于 ICollection 不能说相同,因为它是不变的,我们需要一种方法来访问 Add/Remove 方法.内部访问器提供该功能以及从传递的实体中一般检索属性值的方法.

Following is the correct implementation of the same idea. It uses some EF Core internals (IClrCollectionAccessor returned by the GetCollectionAccessor() method - both require using Microsoft.EntityFrameworkCore.Metadata.Internal;) to manipulate the collection, but your code already is using the internal GetPropertyAccess() method, so I guess that shouldn't be a problem - in case something is changed in some future EF Core version, the code should be updated accordingly. The collection accessor is needed because while IEnumerable<BaseEntity> can be used for generically accessing the collections due to covariance, the same cannot be said about ICollection<BaseEntity> because it's invariant, and we need a way to access Add / Remove methods. The internal accessor provides that capability as well as a way to generically retrieve the property value from the passed entity.

更新:从 EF Core 3.0 开始,GetCollectionAccessorIClrCollectionAccessor 是公共 API 的一部分.

Update: Starting from EF Core 3.0, GetCollectionAccessor and IClrCollectionAccessor are part of the public API.

代码如下:

public async Task<int> UpdateAsync<T>(T entity, params Expression<Func<T, object>>[] navigations) where T : BaseEntity
{
    var dbEntity = await _dbContext.FindAsync<T>(entity.Id);

    var dbEntry = _dbContext.Entry(dbEntity);
    dbEntry.CurrentValues.SetValues(entity);

    foreach (var property in navigations)
    {
        var propertyName = property.GetPropertyAccess().Name;
        var dbItemsEntry = dbEntry.Collection(propertyName);
        var accessor = dbItemsEntry.Metadata.GetCollectionAccessor();

        await dbItemsEntry.LoadAsync();
        var dbItemsMap = ((IEnumerable<BaseEntity>)dbItemsEntry.CurrentValue)
            .ToDictionary(e => e.Id);

        var items = (IEnumerable<BaseEntity>)accessor.GetOrCreate(entity);

        foreach (var item in items)
        {
            if (!dbItemsMap.TryGetValue(item.Id, out var oldItem))
                accessor.Add(dbEntity, item);
            else
            {
                _dbContext.Entry(oldItem).CurrentValues.SetValues(item);
                dbItemsMap.Remove(item.Id);
            }
        }

        foreach (var oldItem in dbItemsMap.Values)
            accessor.Remove(dbEntity, oldItem);
    }

    return await _dbContext.SaveChangesAsync();
}

算法非常标准.从数据库加载集合后,我们创建一个字典,其中包含以 Id 为键的现有项目(用于快速查找).然后我们对新项目进行一次传递.我们使用字典来查找对应的现有项目.如果未找到匹配项,则该项目被视为新项目并简单地添加到目标(跟踪)集合中.否则,找到的项目将从源中更新,并从字典中删除.这样,在完成循环后,字典中包含需要删除的项目,因此我们只需要将它们从目标(跟踪)集合中删除即可.

The algorithm is pretty standard. After loading the collection from the database, we create a dictionary containing the existing items keyed by Id (for fast lookup). Then we do a single pass over the new items. We use the dictionary to find the corresponding existing item. If no match is found, the item is considered new and is simply added to the target (tracked) collection. Otherwise the found item is updated from the source, and removed from the dictionary. This way, after finishing the loop, the dictionary contains the items that needs to be deleted, so all we need is remove them from the target (tracked) collection.

仅此而已.其余工作将由 EF Core 更改跟踪器完成 - 添加到目标集合的项目将标记为 Added,更新的项目 - Unchanged>Modified 和删除的项目,取决于删除级联行为,将被标记为删除或更新(与父级分离).如果要强制删除,只需替换

And that's all. The rest of the work will be done by the EF Core change tracker - the added items to the target collection will be marked as Added, the updated - either Unchanged or Modified, and the removed items, depending of the delete cascade behavior will be either be marked for deletion or update (disassociate from parent). If you want to force deletion, simply replace

accessor.Remove(dbEntity, oldItem);

_dbContext.Remove(oldItem);

这篇关于使用 EF Core 更新通用存储库上的父集合和子集合的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

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