首页 文章

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

提问于
浏览
97

这两个实体是一对多的关系(由代码第一流畅的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; }
}

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

public void Update(UpdateParentModel model)
{
    //what should be done here?
}

目前我有两个想法:

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

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

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

7 回答

  • 7

    在这种情况下,尝试使用LINQ支持的ForEach循环:

    public void Update(ParentModel model, string newData)
    {
        var parent = _dbContext.Parents
            .Where(p => p.Id == model.Id)
            .Include(p => p.Children)
            .FirstOrDefault();
    
        if(parent != null){
           //update parent
             ...
          //update children
          if(parent.Children != null && parent.Children.Any()){
             parent.Children.ForEach(x => x.Data = newData);
          }
    
       _dbContext.SaveChanges();
    }
    
  • 146

    因为发布到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();
        }
    }
    

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

  • 0

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

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

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

    UpdateChildCollection(dbCopy, detached, p => p.MyCollectionProp, collectionItem => collectionItem.Id)
    

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

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

  • 0

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

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

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

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

  • 1

    有一些项目可以使客户端和服务器之间的交互更容易,只要它涉及保存整个对象图 .

    这里有两个你想看的:

    上述两个项目都会在断开的实体返回服务器时识别它们,检测并保存更改,并返回受客户端影响的数据 .

  • 0

    只是 proof of concept Controler.UpdateModel 将无法正常工作 .

    全班here

    const string PK = "Id";
    protected Models.Entities con;
    protected System.Data.Entity.DbSet<T> model;
    
    private void TestUpdate(object item)
    {
        var props = item.GetType().GetProperties();
        foreach (var prop in props)
        {
            object value = prop.GetValue(item);
            if (prop.PropertyType.IsInterface && value != null)
            {
                foreach (var iItem in (System.Collections.IEnumerable)value)
                {
                    TestUpdate(iItem);
                }
            }
        }
    
        int id = (int)item.GetType().GetProperty(PK).GetValue(item);
        if (id == 0)
        {
            con.Entry(item).State = System.Data.Entity.EntityState.Added;
        }
        else
        {
            con.Entry(item).State = System.Data.Entity.EntityState.Modified;
        }
    
    }
    
  • 3

    好,朋友们 . 我曾经有过这个答案,但一路上就丢了 . 当你知道有更好的方法却无法记住是否找到它时,绝对折磨!这很简单 . 我刚试过多种方式 .

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

    您可以用新的列表替换整个列表! SQL代码将根据需要删除和添加实体 . 无需关心自己 . 一定要包括儿童收集或没有骰子 . 祝好运!

相关问题