IT박스

EF에서 상위 항목을 업데이트 할 때 하위 항목을 추가 / 업데이트하는 방법

itboxs 2020. 7. 2. 08:04
반응형

EF에서 상위 항목을 업데이트 할 때 하위 항목을 추가 / 업데이트하는 방법


두 엔티티는 일대 다 관계입니다 (코드 우선 유창한 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. 명명 추적 부모 개체 가져 오기 existing에 의해 model.Id, 그리고 할당 값 model엔티티에 하나 하나. 바보 같네요 그리고 model.Children나는 어떤 자녀가 새로운 것인지, 어떤 자녀가 수정 또는 삭제되었는지 알 수 없습니다.

  2. 을 통해 새 부모 엔터티를 model만들어 DbContext에 첨부 한 후 저장합니다. 그러나 DbContext는 어떻게 아이들의 상태를 알 수 있습니까 (새로운 추가 / 삭제 / 수정)?

이 기능을 구현하는 올바른 방법은 무엇입니까?


WebApi 컨트롤러에 게시 된 모델이 EF (Entity-Framework) 컨텍스트에서 분리되었으므로 유일한 옵션은 데이터베이스에서 오브젝트 그래프 (자식 포함)를로드하고 추가, 삭제 또는 추가 된 하위를 비교하는 것입니다. 업데이트되었습니다. (내 의견으로는 다음보다 더 복잡한 분리 상태 (브라우저 또는 어디서나) 중에 자체 추적 메커니즘으로 변경 사항을 추적하지 않는 한 다음과 같습니다.)

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모든 객체를 가져 와서 속성 이름을 기반으로 속성 값을 연결된 엔터티에 매핑 할 수 있습니다. 모델의 속성 이름이 엔터티의 이름과 다른 경우이 방법을 사용할 수 없으며 값을 하나씩 지정해야합니다.


나는 이런 식으로 엉망이되었습니다 ...

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)

Unfortunately, this kinda falls over if there are collection properties on the child type which also need to be updated. Considering trying to solve this by passing an IRepository (with basic CRUD methods) which would be responsible for calling UpdateChildCollection on its own. Would call the repo instead of direct calls to DbContext.Entry.

Have no idea how this will all perform at scale, but not sure what else to do with this problem.


If you are using EntityFrameworkCore you can do the following in your controller post action (The Attach method recursively attaches navigation properties including collections):

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

It is assumed that each entity that was updated has all properties set and provided in the post data from the client (eg. won't work for partial update of an entity).

You also need to make sure that you are using a new/dedicated entity framework database context for this operation.


OK guys. I had this answer once but lost it along the way. absolute torture when you know there's a better way but can't remember it or find it! It's very simple. I just tested it multiple ways.

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

You can replace the whole list with a new one! The SQL code will remove and add entities as needed. No need to concern yourself with that. Be sure to include child collection or no dice. Good luck!


There are a few projects out there that make the interaction between the client and the server easier as far as it concerns saving an entire object graph.

Here are two you'd want to look at:

Both the projects above take recognize the disconnected entities when it's returned to the server, detect and save the changes, and return to the client affected data.


Just proof of concept Controler.UpdateModel won't work correctly.

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

}

참고URL : https://stackoverflow.com/questions/27176014/how-to-add-update-child-entities-when-updating-a-parent-entity-in-ef

반응형