Nie można zmienić relacji, ponieważ co najmniej jedna właściwość klucza obcego nie ma wartości zerowej

192

Dostaję ten błąd, gdy wykonuję GetById () na encji, a następnie ustawiam kolekcję encji potomnych na mojej nowej liście, która pochodzi z widoku MVC.

Operacja nie powiodła się: Nie można zmienić relacji, ponieważ co najmniej jedna właściwość klucza obcego nie ma wartości zerowej. Po zmianie relacji powiązana właściwość klucza obcego jest ustawiana na wartość zerową. Jeśli klucz obcy nie obsługuje wartości pustych, należy zdefiniować nową relację, właściwość klucza obcego musi mieć przypisaną inną wartość inną niż pusta lub niepowiązany obiekt musi zostać usunięty.

Nie do końca rozumiem tę linię:

Nie można zmienić relacji, ponieważ co najmniej jedna właściwość klucza obcego nie ma wartości zerowej.

Dlaczego miałbym zmieniać relacje między 2 podmiotami? Powinien pozostać taki sam przez cały okres istnienia całej aplikacji.

Kod, na którym występuje wyjątek, polega na prostym przypisaniu zmodyfikowanych klas potomnych w kolekcji do istniejącej klasy macierzystej. Mam nadzieję, że przyczyni się to do usunięcia klas podrzędnych, dodania nowych i modyfikacji. Myślałem, że Entity Framework to załatwi.

Wiersze kodu można przedestylować do:

