하나 이상의 외래 키 속성이 널 입력 가능하지 않으므로 관계를 변경할 수 없습니다


192

엔터티에서 GetById ()를 가져온 다음 자식 엔터티 컬렉션을 MVC보기에서 가져온 새 목록으로 설정하면이 오류가 발생합니다.

작업이 실패했습니다. 하나 이상의 외래 키 속성이 Null을 허용하지 않으므로 관계를 변경할 수 없습니다. 관계가 변경되면 관련 외래 키 속성이 null 값으로 설정됩니다. 외래 키가 null 값을 지원하지 않으면 새 관계를 정의하거나 외래 키 속성에 null이 아닌 다른 값을 할당하거나 관련이없는 개체를 삭제해야합니다.

나는이 줄을 이해하지 못한다.

하나 이상의 외래 키 특성이 널 입력 가능하지 않기 때문에 관계를 변경할 수 없습니다.

두 엔티티 간의 관계를 변경하는 이유는 무엇입니까? 전체 응용 프로그램의 수명 동안 동일하게 유지되어야합니다.

예외가 발생하는 코드는 컬렉션의 수정 된 자식 클래스를 기존 부모 클래스에 간단하게 할당하는 것입니다. 이것은 자식 클래스를 제거하고 새로운 클래스를 추가하고 수정하기를 희망합니다. Entity Framework가 이것을 처리한다고 생각했을 것입니다.

코드 줄을 다음과 같이 증류 할 수 있습니다.

var thisParent = _repo.GetById(1);
thisParent.ChildItems = modifiedParent.ChildItems();
_repo.Save();

아래 기사에서 솔루션 # 2를 사용하여 답변을 찾았습니다. 기본적으로 부모 테이블을 참조하기 위해 자식 테이블에 기본 키를 추가했습니다 (따라서 2 개의 기본 키 (부모 테이블의 외래 키 및 ID) ) 자식 테이블. c-sharpcorner.com/UploadFile/ff2f08/...
yougotiger

@jaffa, 나는 발견 여기 내 대답 stackoverflow.com/questions/22858491/...
안토니오

답변:


159

오래된 하위 항목 thisParent.ChildItems을 하나씩 수동으로 삭제 해야합니다. Entity Framework는 그렇게하지 않습니다. 결국 이전 하위 항목으로 수행 할 작업을 결정할 수 없습니다. 항목을 버릴 경우 또는 다른 상위 항목에 보관하고 할당하려는 경우. Entity Framework에 결정을 알려야합니다. 그러나 외래 키 제약 조건으로 인해 자식 엔터티가 데이터베이스의 부모를 참조하지 않고 혼자 살 수 없기 때문에 결정해야 할 두 가지 결정 중 하나입니다. 이것이 기본적으로 예외가 말하는 것입니다.

편집하다

하위 항목을 추가, 업데이트 및 삭제할 수있는 경우 수행 할 작업 :

public void UpdateEntity(ParentItem parent)
{
    // Load original parent including the child item collection
    var originalParent = _dbContext.ParentItems
        .Where(p => p.ID == parent.ID)
        .Include(p => p.ChildItems)
        .SingleOrDefault();
    // We assume that the parent is still in the DB and don't check for null

    // Update scalar properties of parent,
    // can be omitted if we don't expect changes of the scalar properties
    var parentEntry = _dbContext.Entry(originalParent);
    parentEntry.CurrentValues.SetValues(parent);

    foreach (var childItem in parent.ChildItems)
    {
        var originalChildItem = originalParent.ChildItems
            .Where(c => c.ID == childItem.ID && c.ID != 0)
            .SingleOrDefault();
        // Is original child item with same ID in DB?
        if (originalChildItem != null)
        {
            // Yes -> Update scalar properties of child item
            var childEntry = _dbContext.Entry(originalChildItem);
            childEntry.CurrentValues.SetValues(childItem);
        }
        else
        {
            // No -> It's a new child item -> Insert
            childItem.ID = 0;
            originalParent.ChildItems.Add(childItem);
        }
    }

    // Don't consider the child items we have just added above.
    // (We need to make a copy of the list by using .ToList() because
    // _dbContext.ChildItems.Remove in this loop does not only delete
    // from the context but also from the child collection. Without making
    // the copy we would modify the collection we are just interating
    // through - which is forbidden and would lead to an exception.)
    foreach (var originalChildItem in
                 originalParent.ChildItems.Where(c => c.ID != 0).ToList())
    {
        // Are there child items in the DB which are NOT in the
        // new child item collection anymore?
        if (!parent.ChildItems.Any(c => c.ID == originalChildItem.ID))
            // Yes -> It's a deleted child item -> Delete
            _dbContext.ChildItems.Remove(originalChildItem);
    }

    _dbContext.SaveChanges();
}

