如何在EF中更新父实体时添加/更新子实体

Che*_*hen 134 c# asp.net-mvc entity-framework asp.net-web-api

这两个实体是一对多的关系(由代码第一流畅的api构建).

public class Parent
{
    public Parent()
    {
        this.Children = new List<Child>();
    }

    public int Id { get; set; }

    public virtual ICollection<Child> Children { get; set; }
}

public class Child
{
    public int Id { get; set; }

    public int ParentId { get; set; }

    public string Data { get; set; }
}
Run Code Online (Sandbox Code Playgroud)

在我的WebApi控制器中,我有动作创建父实体(工作正常)并更新父实体(有一些问题).更新操作如下所示:

public void Update(UpdateParentModel model)
{
    //what should be done here?
}
Run Code Online (Sandbox Code Playgroud)

目前我有两个想法:

  1. 获取一个名为existingby 的跟踪父实体model.Id,并将值model逐个赋值给实体.这听起来很愚蠢.在model.Children我不知道哪个孩子是新的,哪个孩子被修改(甚至删除).

  2. 通过创建一个新的父实体model,并将其附加到DbContext并保存.但是DbContext如何知道子进程的状态(new add/delete/modified)?

实现此功能的正确方法是什么?

Sla*_*uma 195

由于发布到WebApi控制器的模型与任何实体框架(EF)上下文分离,唯一的选择是从数据库加载对象图(父项包括其子项)并比较已添加,删除或更新.(除非您在分离状态(在浏览器中或在任何地方)使用您自己的跟踪机制跟踪更改,否则我认为这些更改比以下更复杂.)它可能如下所示:

public void Update(UpdateParentModel model)
{
    var existingParent = _dbContext.Parents
        .Where(p => p.Id == model.Id)
        .Include(p => p.Children)
        .SingleOrDefault();

    if (existingParent != null)
    {
        // Update parent
        _dbContext.Entry(existingParent).CurrentValues.SetValues(model);

        // Delete children
        foreach (var existingChild in existingParent.Children.ToList())
        {
            if (!model.Children.Any(c => c.Id == existingChild.Id))
                _dbContext.Children.Remove(existingChild);
        }

        // Update and Insert children
        foreach (var childModel in model.Children)
        {
            var existingChild = existingParent.Children
                .Where(c => c.Id == childModel.Id)
                .SingleOrDefault();

            if (existingChild != null)
                // Update child
                _dbContext.Entry(existingChild).CurrentValues.SetValues(childModel);
            else
            {
                // Insert child
                var newChild = new Child
                {
                    Data = childModel.Data,
                    //...
                };
                existingParent.Children.Add(newChild);
            }
        }

        _dbContext.SaveChanges();
    }
}
Run Code Online (Sandbox Code Playgroud)