var thisParent = _repo.GetById(1);
thisParent.ChildItems = modifiedParent.ChildItems();
_repo.Save();
jaffa
źródło
Znalazłem odpowiedź kup za pomocą rozwiązania nr 2 w poniższym artykule, w zasadzie utworzyłem dodany klucz podstawowy do tabeli potomnej w celu odniesienia do tabeli nadrzędnej (więc ma 2 klucze podstawowe (klucz obcy dla tabeli nadrzędnej i identyfikator dla stolika podrzędnego). c-sharpcorner.com/UploadFile/ff2f08/...
yougotiger
@jaffa, znalazłem swoją odpowiedź tutaj stackoverflow.com/questions/22858491/…
antonio

Odpowiedzi:

159

Stare elementy potomne należy usuwać thisParent.ChildItemsręcznie jeden po drugim. Entity Framework tego nie robi. W końcu nie może zdecydować, co chcesz zrobić ze starymi elementami potomnymi - jeśli chcesz je wyrzucić lub jeśli chcesz zachować i przypisać je innym podmiotom nadrzędnym. Musisz poinformować Entity Framework o swojej decyzji. Ale jedną z tych dwóch decyzji musisz podjąć, ponieważ podmioty potomne nie mogą żyć same bez odniesienia do jakiegokolwiek rodzica w bazie danych (z powodu ograniczenia klucza obcego). Tak w zasadzie mówi wyjątek.

Edytować

Co zrobiłbym, gdyby elementy potomne można było dodawać, aktualizować i usuwać:

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

Uwaga: To nie zostało przetestowane. Zakłada się, że kolekcja elementów potomnych jest typu ICollection. (Zwykle mam, ILista potem kod wygląda nieco inaczej.) Usunąłem też wszystkie abstrakty repozytoriów, aby było to proste.

Nie wiem, czy to dobre rozwiązanie, ale uważam, że należy wykonać jakąś ciężką pracę zgodnie z tymi wytycznymi, aby zająć się wszelkiego rodzaju zmianami w kolekcji nawigacji. Byłbym również szczęśliwy widząc łatwiejszy sposób na zrobienie tego.

Slauma
źródło
Co jeśli niektóre zostaną zmienione? Czy to oznacza, że ​​nadal muszę je usunąć i dodać ponownie?
jaffa
@Jon: Nie, oczywiście możesz także aktualizować istniejące elementy. Dodałem przykład, jak prawdopodobnie zaktualizowałbym kolekcję potomną, patrz sekcja Edycja powyżej.
Slauma,
@Slauma: Lol, gdybym wiedział, że zamierzasz zmodyfikować swoją odpowiedź, nie napisałbym mojej odpowiedzi ...
Ladislav Mrnka
@Ladislav: Nie, nie, cieszę się, że napisałeś własną odpowiedź. Teraz przynajmniej wiem, że to nie jest kompletna bzdura i zbyt skomplikowane, co zrobiłem powyżej.
Slauma,
1
Dodałbym warunek przy pobieraniu originalChildItem w foreach: ... Where (c => c.ID == childItem.ID && c.ID! = 0) w przeciwnym razie zwróci nowo dodane dzieci, jeśli childItem.ID == 0.
perfect_element
116

Powodem tego jest różnica między kompozycją a agregacją .

W składzie obiekt podrzędny jest tworzony podczas tworzenia elementu nadrzędnego i jest niszczony, gdy element nadrzędny jest niszczony . Tak więc jego żywotność jest kontrolowana przez jego rodzica. np. post na blogu i jego komentarze. Jeśli post zostanie usunięty, jego komentarze powinny zostać usunięte. Komentarze do postu, który nie istnieje, nie ma sensu. To samo dotyczy zamówień i przedmiotów zamówienia.

W agregacji obiekt podrzędny może istnieć niezależnie od jego obiektu nadrzędnego . Jeśli element nadrzędny zostanie zniszczony, obiekt potomny może nadal istnieć, ponieważ można go później dodać do innego elementu nadrzędnego. np .: związek między listą odtwarzania a utworami na tej liście. Jeśli lista odtwarzania zostanie usunięta, utworów nie należy usuwać. Można je dodać do innej listy odtwarzania.

Sposób, w jaki Entity Framework różnicuje relacje agregacji i kompozycji, jest następujący:

  • Do kompozycji: oczekuje, że obiekt potomny będzie miał złożony klucz podstawowy (ParentID, ChildID). Ma to na celu zaprojektowanie, ponieważ identyfikatory dzieci powinny należeć do ich rodziców.

  • W celu agregacji: oczekuje, że właściwość klucza obcego w obiekcie potomnym będzie pusta.

Powodem tego problemu jest sposób ustawienia klucza podstawowego w tabeli podrzędnej. Powinien być złożony, ale nie jest. Tak więc Entity Framework traktuje to powiązanie jako agregację, co oznacza, że ​​po usunięciu lub wyczyszczeniu obiektów potomnych nie nastąpi usunięcie rekordów potomnych. Po prostu usunie to powiązanie i ustawi odpowiednią kolumnę klucza obcego na NULL (dzięki czemu te rekordy potomne mogą być później powiązane z innym rodzicem). Ponieważ twoja kolumna nie pozwala na NULL, otrzymujesz wspomniany wyjątek.

Rozwiązania:

1- Jeśli masz silny powód, dla którego nie chcesz używać klucza złożonego, musisz jawnie usunąć obiekty potomne. Można to zrobić prościej niż sugerowane wcześniej rozwiązania:

context.Children.RemoveRange(parent.Children);

2- W przeciwnym razie, ustawiając odpowiedni klucz podstawowy na tabeli podrzędnej, kod będzie wyglądał bardziej sensownie:

parent.Children.Clear();
Mosh
źródło
9
Uznałem to wyjaśnienie za najbardziej pomocne.
Booji Boy,
7
Dobre wyjaśnienie składu vs agregacji i tego, w jaki sposób jest powiązany z nim szkielet encji.
Chrysalis
# 1 była najmniejszą ilością kodu niezbędną do rozwiązania problemu. Dziękuję Ci!
ryanulit
73

To bardzo duży problem. To, co faktycznie dzieje się w twoim kodzie, to:

  • Wczytujesz Parentz bazy danych i dostajesz dołączoną jednostkę
  • Zastąpiłeś jego kolekcję potomną nową kolekcją dzieci odłączonych
  • Zapisujesz zmiany, ale podczas tej operacji wszystkie dzieci są uważane za dodane, ponieważ EF nie wiedział o nich do tego czasu. Dlatego EF próbuje ustawić wartość null na klucz obcy starych dzieci i wstawić wszystkie nowe dzieci => zduplikowane wiersze.

Teraz rozwiązanie naprawdę zależy od tego, co chcesz zrobić i jak chcesz to zrobić?

Jeśli używasz ASP.NET MVC, możesz spróbować użyć UpdateModel lub TryUpdateModel .

Jeśli chcesz ręcznie zaktualizować istniejące dzieci, możesz po prostu zrobić coś takiego:

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

context.SaveChanges();

Dołączanie nie jest tak naprawdę potrzebne (ustawienie stanu Modifiedspowoduje również dołączenie encji), ale podoba mi się, ponieważ sprawia, że ​​proces jest bardziej oczywisty.

Jeśli chcesz zmodyfikować istniejące, usuń istniejące i wstaw nowe dzieci, musisz zrobić coś takiego:

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();
Ladislav Mrnka
źródło
1
Ale jest twoja interesująca uwaga na temat używania .Clone(). Czy masz na myśli przypadek, że ChildItemma inne właściwości nawigacji podrzędnej? Ale w takim przypadku, czy nie chcielibyśmy, aby cały pod-graf był dołączony do kontekstu, ponieważ spodziewalibyśmy się, że wszystkie pod-potomki są nowymi obiektami, jeśli samo dziecko jest nowe? (Cóż, może różnić się w zależności od modelu, ale załóżmy, że pod-dzieci są „zależne” od dziecka, tak jak dzieci są zależne od rodzica.)
Slauma,
Prawdopodobnie wymagałoby to „inteligentnego” klonu.
Ladislav Mrnka
1
Co jeśli nie chcesz mieć kolekcji dziecka w swoim kontekście? 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 (child); context.Childs.Remove (child); Naprawiono to podwójne usuwanie, DZIĘKI. Dlaczego potrzebujemy obu usunięć? Dlaczego usunięcie tylko od rodzica.ChildItems nie jest wystarczające, ponieważ dziecko żyje tylko jako dziecko?
Fernando Torres
40

Uważam, że ta odpowiedź jest znacznie bardziej pomocna w przypadku tego samego błędu. Wygląda na to, że EF nie lubi go po usunięciu, woli usunąć.

Możesz usunąć zbiór rekordów dołączonych do takiego rekordu.

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

W tym przykładzie wszystkie rekordy szczegółów dołączone do zamówienia mają stan ustawiony na Usuń. (W przygotowaniu do dodania ponownie zaktualizowanych szczegółów, w ramach aktualizacji zamówienia)

Greg Little
źródło
Uważam, że to właściwa odpowiedź.
desmati
logiczne i proste rozwiązanie.
sairfan
19

Nie mam pojęcia, dlaczego pozostałe dwie odpowiedzi są tak popularne!

Uważam, że miałeś rację, zakładając, że struktura ORM powinna sobie z tym poradzić - w końcu to właśnie obiecuje. W przeciwnym razie Twój model domeny zostanie uszkodzony przez problemy związane z trwałością. NHibernate radzi sobie z tym szczęśliwie, jeśli poprawnie skonfigurujesz ustawienia kaskady. W Entity Framework jest to również możliwe, po prostu oczekują, że będziesz przestrzegać lepszych standardów podczas konfigurowania modelu bazy danych, szczególnie gdy będą musieli wywnioskować, jakie kaskady należy wykonać:

Musisz poprawnie zdefiniować relację rodzic-dziecko , używając „ relacji identyfikującej ”.

Jeśli to zrobisz, Entity Framework wie, że obiekt podrzędny jest identyfikowany przez obiekt nadrzędny, a zatem musi to być sytuacja „kaskadowo-usuń-sieroty”.

Inne niż powyższe, może być konieczne (z doświadczenia NHibernate)

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

zamiast całkowicie zastępować listę.

AKTUALIZACJA

Komentarz Slaumy przypomniał mi, że odłączone byty to kolejna część ogólnego problemu. Aby rozwiązać ten problem, możesz przyjąć podejście polegające na użyciu niestandardowego spoiwa modelu, który konstruuje modele, próbując załadować go z kontekstu. Ten post na blogu pokazuje przykład tego, co mam na myśli.

Andre Luus
źródło
Konfiguracja jako relacja identyfikująca nie pomoże tutaj, ponieważ scenariusz w pytaniu musi dotyczyć odłączonych jednostek ( „moja nowa lista, która pochodzi z widoku MVC” ). Nadal musisz załadować oryginalne elementy potomne z bazy danych, znaleźć usunięte elementy w tej kolekcji na podstawie kolekcji odłączonej, a następnie usunąć z bazy danych. Jedyną różnicą jest to, że w relacji identyfikacyjnej możesz zadzwonić parent.ChildItems.Removezamiast _dbContext.ChildItems.Remove. Nadal (EF <= 6) nie ma wbudowanej obsługi EF, aby uniknąć długiego kodu, takiego jak ten w innych odpowiedziach.
Slauma,
Rozumiem twój punkt widzenia. Uważam jednak, że w przypadku niestandardowego modelu segregatora, który ładuje byt z kontekstu lub zwraca nową instancję, powyższe podejście zadziałałoby. Zaktualizuję moją odpowiedź, aby zasugerować to rozwiązanie.
Andre Luus
Tak, możesz użyć modelowego segregatora, ale musiałeś teraz wykonać czynności z innych odpowiedzi w segregatorze modelowym. Po prostu przenosi problem z warstwy repo / service do spoiwa modelu. Przynajmniej nie widzę prawdziwego uproszczenia.
Slauma,
Uproszczenie polega na automatycznym usuwaniu osieroconych bytów. Wszystko, czego potrzebujesz w modelowym segregatorze, to ogólny odpowiednikreturn context.Items.Find(id) ?? new Item()
Andre Luus
Dobre opinie dla zespołu EF, ale proponowane rozwiązanie nie rozwiązuje niczego w terenie EF.
Chris Moschini
9

Jeśli używasz AutoMapper z Entity Framework na tej samej klasie, możesz napotkać ten problem. Na przykład, jeśli twoja klasa jest

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

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

Spróbuje to skopiować obie właściwości. W takim przypadku ClassBId nie ma wartości Nullable. Ponieważ AutoMapper skopiuje destination.ClassB = input.ClassB;, spowoduje to problem.

Ustaw AutoMapper na Ignoruj ClassBwłaściwość.

 cfg.CreateMap<A, A>()
     .ForMember(m => m.ClassB, opt => opt.Ignore()); // We use the ClassBId
jsgoupil
źródło
Mam podobny problem z AutoMapper, ale to nie działa dla mnie :( Zobacz stackoverflow.com/q/41430679/613605
J86
4

Właśnie miałem ten sam błąd. Mam dwie tabele z rodzicielską relacją potomną, ale skonfigurowałem „kaskadę usuwania” w kolumnie klucza obcego w definicji tabeli potomnej. Kiedy więc ręcznie usunę wiersz macierzysty (przez SQL) w bazie danych, automatycznie usunie on wiersze potomne.

Jednak nie działało to w EF, pojawił się błąd opisany w tym wątku. Powodem tego było to, że w moim modelu danych encji (plik edmx) właściwości powiązania między tabelą nadrzędną i tabelą podrzędną były nieprawidłowe. End1 OnDeleteOpcja została skonfigurowana, aby być none( „koniec 1” w moim modelu jest koniec, który ma wiele 1).

Ręcznie zmieniłem End1 OnDeleteopcję na Cascadei wtedy zadziałało. Nie wiem, dlaczego EF nie jest w stanie tego podnieść, kiedy aktualizuję model z bazy danych (mam pierwszy model bazy danych).

Dla kompletności, tak wygląda mój kod do usunięcia:

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

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

Gdybym nie zdefiniował kasowania kaskadowego, musiałbym ręcznie usunąć wiersze podrzędne przed usunięciem wiersza nadrzędnego.

Jaskółka oknówka
źródło
4

Dzieje się tak, ponieważ element podrzędny jest oznaczony jako Zmodyfikowany zamiast Usunięte.

A modyfikacja, którą EF robi dla elementu potomnego, gdy parent.Remove(child)jest wykonywana, polega po prostu na ustawieniu odwołania na jego element nadrzędny na null.

Możesz sprawdzić EntityState dziecka, wpisując następujący kod w oknie natychmiastowym Visual Studio, gdy wystąpi wyjątek, po wykonaniu SaveChanges():

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

gdzie X należy zastąpić usuniętą jednostką.

Jeśli nie masz dostępu do ObjectContextwykonania _context.ChildEntity.Remove(child), możesz rozwiązać ten problem, czyniąc klucz obcy częścią klucza podstawowego w tabeli potomnej.

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

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

W ten sposób, jeśli wykonasz parent.Remove(child), EF poprawnie oznaczy jednostkę jako usuniętą.

Mauricio Ramalho
źródło
2

Tego rodzaju rozwiązanie zrobiło dla mnie lewę:

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

Ważne jest, aby powiedzieć, że usuwa to wszystkie rekordy i wstawia je ponownie. Ale w moim przypadku (mniej niż 10) jest w porządku.

Mam nadzieję, że to pomoże.

Wagner Bertolini Junior
źródło
Czy ponowne wstawienie ma miejsce w przypadku nowych identyfikatorów, czy przede wszystkim zachowuje identyfikatory dziecka, które mieli?
Pepito Fernandez
2

Zetknąłem się dzisiaj z tym problemem i chciałem podzielić się moim rozwiązaniem. W moim przypadku rozwiązaniem było usunięcie elementów podrzędnych przed pobraniem elementu nadrzędnego z bazy danych.

Wcześniej robiłem to tak, jak w poniższym kodzie. Otrzymam ten sam błąd wymieniony w tym pytaniu.

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

To, co zadziałało, to najpierw pobrać elementy potomne, używając ParentId (klucz obcy), a następnie usunąć te elementy. Następnie mogę pobrać Rodzica z bazy danych iw tym momencie nie powinno już mieć żadnych elementów potomnych i mogę dodawać nowe elementy potomne.

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
Dino Bansigan
źródło
2

Musisz ręcznie wyczyścić kolekcję ChildItems i dołączyć do niej nowe elementy:

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

Następnie możesz wywołać metodę rozszerzenia DeleteOrphans, która będzie obsługiwać osierocone jednostki (musi być wywołana między metodami DetectChanges i 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);
    }
}
Sob
źródło
To działało dla mnie dobrze. Po prostu musiałem dodać context.DetectChanges();.
Andy Edinborough,
1

