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

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

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


중복 질문에 GraphDiff와도 예를 참조 stackoverflow.com/questions/29351401/...
마이클 Freidgeim에게

답변:


219

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


35
그러나 왜 ef가 더 "뛰어난"방식을 가지고 있지 않은가? 나는 ef가 자식이 수정 / 삭제 / 추가되었는지 감지 할 수 있다고 생각합니다. IMO 위의 코드는 EF 프레임 워크의 일부가되어보다 일반적인 솔루션이 될 수 있습니다.
Cheng Chen

7
@DannyChen : 실제로 연결이 끊긴 엔터티를 업데이트하는 것이 EF에 의해보다 편안한 방식으로 지원되어야한다는 오랜 요청 이었지만 ( Entityframework.codeplex.com/workitem/864 ) 여전히 프레임 워크의 일부는 아닙니다. 현재는 해당 코드 플렉스 작업 항목에 언급 된 타사 라이브러리 "GraphDiff"만 시도하거나 위의 답변과 같이 수동 코드를 작성할 수 있습니다.
Slauma

7
한 가지 추가 할 사항 : foreach of update 및 insert 자식 내에서 existingParent.Children.Add(newChild)기존 자식 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 Go

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)

불행히도 자식 유형에 컬렉션 속성이 있고 업데이트 해야하는 경우이 종류가 적용됩니다. UpdateChildCollection 자체를 호출하는 IRepository (기본 CRUD 메소드 사용)를 전달하여이 문제를 해결하려고합니다. DbContext.Entry에 대한 직접 호출 대신 repo를 호출합니다.

이것이 어떻게 대규모로 수행되는지는 모르지만이 문제와 어떤 관련이 있는지 잘 모릅니다.


1
훌륭한 솔루션! 그러나 하나 이상의 새 항목을 추가하면 업데이트 된 사전에 ID가 두 번있을 수 없습니다. 몇 가지 작업이 필요합니다. 관계가 N-> N 인 경우에도 실패합니다. 실제로 항목이 데이터베이스에 추가되지만 N-> N 테이블은 수정되지 않습니다.
RenanStr

1
toAdd.ForEach(i => (selector(dbItem) as ICollection<Tchild>).Add(i.Value));n-> n 문제를 해결해야합니다.
RenanStr

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가 처음에 테이블에서 모든 원래 자식을 삭제 한 다음 모든 새 자식을 추가한다고 가정하면 성능에 문제가되지 않습니다.
William T. Mallard

@ 찰스 매킨토시. 초기 쿼리에 어린이를 포함시키면서 왜 어린이를 다시 설정했는지 이해가되지 않습니다.
pantonis

1
@pantonis 편집을 위해로드 할 수 있도록 자식 컬렉션을 포함시킵니다. 게으른 로딩에 의존하여 작동하지 않는 경우. 컬렉션에 항목을 수동으로 삭제하고 추가하는 대신 단순히 목록을 바꾸고 엔티티 프레임 워크가 항목을 추가하고 삭제하기 때문에 자식 (한 번)을 설정했습니다. 핵심은 엔터티의 상태를 수정으로 설정하고 엔터티 프레임 워크가 무거운 작업을 수행하도록하는 것입니다.
찰스 맥킨토시

@CharlesMcIntosh 나는 아직도 당신이 거기에서 아이들과 함께 달성하려는 것을 이해하지 못합니다. 첫 번째 요청에 포함 시켰습니다 (Include (p => p.Children). 왜 다시 요청합니까?
pantonis

@pantonis, .include ()를 사용하여 이전 목록을 가져와 데이터베이스에서 컬렉션으로로드하고 첨부해야했습니다. 게으른 로딩이 호출되는 방식입니다. 그렇지 않으면 entitystate.modified를 사용할 때 목록의 변경 사항이 추적되지 않습니다. 다시 말하지만, 내가하고있는 일은 현재 자식 컬렉션을 다른 자식 컬렉션으로 설정하는 것입니다. 관리자가 많은 신입 사원을 얻거나 몇 명을 잃어버린 경우처럼 쿼리를 사용하여 새 직원을 포함하거나 제외하고 이전 목록을 새 목록으로 바꾸고 EF가 데이터베이스 측에서 필요에 따라 추가하거나 삭제할 수 있도록합니다.
찰스 맥킨토시

9

EntityFrameworkCore를 사용하는 경우 컨트롤러 사후 조치에서 다음을 수행 할 수 있습니다 ( 첨부 메소드 는 콜렉션을 포함하여 탐색 특성을 반복적으로 첨부 합니다 ).

_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;
        }
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.