...CurrentValues.SetValues可以根据属性名称将任何对象和属性值映射到附加实体.如果模型中的属性名称与实体中的名称不同,则无法使用此方法,并且必须逐个分配值.

  • 但为什么ef没有更"辉煌"的方式呢?我认为ef可以检测孩子是否被修改/删除/添加,IMO上面的代码可以成为EF框架的一部分,并成为更通用的解决方案. (24认同)
  • @DannyChen:确实有一个很长的要求,EF应该以更舒适的方式支持更新断开连接的实体(https://entityframework.codeplex.com/workitem/864),但它仍然不是框架的一部分.目前,您只能尝试该codeplex工作项中提到的第三方库"GraphDiff"或编写手动代码,如上面的答案. (7认同)
  • 要添加的一件事:在更新和插入子项的foreach中,你不能执行`existingParent.Children.Add(newChild)`因为那时existingChild linq搜索将返回最近添加的实体,因此该实体将被更新.您只需要插入临时列表然后添加即可. (4认同)
  • @RandolfRincónFadul我刚遇到这个问题.我的修复工作少了一点就是更改`existingChild` LINQ查询中的where子句:`.Where(c => c.ID == childModel.ID && c.ID!= default(int))` (2认同)
  • @RalphWillgoss您正在谈论的2.2中的修复程序是什么? (2认同)

bre*_*man 10

我一直在搞这样的事......

protected void UpdateChildCollection<Tparent, Tid , Tchild>(Tparent dbItem, Tparent newItem, Func<Tparent, IEnumerable<Tchild>> selector, Func<Tchild, Tid> idSelector) where Tchild : class
    {
        var dbItems = selector(dbItem).ToList();
        var newItems = selector(newItem).ToList();

        if (dbItems == null && newItems == null)
            return;

        var original = dbItems?.ToDictionary(idSelector) ?? new Dictionary<Tid, Tchild>();
        var updated = newItems?.ToDictionary(idSelector) ?? new Dictionary<Tid, Tchild>();

        var toRemove = original.Where(i => !updated.ContainsKey(i.Key)).ToArray();
        var removed = toRemove.Select(i => DbContext.Entry(i.Value).State = EntityState.Deleted).ToArray();

        var toUpdate = original.Where(i => updated.ContainsKey(i.Key)).ToList();
        toUpdate.ForEach(i => DbContext.Entry(i.Value).CurrentValues.SetValues(updated[i.Key]));

        var toAdd = updated.Where(i => !original.ContainsKey(i.Key)).ToList();
        toAdd.ForEach(i => DbContext.Set<Tchild>().Add(i.Value));
    }
Run Code Online (Sandbox Code Playgroud)

您可以使用以下内容调用:

UpdateChildCollection(dbCopy, detached, p => p.MyCollectionProp, collectionItem => collectionItem.Id)
Run Code Online (Sandbox Code Playgroud)

不幸的是,如果子类型上还有需要更新的集合属性,这种情况就会失败.考虑尝试通过传递IRepository(使用基本CRUD方法)来解决这个问题,该IRepository将负责自己调用UpdateChildCollection.将调用repo而不是直接调用DbContext.Entry.

不知道这将如何大规模地执行,但不知道还有什么可以解决这个问题.


小智 7

如果您正在使用EntityFrameworkCore,则可以在控制器后期操作中执行以下操作(Attach方法递归地附加导航属性,包括集合):

_context.Attach(modelPostedToController);

IEnumerable<EntityEntry> unchangedEntities = _context.ChangeTracker.Entries().Where(x => x.State == EntityState.Unchanged);

foreach(EntityEntry ee in unchangedEntities){
     ee.State = EntityState.Modified;
}

await _context.SaveChangesAsync();
Run Code Online (Sandbox Code Playgroud)

假设已更新的每个实体具有在来自客户端的发布数据中设置和提供的所有属性(例如,将不用于实体的部分更新).

您还需要确保为此操作使用新的/专用实体框架数据库上下文.

  • 它不会从父集合中删除已删除的实体 (6认同)

Cha*_*osh 7

好了朋友们。我曾经有这个答案,但一路迷路了。当您知道有更好的方法但不记得或找不到它时,绝对折磨!非常简单 我只是对其进行了多种测试。

var parent = _dbContext.Parents
  .Where(p => p.Id == model.Id)
  .Include(p => p.Children)
  .FirstOrDefault();

parent.Children = _dbContext.Children.Where(c => <Query for New List Here>);
_dbContext.Entry(parent).State = EntityState.Modified;

_dbContext.SaveChanges();
Run Code Online (Sandbox Code Playgroud)

您可以将整个列表替换为新列表!SQL代码将根据需要删除和添加实体。无需为此担心。确保包括儿童收集或没有骰子。祝好运!

  • @CharlesMcIntosh ```_dbContext.Children.Where(c =&gt; &lt;在此处查询新列表&gt;);``` 这里不完整的代码是什么? (2认同)

小智 6

public async Task<IHttpActionResult> PutParent(int id, Parent parent)
        {
            if (!ModelState.IsValid)
            {
                return BadRequest(ModelState);
            }

            if (id != parent.Id)
            {
                return BadRequest();
            }

            db.Entry(parent).State = EntityState.Modified;

            foreach (Child child in parent.Children)
            {
                db.Entry(child).State = child.Id == 0 ? EntityState.Added : EntityState.Modified;
            }

            try
            {
                await db.SaveChangesAsync();
            }
            catch (DbUpdateConcurrencyException)
            {
                if (!ParentExists(id))
                {
                    return NotFound();
                }
                else
                {
                    throw;
                }
            }

            return Ok(db.Parents.Find(id));
        }
Run Code Online (Sandbox Code Playgroud)

这就是我解决这个问题的方法。这样,EF 就知道要添加哪些要更新。

  • 删除在哪里?另外,这可以与客户端生成的 GUIDS 一起使用吗? (2认同)

小智 5

这应该可以做到...

private void Reconcile<T>(DbContext context,
    IReadOnlyCollection<T> oldItems,
    IReadOnlyCollection<T> newItems,
    Func<T, T, bool> compare)
{
    var itemsToAdd = new List<T>();
    var itemsToRemove = new List<T>();

    foreach (T newItem in newItems)
    {
        T oldItem = oldItems.FirstOrDefault(arg1 => compare(arg1, newItem));

        if (oldItem == null)
        {
            itemsToAdd.Add(newItem);
        }
        else
        {
            context.Entry(oldItem).CurrentValues.SetValues(newItem);
        }
    }

    foreach (T oldItem in oldItems)
    {
        if (!newItems.Any(arg1 => compare(arg1, oldItem)))
        {
            itemsToRemove.Add(oldItem);
        }
    }

    foreach (T item in itemsToAdd)
        context.Add(item);

    foreach (T item in itemsToRemove)
        context.Remove(item);
}
Run Code Online (Sandbox Code Playgroud)


归档时间:

查看次数:

96196 次

最近记录:

6 年,5 月 前