Wypróbowałem te rozwiązania i wiele innych, ale żadne z nich się nie udało. Ponieważ jest to pierwsza odpowiedź w Google, dodam tutaj moje rozwiązanie.

Metodą, która działała dla mnie dobrze, było usunięcie relacji z obrazu podczas zatwierdzeń, więc EF nie miał nic do zepsucia. Zrobiłem to, ponownie wyszukując obiekt nadrzędny w DBContext i usuwając to. Ponieważ właściwości nawigacyjne ponownie znalezionego obiektu są zerowe, relacje dzieci są ignorowane podczas zatwierdzania.

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

Należy pamiętać, że zakłada to, że klucze obce są skonfigurowane przy użyciu opcji USUŃ KASKADĘ, więc po usunięciu wiersza nadrzędnego dzieci zostaną wyczyszczone przez bazę danych.

Steve
źródło
1

Użyłem rozwiązania Mosha , ale nie było dla mnie oczywiste, jak najpierw poprawnie zaimplementować klucz kompozycji w kodzie.

Oto rozwiązanie:

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; }
}
PeterB
źródło
1

Miałem ten sam problem, ale wiedziałem, że działał OK w innych przypadkach, więc zredukowałem problem do tego:

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 miał złożony klucz podstawowy (parentId + część lokalnej kolumny) i działał OK
  • ProblematicItems miały własny jednokolumnowy klucz podstawowy, a parentId był tylko FK. Powodowało to wyjątek po Clear ().

