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


151

这两个实体是一对多关系(由代码优先流利的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?
}

目前我有两个想法:

  1. 获得命名为跟踪父实体existingmodel.Id中,并指定值model由一个实体之一。听起来很蠢。而且model.Children我不知道哪个孩子是新孩子,哪个孩子被修改(甚至删除)。

  2. 通过创建一个新的父实体model,并将其附加到DbContext并保存。但是DbContext如何知道子状态(新的添加/删除/修改)?

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


Answers:


219

因为发布到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可以接受任何对象,并根据属性名称将属性值映射到附加的实体。如果模型中的属性名称与实体中的名称不同,则不能使用此方法,必须逐个分配值。


35
但是,为什么ef没有更“辉煌”的方式呢?我认为ef可以检测到孩子是否被修改/删除/添加,IMO上面的代码可以成为EF框架的一部分,并成为更通用的解决方案。
Cheng Chen

7
@DannyChen:确实很长的要求是,EF应该以更舒适的方式来支持更新断开连接的实体(entityframework.codeplex.com/workitem/864),但是它仍然不是框架的一部分。当前,您只能尝试该codeplex工作项中提到的第三方库“ GraphDiff”,或者像上面我的回答一样编写手动代码。
Slauma 2014年

7
要添加的一件事:在更新和插入子项的foreach中,您无法执行此操作,existingParent.Children.Add(newChild)因为这时现有的Child linq搜索将返回最近添加的实体,因此该实体将被更新。您只需要插入一个临时列表,然后添加即可。
Erre Efe

3
@RandolfRincónFadul我刚刚遇到了这个问题。我的解决方法是省力一些,它是更改existingChildLINQ查询中的where子句:.Where(c => c.ID == childModel.ID && c.ID != default(int))
Gavin Ward

2
@RalphWillgoss您正在谈论的2.2中的修复程序是什么?
Jan Paolo

11

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

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方法)来解决此问题,该方法将负责自行调用UpdateChildCollection。将调用存储库,而不是直接调用DbContext.Entry。

不知道这将如何大规模执行,但是不确定该问题还有什么其他用途。


1
很好的解决方案!但是,如果添加多个新项目失败,则更新的字典不能两次具有零ID。需要一些工作。如果关系为N-> N,也会失败,实际上,该项目已添加到数据库,但N-> N表未修改。
RenanStr '18年

1
toAdd.ForEach(i => (selector(dbItem) as ICollection<Tchild>).Add(i.Value));应该解决n-> n问题。
RenanStr '18

10

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

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代码将根据需要删除和添加实体。无需为此担心。确保包括儿童收集或没有骰子。祝好运!


正是我所需要的,因为模型中的子代数通常很小,因此假设Linq首先会从表中删除所有原始子代,然后添加所有新的子代,那么性能影响就不成问题了。
威廉·T·马拉德

@查尔斯·麦金托什(Charles McIntosh)。我不理解为什么您在初始查询中包括子项时再次设置子项?
pantonis

1
@pantonis我包括子集合,以便可以加载它进行编辑。如果我依靠延迟加载来找出它是行不通的。我设置子项(一次)是因为,无需手动删除和向集合中添加项目,我只需替换列表,entityframework将为我添加和删除项目。关键是将实体的状态设置为已修改,并允许实体框架进行繁重的工作。
查尔斯·麦金托什

@CharlesMcIntosh我仍然不明白你想与那里的孩子一起实现什么。您将其包含在第一个请求中(Include(p => p.Children)。为什么要再次请求它?
pantonis

@pantonis,我不得不使用.include()拉出旧列表,以便从数据库中将其加载并附加为集合。这就是调用延迟加载的方式。没有它,当我使用entitystate.modified时,将不会跟踪列表的任何更改。重申一下,我正在做的是将当前子集合设置为其他子集合。就像经理有一大堆新员工或丢了几个。我将使用查询来包括或排除那些新雇员,并简单地用新列表替换旧列表,然后让EF根据需要从数据库端添加或删除。
查尔斯·麦金托什

9

如果使用的是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();

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

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


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

这就是我解决这个问题的方法。这样,EF知道要添加的内容进行更新。


像魅力一样工作!谢谢。
Inktkiller

2

就保存整个对象图而言,有一些项目可以使客户端和服务器之间的交互更加容易。

这是您要查看的两个:

上面的两个项目都需要在断开连接的实体返回到服务器后对其进行识别,检测并保存更改,然后返回受客户端影响的数据。


1

只是概念证明 Controler.UpdateModel无法正常工作。

满级的在这里

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

}

