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

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

问题描述

说我有一个销售课:

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

还有一个项目类:

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

我在更新 Item <时遇到困难/ code>销售类中的集合。有了这段代码,我成功地添加修改一个项目 。但是,当我 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?

更新

似乎丢失了项目跟踪。这是我的包含的通用检索方法。

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 -都需要使用Microsoft.EntityFrameworkCore.Metadata.Internal; )来操作集合,但是您的代码已经在使用内部 GetPropertyAccess()方法,因此我猜想这应该不成问题-如果将来的EF Core版本中进行了更改,则应该相应地更新代码。之所以需要集合访问器,是因为由于协方差,虽然 IEnumerable< BaseEntity> 可以用于一般性地访问集合,但是对于 ICollection< BaseEntity> ,因为它是不变的,我们需要一种方法来访问添加 / 删除方法。内部访问器提供了该功能以及从传递的实体中一般检索属性值的方法。

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开始, GetCollectionAccessor IClrCollectionAccessor 的一部分

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更改跟踪器完成-添加到目标集合的项目将标记为已添加,已更新的项目将标记为未更改已修改,并且已删除的项目将根据删除级联行为被标记为删除或更新(与父级取消关联) 。如果要强制删除,只需替换

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

with

_dbContext.Remove(oldItem);

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

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