【问题标题】:Entity Framework - Replace child collection using a detached model实体框架 - 使用分离模型替换子集合
【发布时间】:2021-05-15 11:34:06
【问题描述】:

我在 Entity A 和 Entity B 之间有一个多对多的关系。Entity Framework 在运行迁移后自动在 SQL Server 中创建了一个联结表。 (我没有在代码的任何地方定义这个联结表。)例如:

class EntityA
{
   // ...
   public ICollection<EntityB> Foo { get; set; }
}

class EntityB
{
   // ...
   public ICollection<EntityA> Bar { get; set; }
}

我需要使用来自客户端应用程序的(分离的)列表替换EntityA 上的Foo 集合。我花了一天的大部分时间试图弄清楚这一点。这是我尝试过的:

[HttpPut]
public async Task<IActionResult> Update(EntityA someEntity)
{
        var entry = context.EntityA.Attach(someEntity);
        entry.State = EntityState.Modified;

        var collection = entry.Collection(x => x.Foo);
        collection.IsModified = true;

        await context.SaveChangesAsync();
}

我也尝试过更改collectionCurrentValue 属性,显然我也尝试过直接替换Foo,但似乎没有任何效果——连接表仍然是空的。怎样才能完全替换这个子列表而不必Include() / 将整个列表加载到内存中以进行手动跟踪/删除?

【问题讨论】:

  • 替换是什么意思。要删除以前的 Foo 并分配一个全新的实体 B 的列表,或者只是从实体 A 中删除以前的 foo 并添加现有实体 B 的列表?
  • 新的 Foo 列表可以包含新实体和现有实体(不在其中的都是已删除的实体)。我想最简单的处理方法是删除数据库中的所有内容并将其替换为全新的列表。
  • 如果不从数据库加载原始列表,EF Core 无法知道如何处理修改后的列表。顺便说一句,你也不是。并且没有没有加载的“删除某些东西”命令。这就是 EF 的工作方式 - 加载/修改/保存。您可以从docs.microsoft.com/en-us/ef/core/extensions 列表中尝试一些扩展,例如Detached Mapper。它可能会帮助您不编写样板代码,但我怀疑它不会“为您”加载原始集合,因为没有其他方法。

标签: entity-framework entity-framework-core


【解决方案1】:

Ivan(在上面的 cmets 中)是对的。经过反复试验,我最终编写了一个适用于我的案例的扩展方法。在开始之前,我想感谢this answer 为我指明了正确的方向,我最终对其进行了修改以使其与自动生成的 EF 联结表一起使用。一、扩展方法:

    // assuming your models inherit from a base class or implement an interface
    public interface IEntity
    {
        Guid Id { get; set; } // or int or whatever your ID field is
    }

    public static class DbExtensions
    {
        // Updates the many-to-many child collections of an entity (for an auto-generated EF junction table)
        public static async Task UpdateJunctionTableAsync<T, Y>(this DbContext baseContext, T entity, Expression<Func<T, IEnumerable<Y>>> property)
            where T : class, IEntity
            where Y : class, IEntity
        {
            // scope these calls to a new context -- working off the base context
            // tends to cause issues down the line with the change tracking
            using var context = new DbContext();

            // EF internally compares with DB entities, so we'll do the same
            var dbEntity = await context.FindAsync<T>(entity.Id);
            var dbEntry = context.Entry(dbEntity);

            // access the collection entry that resulted in a junction table
            var dbItemsEntry = dbEntry.Collection(property);

            // get its associated CLR collection accessor
            var accessor = dbItemsEntry.Metadata.GetCollectionAccessor();

            // load the entry's items
            await dbItemsEntry.LoadAsync();

            // build a dictionary to track what needs to be added vs removed
            var dbItemsMap = dbItemsEntry.CurrentValue.ToDictionary(e => e.Id);

            // get the current items in the entity (not DB)
            var items = (IEnumerable<Y>)accessor.GetOrCreate(entity, false);

            // add them to the DB as needed
            foreach (var item in items)
            {
                // if this already exists, no need to process it.
                if (dbItemsMap.ContainsKey(item.Id))
                    dbItemsMap.Remove(item.Id);
                else
                {
                    // otherwise, add a tracked version of it.
                    context.Set<Y>().Attach(item);
                    accessor.Add(dbEntity, item, false);
                }
            }

            // anything still left here has been deleted from the entity
            foreach (var oldItem in dbItemsMap.Values)
                accessor.Remove(dbEntity, oldItem);

            // we have to clear the junction table from the incoming model's collection,
            // otherwise EF will try to attach to it again, which will cause errors
            // further down the line
            var memberSelectorExpression = property.Body as MemberExpression;
            if (memberSelectorExpression != null)
            {
                var propertyInfo = memberSelectorExpression.Member as PropertyInfo;
                if (propertyInfo != null)
                    propertyInfo.SetValue(entity, null, null);
            }

            await context.SaveChangesAsync();
        }
    }

使用很简单:

        [HttpPut]
        public async Task<IActionResult> UpdateFoo(EntityA model)
        {
            // update the junction table first
            await context.UpdateJunctionTableAsync(model, x => x.Foo);

            // then update whatever else you want
            // e.g., if we were updating the whole row:
            // context.EntityA.Attach(model).State = EntityState.Modified;

            // save
            await context.SaveChangesAsync();

            return Ok();
        }

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2012-03-27
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-07-03
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多