참고 : 이것은 테스트되지 않았습니다. 하위 항목 컬렉션이 유형이라고 가정합니다 ICollection. (보통 IList코드가 다르고 약간 다르게 보입니다.) 또한 모든 저장소 추상화를 제거하여 간단하게 유지했습니다.

이것이 좋은 해결책인지는 모르겠지만 탐색 모음의 모든 종류의 변경 사항을 처리하려면 이러한 선을 따라 일종의 노력을 기울여야한다고 생각합니다. 또한 더 쉬운 방법을 보게되어 기쁩니다.


일부만 변경하면 어떻게됩니까? 그래도 여전히 제거하고 다시 추가해야합니까?
자파

@Jon : 아니요. 기존 항목도 업데이트 할 수 있습니다. 하위 컬렉션을 업데이트하는 방법에 대한 예를 추가했습니다 (위의 편집 섹션 참조).
Slauma


@Ladislav : 아니요, 아니요. 본인이 직접 답변 한 것을 기쁘게 생각합니다. 적어도 나는 그것이 내가 말한 것처럼 말도 안되고 너무 복잡하지 않다는 것을 안다.
Slauma

1
foreach에서 originalChildItem을 검색 할 때 조건을 추가합니다. ... Where (c => c.ID == childItem.ID && c.ID! = 0) 그렇지 않으면 childItem.ID 인 경우 새로 추가 된 자식을 반환합니다. == 0.
perfect_element

116

당신이 직면하는 이유는 구성집계 의 차이 때문 입니다.

컴포지션에서 자식 개체는 부모가 만들어 질 때 만들어지고 부모가 파괴 될 때 소멸 됩니다. 따라서 수명은 부모에 의해 제어됩니다. 예 : 블로그 게시물 및 댓글. 게시물이 삭제되면 해당 댓글이 삭제되어야합니다. 존재하지 않는 게시물에 대한 의견이있는 것은 이치에 맞지 않습니다. 주문 및 주문 항목도 동일합니다.

집계에서 하위 오브젝트는 상위 오브젝트에 관계없이 존재할 수 있습니다 . 부모가 파괴 된 경우 나중에 다른 부모에 추가 될 수 있으므로 자식 개체가 여전히 존재할 수 있습니다. 예 : 재생 목록과 해당 재생 목록의 노래 사이의 관계. 재생 목록이 삭제되면 노래가 삭제되지 않아야합니다. 다른 재생 목록에 추가 될 수 있습니다.

Entity Framework가 집계 및 컴포지션 관계를 차별화하는 방법은 다음과 같습니다.

  • 구성의 경우 : 하위 오브젝트에 복합 기본 키 (ParentID, ChildID)가 있어야합니다. 이것은 어린이의 ID가 부모의 범위 내에 있어야하기 때문에 의도적으로 설계된 것입니다.

  • 집계의 경우 : 하위 오브젝트의 외부 키 특성이 널 입력 가능할 것으로 예상합니다.

따라서이 문제가 발생하는 이유는 자식 테이블에서 기본 키를 설정 한 방법 때문입니다. 복합적이어야하지만 그렇지 않습니다. 따라서 Entity Framework는이 연관을 집계로 간주하므로 하위 개체를 제거하거나 지울 때 하위 레코드가 삭제되지 않습니다. 단순히 연결을 제거하고 해당 외래 키 열을 NULL로 설정합니다 (따라서 해당 자식 레코드는 나중에 다른 부모와 연결될 수 있음). 열이 NULL을 허용하지 않으므로 언급 한 예외가 발생합니다.

솔루션 :

1- 복합 키를 사용하지 않을 강력한 이유가있는 경우 하위 오브젝트를 명시 적으로 삭제해야합니다. 그리고 이것은 앞에서 제안한 솔루션보다 간단하게 수행 할 수 있습니다.

context.Children.RemoveRange(parent.Children);

2- 그렇지 않으면 자식 테이블에 적절한 기본 키를 설정하면 코드가 더 의미있게 보입니다.

parent.Children.Clear();

9
이 설명이 가장 유용하다는 것을 알았습니다.
Booji Boy

7
구성 대 집계 및 엔티티 프레임 워크와의 관계에 대한 좋은 설명.
번데기

# 1은 문제를 해결하는 데 필요한 최소량의 코드입니다. 감사합니다!
ryanulit

73