Wszystko, co musiałem zrobić, to uczynić ParentId częścią złożonej PK, aby wskazać, że dzieci nie mogą istnieć bez rodzica. Użyłem modelu DB-first, dodałem PK i oznaczyłem kolumnę parentId jako EntityKey (więc musiałem zaktualizować go zarówno w DB, jak i EF - nie jestem pewien, czy wystarczy EF).

Uczyniłem RequestId częścią PK A następnie zaktualizował model EF ORAZ ustawił inną właściwość jako część klucza jednostki

Kiedy się nad tym zastanowisz, jest to bardzo eleganckie rozróżnienie, którego EF używa do decydowania, czy dzieci „mają sens” bez rodzica (w tym przypadku Clear () nie usunie ich i nie wyrzuci wyjątku, chyba że ustawisz ParentId na coś innego / specjalnego ) lub - jak w pierwotnym pytaniu - oczekujemy, że elementy zostaną usunięte po usunięciu z elementu nadrzędnego.

Ekus
źródło
0

Ten problem powstaje, ponieważ próbujemy usunąć tabelę nadrzędną, nadal istnieją dane tabeli podrzędnej. Rozwiązujemy problem za pomocą kasowania.

W modelu Utwórz metodę w klasie dbcontext.

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

Następnie w naszym wywołaniu API

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