0

@Charles McIntosh确实为我的情况提供了答案,因为传入的模型已分离。对我来说,最终有效的方法是先保存传入的模型……然后像以前一样继续添加子代:

public async Task<IHttpActionResult> GetUPSFreight(PartsExpressOrder order)
{
    db.Entry(order).State = EntityState.Modified;
    db.SaveChanges();
  ...
}

0

VB.NET开发人员使用此通用子标记子状态,易于使用

笔记:

  • PromatCon:实体对象
  • amList:是要添加或修改的子列表
  • rList:是要删除的子列表
updatechild(objCas.ECC_Decision, PromatCon.ECC_Decision.Where(Function(c) c.rid = objCas.rid And Not objCas.ECC_Decision.Select(Function(x) x.dcid).Contains(c.dcid)).toList)
Sub updatechild(Of Ety)(amList As ICollection(Of Ety), rList As ICollection(Of Ety))
        If amList IsNot Nothing Then
            For Each obj In amList
                Dim x = PromatCon.Entry(obj).GetDatabaseValues()
                If x Is Nothing Then
                    PromatCon.Entry(obj).State = EntityState.Added
                Else
                    PromatCon.Entry(obj).State = EntityState.Modified
                End If
            Next
        End If

        If rList IsNot Nothing Then
            For Each obj In rList.ToList
                PromatCon.Entry(obj).State = EntityState.Deleted
            Next
        End If
End Sub
PromatCon.SaveChanges()

0
var parent = context.Parent.FirstOrDefault(x => x.Id == modelParent.Id);
if (parent != null)
{
  parent.Childs = modelParent.Childs;
}

资源


0

这是我的代码,效果很好。

public async Task<bool> UpdateDeviceShutdownAsync(Guid id, DateTime shutdownAtTime, int areaID, decimal mileage,
        decimal motohours, int driverID, List<int> commission,
        string shutdownPlaceDescr, int deviceShutdownTypeID, string deviceShutdownDesc,
        bool isTransportation, string violationConditions, DateTime shutdownStartTime,
        DateTime shutdownEndTime, string notes, List<Guid> faultIDs )
        {
            try
            {
                using (var db = new GJobEntities())
                {
                    var isExisting = await db.DeviceShutdowns.FirstOrDefaultAsync(x => x.ID == id);

                    if (isExisting != null)
                    {
                        isExisting.AreaID = areaID;
                        isExisting.DriverID = driverID;
                        isExisting.IsTransportation = isTransportation;
                        isExisting.Mileage = mileage;
                        isExisting.Motohours = motohours;
                        isExisting.Notes = notes;                    
                        isExisting.DeviceShutdownDesc = deviceShutdownDesc;
                        isExisting.DeviceShutdownTypeID = deviceShutdownTypeID;
                        isExisting.ShutdownAtTime = shutdownAtTime;
                        isExisting.ShutdownEndTime = shutdownEndTime;
                        isExisting.ShutdownStartTime = shutdownStartTime;
                        isExisting.ShutdownPlaceDescr = shutdownPlaceDescr;
                        isExisting.ViolationConditions = violationConditions;

                        // Delete children
                        foreach (var existingChild in isExisting.DeviceShutdownFaults.ToList())
                        {
                            db.DeviceShutdownFaults.Remove(existingChild);
                        }

                        if (faultIDs != null && faultIDs.Any())
                        {
                            foreach (var faultItem in faultIDs)
                            {
                                var newChild = new DeviceShutdownFault
                                {
                                    ID = Guid.NewGuid(),
                                    DDFaultID = faultItem,
                                    DeviceShutdownID = isExisting.ID,
                                };

                                isExisting.DeviceShutdownFaults.Add(newChild);
                            }
                        }

                        // Delete all children
                        foreach (var existingChild in isExisting.DeviceShutdownComissions.ToList())
                        {
                            db.DeviceShutdownComissions.Remove(existingChild);
                        }

                        // Add all new children
                        if (commission != null && commission.Any())
                        {
                            foreach (var cItem in commission)
                            {
                                var newChild = new DeviceShutdownComission
                                {
                                    ID = Guid.NewGuid(),
                                    PersonalID = cItem,
                                    DeviceShutdownID = isExisting.ID,
                                };

                                isExisting.DeviceShutdownComissions.Add(newChild);
                            }
                        }

                        await db.SaveChangesAsync();

                        return true;
                    }
                }
            }
            catch (Exception ex)
            {
                logger.Error(ex);
            }

            return false;
        }
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.