이것은 매우 큰 문제입니다. 실제로 코드에서 일어나는 일은 다음과 같습니다.

  • Parent데이터베이스에서 로드 하고 첨부 된 엔티티를 얻습니다.
  • 하위 컬렉션을 분리 된 하위 컬렉션으로 교체
  • 변경 사항을 저장하지만이 작업 중에는 EF가이 시점까지 알지 못했기 때문에 모든 하위 항목이 추가 된 것으로 간주됩니다 . 따라서 EF는 이전 하위의 외래 키에 null을 설정하고 모든 새 하위 => 중복 행을 삽입하려고합니다.

이제 해결책은 실제로 당신이하고 싶은 것과 어떻게하고 싶습니까?

ASP.NET MVC를 사용하는 경우 UpdateModel 또는 TryUpdateModel을 사용해보십시오 .

기존 하위 항목을 수동으로 업데이트하려면 다음과 같이하면됩니다.

foreach (var child in modifiedParent.ChildItems)
{
    context.Childs.Attach(child); 
    context.Entry(child).State = EntityState.Modified;
}

context.SaveChanges();

부착은 실제로 필요하지 않습니다 (상태를 Modified 엔티티를 연결하도록) 프로세스가 더 명확 해지 기 때문에 좋아합니다.

기존 항목을 수정하고 기존 항목을 삭제하고 새 자식을 삽입하려면 다음과 같은 작업을 수행해야합니다.

var parent = context.Parents.GetById(1); // Make sure that childs are loaded as well
foreach(var child in modifiedParent.ChildItems)
{
    var attachedChild = FindChild(parent, child.Id);
    if (attachedChild != null)
    {
        // Existing child - apply new values
        context.Entry(attachedChild).CurrentValues.SetValues(child);
    }
    else
    {
        // New child
        // Don't insert original object. It will attach whole detached graph
        parent.ChildItems.Add(child.Clone());
    }
}

// Now you must delete all entities present in parent.ChildItems but missing
// in modifiedParent.ChildItems
// ToList should make copy of the collection because we can't modify collection
// iterated by foreach
foreach(var child in parent.ChildItems.ToList())
{
    var detachedChild = FindChild(modifiedParent, child.Id);
    if (detachedChild == null)
    {
        parent.ChildItems.Remove(child);
        context.Childs.Remove(child); 
    }
}

context.SaveChanges();