Opcja usuwania kaskadowego pozwala usunąć tabelę podrzędną nadrzędną i nadrzędną za pomocą tego prostego kodu. Spróbuj w ten prosty sposób.

Usuń zakres, który został użyty do usunięcia listy rekordów w bazie Dzięki

Sowmiya V.
źródło
0

Rozwiązałem również mój problem z odpowiedzią Mosha i pomyślałem, że odpowiedź PeteraB była nieco inna, ponieważ używał wyliczenia jako klucza obcego. Pamiętaj, że musisz dodać nową migrację po dodaniu tego kodu.

Mogę również polecić ten post na blogu, aby znaleźć inne rozwiązania:

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

Kod:

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; }
}
Ogglas
źródło
0

Korzystając z rozwiązania Slauma, stworzyłem ogólne funkcje, które pomagają aktualizować obiekty potomne i kolekcje obiektów potomnych.

Wszystkie moje trwałe obiekty implementują ten interfejs

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

Dzięki temu zaimplementowałem te dwie funkcje w moim repozytorium

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

Aby go użyć, wykonaj następujące czynności:

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

Mam nadzieję że to pomoże


DODATKOWE: Możesz także utworzyć osobną klasę DbContextExtentions (lub własną kontekstową warstwę):

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

i używaj go w następujący sposób:

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
źródło
Możesz również utworzyć klasę rozszerzenia dla swojego kontekstu za pomocą następujących funkcji:
Bluemoon74
0

Miałem ten sam problem, kiedy zamierzam usunąć mój rekord, niż pojawił się jakiś problem, ponieważ w przypadku tego rozwiązania problemu, gdy masz zamiar usunąć swój rekord, niż czegoś brakuje przed usunięciem nagłówka / rekordu głównego, musisz napisać w kodzie dla usuń jego szczegóły przed nagłówkiem / Master Mam nadzieję, że problem zostanie rozwiązany.

Ghazi Hur
źródło
-1

Problem ten spotkałem przed kilkoma godzinami i próbowałem wszystkiego, ale w moim przypadku rozwiązanie różniło się od powyższego.

Jeśli użyjesz już pobranej encji z bazy danych i spróbujesz zmodyfikować jej potomek, wystąpi błąd, ale jeśli otrzymasz nową kopię encji z bazy danych, nie powinno być żadnych problemów. Nie używaj tego:

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

Użyj tego:

 public void CheckUsersCount(Guid companyProductId)
 {
      CompanyProduct companyProduct = CompanyProductManager.Get(companyProductId);
      companyProduct.Name = "Test";
 }
Tanyo Iwanow
źródło