1
그러나 사용에 대한 흥미로운 말이 .Clone()있습니다. 에 ChildItem다른 하위 하위 탐색 속성이 있다는 것을 명심 하십니까? 그러나이 경우 하위 하위 전체가 컨텍스트에 첨부되는 것을 원하지 않을 것입니다. 왜냐하면 하위 자체가 새로운 경우 하위 하위가 모두 새 객체가 될 것으로 기대하기 때문입니다. (글쎄, 모델마다 다를 수 있지만, 자녀가 부모로부터 의존하는 것처럼 하위 자녀가 자녀로부터 "의존적"인 경우를 가정 해 봅시다.
Slauma

아마도 "스마트"클론이 필요할 것입니다.
Ladislav Mrnka

1
상황에 따라 아동 소장품을 갖고 싶지 않다면 어떻게합니까? http://stackoverflow.com/questions/20233994/do-i-need-to-create-a-dbset-for-every-table-so-that-i-can-persist-child-entitie
Kirsten Greed

1
parent.ChildItems.Remove (자식); context.Childs.Remove (child); 이 이중 제거 수정으로 인해 문제가 발생할 수 있습니다. 왜 둘 다 제거해야합니까? childs가 childs로만 살기 때문에 parent.ChildItems 만 제거하는 이유는 무엇입니까?
페르난도 토레스

40

답변 동일한 오류에 훨씬 도움 된다는 것을 알았습니다 . 제거 할 때 EF가 마음에 들지 않는 것 같습니다. 삭제를 선호합니다.

이와 같이 레코드에 첨부 된 레코드 콜렉션을 삭제할 수 있습니다.

order.OrderDetails.ToList().ForEach(s => db.Entry(s).State = EntityState.Deleted);

이 예에서 주문에 첨부 된 모든 상세 레코드는 상태가 삭제로 설정되어 있습니다. (주문 업데이트의 일부로 업데이트 된 세부 정보를 다시 추가 할 준비가 되었음)


나는 그것이 정답이라고 믿는다.
desmati

논리적이고 간단한 솔루션.
sairfan

19

다른 두 답변이 왜 그렇게 인기가 있는지 모르겠습니다!

나는 당신이 ORM 프레임 워크가 그것을 처리해야한다고 가정했을 때 옳았다 고 생각합니다. 결국 그것이 그것이 약속하는 것입니다. 그렇지 않으면 지속성 문제로 인해 도메인 모델이 손상됩니다. 캐스케이드 설정을 올바르게 설정하면 NHibernate가이를 행복하게 관리합니다. Entity Framework에서는 데이터베이스 모델을 설정할 때, 특히 계단식으로 수행해야 할 작업을 유추해야 할 때 더 나은 표준을 따르기를 기대할 수 있습니다.

" 식별 관계 "를 사용하여 부모-자식 관계를 올바르게 정의 해야합니다 .

이렇게하면 Entity Framework는 자식 개체가 부모 개체로 식별 되므로 "캐스케이드 삭제-고아"상황이어야합니다.

상기 이외의, 당신이 에 필요한 (NHibernate에 경험에서)

thisParent.ChildItems.Clear();
thisParent.ChildItems.AddRange(modifiedParent.ChildItems);

목록을 완전히 바꾸는 대신.

최신 정보

@Slauma의 의견은 분리 된 엔티티가 전반적인 문제의 또 다른 부분임을 상기시켜주었습니다. 이를 해결하기 위해 컨텍스트에서 모델을로드하여 모델을 구성하는 사용자 정의 모델 바인더를 사용하는 방법을 사용할 수 있습니다. 이 블로그 게시물 은 내가 의미하는 바의 예를 보여줍니다.


문제의 시나리오가 분리 된 엔티티 ( "MVC보기에서 가져온 새 목록") 를 처리해야하기 때문에 식별 관계로 설정하는 것이 도움이되지 않습니다 . 여전히 DB에서 원래 자식을로드하고 분리 된 컬렉션을 기반으로 해당 컬렉션에서 제거 된 항목을 찾은 다음 DB에서 제거해야합니다. 유일한 차이점은 식별 관계를 parent.ChildItems.Remove사용하여 대신에 전화 할 수 있다는 것입니다 _dbContext.ChildItems.Remove. 다른 답변의 코드와 같이 긴 코드를 피하기 위해 EF의 내장 지원은 여전히 ​​없습니다 (EF <= 6).
Slauma

나는 당신의 요점을 이해합니다. 그러나 컨텍스트에서 엔터티를로드하거나 위의 방법으로 작동하는 새 인스턴스를 반환하는 사용자 지정 모델 바인더를 사용합니다. 그 해결책을 제안하기 위해 대답을 업데이트 할 것입니다.
Andre Luus

예, 모델 바인더를 사용할 수 있지만 이제 모델 바인더의 다른 답변에서 작업을 수행해야합니다. 리포지토리 / 서비스 계층에서 모델 바인더로 문제를 옮깁니다. 적어도 실제 단순화는 보이지 않습니다.
Slauma

단순화 된 개체는 고아 개체를 자동으로 삭제하는 것입니다. 모델 바인더에 필요한 것은 다음과 같습니다.return context.Items.Find(id) ?? new Item()
Andre Luus

EF 팀에 대한 좋은 피드백이지만 귀하의 제안 된 솔루션으로 불행히도 EF 육상의 어떤 것도 해결되지 않습니다.
Chris Moschini

9

동일한 클래스에서 Entity Framework와 함께 AutoMapper를 사용하는 경우이 문제가 발생할 수 있습니다. 예를 들어 수업이

class A
{
    public ClassB ClassB { get; set; }
    public int ClassBId { get; set; }
}

AutoMapper.Map<A, A>(input, destination);

두 속성을 모두 복사하려고합니다. 이 경우 ClassBId는 Null을 허용하지 않습니다. AutoMapper가 복사하기 때문에destination.ClassB = input.ClassB; 문제가 발생합니다.

AutoMapper를 무시 ClassB속성으로 설정하십시오 .

 cfg.CreateMap<A, A>()
     .ForMember(m => m.ClassB, opt => opt.Ignore()); // We use the ClassBId

AutoMapper와 비슷한 문제가 발생하지만 작동하지 않습니다. ( stackoverflow.com/q/41430679/613605
J86

4

방금 같은 오류가있었습니다. 부모 자식 관계가있는 두 개의 테이블이 있지만 자식 테이블의 테이블 정의에서 외래 키 열에 "삭제시 캐스케이드"를 구성했습니다. 따라서 데이터베이스에서 부모 행을 수동으로 삭제하면 (SQL을 통해) 자식 행이 자동으로 삭제됩니다.

그러나 이것은 EF에서 작동하지 않았 으며이 스레드에 설명 된 오류가 나타났습니다. 그 이유는 엔티티 데이터 모델 (edmx 파일)에서 상위 테이블과 하위 테이블 간의 연관 특성이 올바르지 않기 때문입니다. End1 OnDelete옵션으로 구성되었습니다none (내 모델에서 "END1은"1의 다양성이있는 끝).

End1 OnDelete옵션을 수동으로 변경 한 Cascade것보다 변경 했습니다. 데이터베이스에서 모델을 업데이트 할 때 EF가 왜 이것을 선택할 수 없는지 모르겠습니다 (데이터베이스 첫 모델이 있음).

완성도를 높이려면 다음과 같이 코드를 삭제하십시오.

   public void Delete(int id)
    {
        MyType myObject = _context.MyTypes.Find(id);

        _context.MyTypes.Remove(myObject);
        _context.SaveChanges(); 
   }    

연속 삭제를 정의하지 않은 경우 상위 행을 삭제하기 전에 하위 행을 수동으로 삭제해야합니다.


4

하위 엔터티가 삭제 대신 수정 됨으로 표시되어 있기 때문입니다.

그리고 EF parent.Remove(child)가 실행될 때 자식 엔티티에 대해 수행하는 수정 은 단순히 부모에 대한 참조를로 설정하는 것 null입니다.

실행 후 예외가 발생하면 Visual Studio의 직접 실행 창에 다음 코드를 입력하여 자식의 EntityState를 확인할 수 있습니다 SaveChanges().

_context.ObjectStateManager.GetObjectStateEntries(System.Data.EntityState.Modified).ElementAt(X).Entity

여기서 X는 삭제 된 엔티티로 대체되어야합니다.

ObjectContextto execute에 액세스 할 수없는 경우 _context.ChildEntity.Remove(child)외래 키를 자식 테이블의 기본 키의 일부로 만들어이 문제를 해결할 수 있습니다.

Parent
 ________________
| PK    IdParent |
|       Name     |
|________________|

Child
 ________________
| PK    IdChild  |
| PK,FK IdParent |
|       Name     |
|________________|

이런 식 parent.Remove(child)으로을 실행 하면 EF가 엔티티를 삭제됨으로 올바르게 표시합니다.


2

이 유형의 솔루션은 나를 위해 속임수를 사용했습니다.

Parent original = db.Parent.SingleOrDefault<Parent>(t => t.ID == updated.ID);
db.Childs.RemoveRange(original.Childs);
updated.Childs.ToList().ForEach(c => original.Childs.Add(c));
db.Entry<Parent>(original).CurrentValues.SetValues(updated);

이렇게하면 모든 레코드가 삭제되고 다시 삽입됩니다. 그러나 내 경우 (10 미만)는 괜찮습니다.

도움이 되길 바랍니다.


재 삽입은 새로운 ID로 발생합니까 아니면 처음에 자녀의 ID를 유지합니까?
Pepito Fernandez

2

오늘이 문제가 발생하여 솔루션을 공유하고 싶었습니다. 필자의 경우 데이터베이스에서 부모를 가져 오기 전에 자식 항목을 삭제하는 것이 해결책이었습니다.

이전에는 아래 코드와 같이하고있었습니다. 그런 다음이 질문에 동일한 오류가 표시됩니다.

var Parent = GetParent(parentId);
var children = Parent.Children;
foreach (var c in children )
{
     Context.Children.Remove(c);
}
Context.SaveChanges();

나를 위해 일한 것은 parentId (외국 키)를 사용하여 하위 항목을 먼저 가져온 다음 해당 항목을 삭제하는 것입니다. 그런 다음 데이터베이스에서 부모를 가져올 수 있으며 그 시점에서 더 이상 자식 항목이 없어야하며 새 자식 항목을 추가 할 수 있습니다.

var children = GetChildren(parentId);
foreach (var c in children )
{
     Context.Children.Remove(c);
}
Context.SaveChanges();

var Parent = GetParent(parentId);
Parent.Children = //assign new entities/items here

2

ChildItems 컬렉션을 수동으로 지우고 새 항목을 추가해야합니다.

thisParent.ChildItems.Clear();
thisParent.ChildItems.AddRange(modifiedParent.ChildItems);

그런 다음 고아 엔터티를 처리 할 DeleteOrphans 확장 메서드를 호출 할 수 있습니다 (DetectChanges 및 SaveChanges 메서드간에 호출해야 함).

public static class DbContextExtensions
{
    private static readonly ConcurrentDictionary< EntityType, ReadOnlyDictionary< string, NavigationProperty>> s_navPropMappings = new ConcurrentDictionary< EntityType, ReadOnlyDictionary< string, NavigationProperty>>();

    public static void DeleteOrphans( this DbContext source )
    {
        var context = ((IObjectContextAdapter)source).ObjectContext;
        foreach (var entry in context.ObjectStateManager.GetObjectStateEntries(EntityState.Modified))
        {
            var entityType = entry.EntitySet.ElementType as EntityType;
            if (entityType == null)
                continue;

            var navPropMap = s_navPropMappings.GetOrAdd(entityType, CreateNavigationPropertyMap);
            var props = entry.GetModifiedProperties().ToArray();
            foreach (var prop in props)
            {
                NavigationProperty navProp;
                if (!navPropMap.TryGetValue(prop, out navProp))
                    continue;

                var related = entry.RelationshipManager.GetRelatedEnd(navProp.RelationshipType.FullName, navProp.ToEndMember.Name);
                var enumerator = related.GetEnumerator();
                if (enumerator.MoveNext() && enumerator.Current != null)
                    continue;

                entry.Delete();
                break;
            }
        }
    }

    private static ReadOnlyDictionary<string, NavigationProperty> CreateNavigationPropertyMap( EntityType type )
    {
        var result = type.NavigationProperties
            .Where(v => v.FromEndMember.RelationshipMultiplicity == RelationshipMultiplicity.Many)
            .Where(v => v.ToEndMember.RelationshipMultiplicity == RelationshipMultiplicity.One || (v.ToEndMember.RelationshipMultiplicity == RelationshipMultiplicity.ZeroOrOne && v.FromEndMember.GetEntityType() == v.ToEndMember.GetEntityType()))
            .Select(v => new { NavigationProperty = v, DependentProperties = v.GetDependentProperties().Take(2).ToArray() })
            .Where(v => v.DependentProperties.Length == 1)
            .ToDictionary(v => v.DependentProperties[0].Name, v => v.NavigationProperty);

        return new ReadOnlyDictionary<string, NavigationProperty>(result);
    }
}

이것은 나를 위해 잘 작동했습니다. 방금 추가해야했습니다 context.DetectChanges();.
Andy Edinborough

1

나는이 솔루션들과 다른 많은 것들을 시도했지만 그중 아무것도 해결되지 않았습니다. 이것이 Google의 첫 번째 답변이므로 여기에 솔루션을 추가하겠습니다.

나를 위해 잘 작동 한 방법은 커밋 중에 그림에서 관계를 제거하는 것이 었으므로 EF가 망치는 것은 아무것도 없었습니다. DBContext에서 부모 객체를 다시 찾아서 삭제 하여이 작업을 수행했습니다. 다시 찾은 개체의 탐색 속성이 모두 null이므로 커밋 중에 자식 관계가 무시됩니다.

var toDelete = db.Parents.Find(parentObject.ID);
db.Parents.Remove(toDelete);
db.SaveChanges();

이것은 외래 키가 ON DELETE CASCADE로 설정되어 있다고 가정하므로 부모 행을 제거하면 자식이 데이터베이스에 의해 정리됩니다.


1

Mosh의 솔루션을 사용 했지만 코드에서 컴포지션 키를 올바르게 구현하는 방법은 분명하지 않았습니다.

솔루션은 다음과 같습니다.

public class Holiday
{
    [Key, Column(Order = 0), DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    public int HolidayId { get; set; }
    [Key, Column(Order = 1), ForeignKey("Location")]
    public LocationEnum LocationId { get; set; }

    public virtual Location Location { get; set; }

    public DateTime Date { get; set; }
    public string Name { get; set; }
}

1

나는 같은 문제가 있었지만 다른 경우에는 정상적으로 작동한다는 것을 알았으므로 문제를 다음과 같이 줄였습니다.

parent.OtherRelatedItems.Clear();  //this worked OK on SaveChanges() - items were being deleted from DB
parent.ProblematicItems.Clear();   // this was causing the mentioned exception on SaveChanges()
  • OtherRelatedItems 에는 복합 기본 키 (parentId + 일부 로컬 열)가 있고 정상적으로 작동했습니다.
  • ProblematicItems 에는 자체 단일 열 기본 키가 있으며 parentId는 FK 일뿐 입니다. Clear () 후에 예외가 발생했습니다.

부모가 없으면 자녀가 존재할 수 없음을 나타 내기 위해 ParentId를 복합 PK의 일부로 만들면 됩니다. DB-first 모델을 사용하고 PK를 추가하고 parentId 열을 EntityKey로 표시했습니다 (따라서 DB와 EF에서 모두 업데이트해야했습니다. EF만으로도 충분하지는 않습니다).

PK의 RequestId를 만들었습니다. 그런 다음 EF 모델을 업데이트하고 다른 특성을 엔티티 키의 일부로 설정하십시오.

일단 당신이 그것에 대해 생각하면, EF가 부모없이 아이들이 "이해"하는지 결정하기 위해 EF가 사용하는 것은 매우 우아한 구별입니다 (이 경우 Clear ()는 부모 / 다른 것을 특별한 것으로 설정하지 않으면 예외를 던지지 않습니다. ) 또는 원래 질문과 마찬가지로 항목이 상위 항목에서 삭제되면 항목이 삭제 될 것으로 예상합니다.


0

이 문제는 부모 테이블을 삭제하려고하는데 여전히 자식 테이블 데이터가 있습니다. 계단식 삭제를 통해 문제를 해결합니다.

dbcontext 클래스의 Create 메소드 모델.

 modelBuilder.Entity<Job>()
                .HasMany<JobSportsMapping>(C => C.JobSportsMappings)
                .WithRequired(C => C.Job)
                .HasForeignKey(C => C.JobId).WillCascadeOnDelete(true);
            modelBuilder.Entity<Sport>()
                .HasMany<JobSportsMapping>(C => C.JobSportsMappings)
                  .WithRequired(C => C.Sport)
                  .HasForeignKey(C => C.SportId).WillCascadeOnDelete(true);

그 후 API 호출에서

var JobList = Context.Job                       
          .Include(x => x.JobSportsMappings)                                     .ToList();
Context.Job.RemoveRange(JobList);
Context.SaveChanges();

계단식 삭제 옵션은이 간단한 코드로 상위 및 상위 관련 하위 테이블을 삭제합니다. 이 간단한 방법으로 시도하십시오.

데이터베이스에서 레코드 목록을 삭제하는 데 사용 된 범위 제거 감사


0

또한 Mosh의 대답으로 내 문제를 해결 했으며 PeterB의 대답 은 열거 형을 외래 키로 사용했기 때문에 약간 의 답변 이라고 생각 했습니다. 이 코드를 추가 한 후 새 마이그레이션을 추가해야합니다.

다른 솔루션을 위해이 블로그 게시물을 추천 할 수도 있습니다.

http://www.kianryan.co.uk/2013/03/alone-child/

암호:

public class Child
{
    [Key, Column(Order = 0), DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    public int Id { get; set; }

    public string Heading { get; set; }
    //Add other properties here.

    [Key, Column(Order = 1)]
    public int ParentId { get; set; }

    public virtual Parent Parent { get; set; }
}

0

Slauma 솔루션을 사용하여 자식 개체 및 자식 개체 컬렉션을 업데이트하는 데 도움이되는 몇 가지 일반적인 기능을 만들었습니다.

모든 영구 객체가이 인터페이스를 구현합니다.

/// <summary>
/// Base interface for all persisted entries
/// </summary>
public interface IBase
{
    /// <summary>
    /// The Id
    /// </summary>
    int Id { get; set; }
}

이를 통해 리포지토리 에서이 두 가지 기능을 구현했습니다.

    /// <summary>
    /// Check if orgEntry is set update it's values, otherwise add it
    /// </summary>
    /// <param name="set">The collection</param>
    /// <param name="entry">The entry</param>
    /// <param name="orgEntry">The original entry found in the database (can be <code>null</code> is this is a new entry)</param>
    /// <returns>The added or updated entry</returns>
    public T AddOrUpdateEntry<T>(DbSet<T> set, T entry, T orgEntry) where T : class, IBase
    {
        if (entry.Id == 0 || orgEntry == null)
        {
            entry.Id = 0;
            return set.Add(entry);
        }
        else
        {
            Context.Entry(orgEntry).CurrentValues.SetValues(entry);
            return orgEntry;
        }
    }

    /// <summary>
    /// check if each entry of the new list was in the orginal list, if found, update it, if not found add it
    /// all entries found in the orignal list that are not in the new list are removed
    /// </summary>
    /// <typeparam name="T">The type of entry</typeparam>
    /// <param name="set">The database set</param>
    /// <param name="newList">The new list</param>
    /// <param name="orgList">The original list</param>
    public void AddOrUpdateCollection<T>(DbSet<T> set, ICollection<T> newList, ICollection<T> orgList) where T : class, IBase
    {
        // attach or update all entries in the new list
        foreach (T entry in newList)
        {
            // Find out if we had the entry already in the list
            var orgEntry = orgList.SingleOrDefault(e => e.Id != 0 && e.Id == entry.Id);

            AddOrUpdateEntry(set, entry, orgEntry);
        }

        // Remove all entries from the original list that are no longer in the new list
        foreach (T orgEntry in orgList.Where(e => e.Id != 0).ToList())
        {
            if (!newList.Any(e => e.Id == orgEntry.Id))
            {
                set.Remove(orgEntry);
            }
        }
    }

그것을 사용하려면 다음을 수행하십시오.

var originalParent = _dbContext.ParentItems
    .Where(p => p.Id == parent.Id)
    .Include(p => p.ChildItems)
    .Include(p => p.ChildItems2)
    .SingleOrDefault();

// Add the parent (including collections) to the context or update it's values (except the collections)
originalParent = AddOrUpdateEntry(_dbContext.ParentItems, parent, originalParent);

// Update each collection
AddOrUpdateCollection(_dbContext.ChildItems, parent.ChildItems, orgiginalParent.ChildItems);
AddOrUpdateCollection(_dbContext.ChildItems2, parent.ChildItems2, orgiginalParent.ChildItems2);

도움이 되었기를 바랍니다


EXTRA : 별도의 DbContextExtentions (또는 고유 한 컨텍스트 유추) 클래스를 만들 수도 있습니다.

public static void DbContextExtentions {
    /// <summary>
    /// Check if orgEntry is set update it's values, otherwise add it
    /// </summary>
    /// <param name="_dbContext">The context object</param>
    /// <param name="set">The collection</param>
    /// <param name="entry">The entry</param>
    /// <param name="orgEntry">The original entry found in the database (can be <code>null</code> is this is a new entry)</param>
    /// <returns>The added or updated entry</returns>
    public static T AddOrUpdateEntry<T>(this DbContext _dbContext, DbSet<T> set, T entry, T orgEntry) where T : class, IBase
    {
        if (entry.IsNew || orgEntry == null) // New or not found in context
        {
            entry.Id = 0;
            return set.Add(entry);
        }
        else
        {
            _dbContext.Entry(orgEntry).CurrentValues.SetValues(entry);
            return orgEntry;
        }
    }

    /// <summary>
    /// check if each entry of the new list was in the orginal list, if found, update it, if not found add it
    /// all entries found in the orignal list that are not in the new list are removed
    /// </summary>
    /// <typeparam name="T">The type of entry</typeparam>
    /// <param name="_dbContext">The context object</param>
    /// <param name="set">The database set</param>
    /// <param name="newList">The new list</param>
    /// <param name="orgList">The original list</param>
    public static void AddOrUpdateCollection<T>(this DbContext _dbContext, DbSet<T> set, ICollection<T> newList, ICollection<T> orgList) where T : class, IBase
    {
        // attach or update all entries in the new list
        foreach (T entry in newList)
        {
            // Find out if we had the entry already in the list
            var orgEntry = orgList.SingleOrDefault(e => e.Id != 0 && e.Id == entry.Id);

            AddOrUpdateEntry(_dbContext, set, entry, orgEntry);
        }

        // Remove all entries from the original list that are no longer in the new list
        foreach (T orgEntry in orgList.Where(e => e.Id != 0).ToList())
        {
            if (!newList.Any(e => e.Id == orgEntry.Id))
            {
                set.Remove(orgEntry);
            }
        }
    }
}

다음과 같이 사용하십시오.

var originalParent = _dbContext.ParentItems
    .Where(p => p.Id == parent.Id)
    .Include(p => p.ChildItems)
    .Include(p => p.ChildItems2)
    .SingleOrDefault();

// Add the parent (including collections) to the context or update it's values (except the collections)
originalParent = _dbContext.AddOrUpdateEntry(_dbContext.ParentItems, parent, originalParent);

// Update each collection
_dbContext.AddOrUpdateCollection(_dbContext.ChildItems, parent.ChildItems, orgiginalParent.ChildItems);
_dbContext.AddOrUpdateCollection(_dbContext.ChildItems2, parent.ChildItems2, orgiginalParent.ChildItems2);

다음 함수를 사용하여 상황에 맞는 확장 클래스를 만들 수도 있습니다.
Bluemoon74

0

일부 문제가 발생한 것보다 내 레코드를 삭제하려고 할 때 동일한 문제에 직면했습니다.이 문제 해결 방법은 헤더 / 마스터 레코드를 삭제하기 전에 누락 된 것보다 레코드를 삭제할 때 코드에 작성해야한다는 것입니다 헤더 / 마스터 전에 세부 정보를 삭제하십시오. 문제가 해결되기를 바랍니다.


-1

몇 시간 전에이 문제를 만났고 모든 것을 시도했지만 내 경우에는 솔루션이 위의 목록과 다릅니다.

데이터베이스에서 이미 검색된 엔터티를 사용하고 하위 항목을 수정하려고하면 오류가 발생하지만 데이터베이스에서 엔터티의 새로운 복사본을 얻는 경우 아무런 문제가 없습니다. 이것을 사용하지 마십시오 :

 public void CheckUsersCount(CompanyProduct companyProduct) 
 {
     companyProduct.Name = "Test";
 }

이것을 사용하십시오 :

 public void CheckUsersCount(Guid companyProductId)
 {
      CompanyProduct companyProduct = CompanyProductManager.Get(companyProductId);
      companyProduct.Name = "Test";
 }
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.