Die Beziehung konnte nicht geändert werden, da eine oder mehrere der Fremdschlüsseleigenschaften nicht nullwertfähig sind

192

Ich erhalte diesen Fehler, wenn ich GetById () für eine Entität erhalte und dann die Sammlung untergeordneter Entitäten auf meine neue Liste setze, die aus der MVC-Ansicht stammt.

Der Vorgang ist fehlgeschlagen: Die Beziehung konnte nicht geändert werden, da eine oder mehrere der Fremdschlüsseleigenschaften nicht nullwertfähig sind. Wenn eine Beziehung geändert wird, wird die zugehörige Fremdschlüsseleigenschaft auf einen Nullwert gesetzt. Wenn der Fremdschlüssel keine Nullwerte unterstützt, muss eine neue Beziehung definiert, der Fremdschlüsseleigenschaft ein anderer Nicht-Nullwert zugewiesen oder das nicht verwandte Objekt gelöscht werden.

Ich verstehe diese Zeile nicht ganz:

Die Beziehung konnte nicht geändert werden, da eine oder mehrere der Fremdschlüsseleigenschaften nicht nullwertfähig sind.

Warum sollte ich die Beziehung zwischen zwei Entitäten ändern? Sie sollte während der gesamten Lebensdauer der gesamten Anwendung gleich bleiben.

Der Code, bei dem die Ausnahme auftritt, besteht darin, der vorhandenen übergeordneten Klasse einfach geänderte untergeordnete Klassen in einer Auflistung zuzuweisen. Dies würde hoffentlich das Entfernen von Kinderklassen, das Hinzufügen neuer Klassen und Änderungen ermöglichen. Ich hätte gedacht, Entity Framework kümmert sich darum.

Die Codezeilen können destilliert werden zu:

var thisParent = _repo.GetById(1);
thisParent.ChildItems = modifiedParent.ChildItems();
_repo.Save();
Jaffa
quelle
Ich habe meine Antwort mit der Lösung Nr. 2 im folgenden Artikel gekauft. Im Grunde habe ich der untergeordneten Tabelle einen Primärschlüssel hinzugefügt, um auf die übergeordnete Tabelle zu verweisen (also 2 Primärschlüssel (der Fremdschlüssel für die übergeordnete Tabelle und die ID) für die untergeordnete
yougotiger
@jaffa, fand ich meine Antwort hier stackoverflow.com/questions/22858491/...
antonio

Antworten:

159

Sie sollten alte untergeordnete Elemente thisParent.ChildItemseinzeln manuell löschen . Entity Framework erledigt das nicht für Sie. Es kann schließlich nicht entscheiden, was Sie mit den alten untergeordneten Elementen tun möchten - ob Sie sie wegwerfen möchten oder ob Sie sie behalten und anderen übergeordneten Entitäten zuweisen möchten. Sie müssen Entity Framework Ihre Entscheidung mitteilen. Aber eine dieser beiden Entscheidungen MÜSSEN Sie treffen, da die untergeordneten Entitäten nicht alleine leben können, ohne auf einen Elternteil in der Datenbank zu verweisen (aufgrund der Fremdschlüsseleinschränkung). Das sagt im Grunde die Ausnahme.

Bearbeiten

Was würde ich tun, wenn untergeordnete Elemente hinzugefügt, aktualisiert und gelöscht werden könnten:

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

Hinweis: Dies wird nicht getestet. Es wird davon ausgegangen, dass die Sammlung untergeordneter Elemente vom Typ ist ICollection. (Normalerweise habe ich IListund dann sieht der Code etwas anders aus.) Ich habe auch alle Repository-Abstraktionen entfernt, um es einfach zu halten.

Ich weiß nicht, ob das eine gute Lösung ist, aber ich glaube, dass eine harte Arbeit in dieser Richtung geleistet werden muss, um alle Arten von Änderungen in der Navigationssammlung zu berücksichtigen. Ich würde mich auch freuen, einen einfacheren Weg zu sehen.

Slauma
quelle
Was ist, wenn einige nur geändert werden? Bedeutet das, dass ich sie noch entfernen und erneut hinzufügen muss?
Jaffa
@ Jon: Nein, Sie können natürlich auch vorhandene Elemente aktualisieren. Ich habe ein Beispiel hinzugefügt, wie ich die untergeordnete Sammlung wahrscheinlich aktualisieren würde, siehe Abschnitt Bearbeiten oben.
Slauma
@ Slauma: Lol, wenn ich wüsste, dass Sie Ihre Antwort ändern werden, würde ich meine Antwort nicht schreiben ...
Ladislav Mrnka
@Ladislav: Nein, nein, ich bin froh, dass du deine eigene Antwort geschrieben hast. Jetzt weiß ich zumindest, dass es kein völliger Unsinn und viel zu kompliziert ist, was ich oben getan habe.
Slauma
1
Ich würde eine Bedingung hinzufügen, wenn ich das originalChildItem im foreach abrufe: ... Where (c => c.ID == childItem.ID && c.ID! = 0), sonst werden die neu hinzugefügten Kinder zurückgegeben, wenn die childItem.ID == 0.
perfect_element
116

Der Grund dafür ist der Unterschied zwischen Zusammensetzung und Aggregation .

In der Komposition wird das untergeordnete Objekt erstellt, wenn das übergeordnete Objekt erstellt wird, und wird zerstört, wenn das übergeordnete Objekt zerstört wird . Die Lebensdauer wird also von den Eltern gesteuert. zB Ein Blog-Beitrag und seine Kommentare. Wenn ein Beitrag gelöscht wird, sollten seine Kommentare gelöscht werden. Es macht keinen Sinn, Kommentare für einen Beitrag zu haben, der nicht existiert. Gleiches gilt für Bestellungen und Bestellartikel.

In der Aggregation kann das untergeordnete Objekt unabhängig von seinem übergeordneten Objekt vorhanden sein . Wenn das übergeordnete Objekt zerstört wird, kann das untergeordnete Objekt weiterhin vorhanden sein, da es später einem anderen übergeordneten Objekt hinzugefügt werden kann. Beispiel: Die Beziehung zwischen einer Wiedergabeliste und den Titeln in dieser Wiedergabeliste. Wenn die Wiedergabeliste gelöscht wird, sollten die Songs nicht gelöscht werden. Sie können einer anderen Wiedergabeliste hinzugefügt werden.

Entity Framework unterscheidet Aggregations- und Zusammensetzungsbeziehungen wie folgt:

  • Für die Komposition: Es wird erwartet, dass das untergeordnete Objekt einen zusammengesetzten Primärschlüssel (ParentID, ChildID) hat. Dies ist beabsichtigt, da die IDs der Kinder im Bereich ihrer Eltern liegen sollten.

  • Für die Aggregation: Es wird erwartet, dass die Fremdschlüsseleigenschaft im untergeordneten Objekt nullwertfähig ist.

Der Grund für dieses Problem liegt darin, wie Sie Ihren Primärschlüssel in Ihrer untergeordneten Tabelle festgelegt haben. Es sollte zusammengesetzt sein, ist es aber nicht. Entity Framework betrachtet diese Zuordnung also als Aggregation. Wenn Sie also die untergeordneten Objekte entfernen oder löschen, werden die untergeordneten Datensätze nicht gelöscht. Die Zuordnung wird einfach entfernt und die entsprechende Fremdschlüsselspalte auf NULL gesetzt (sodass diese untergeordneten Datensätze später einem anderen übergeordneten Datensatz zugeordnet werden können). Da Ihre Spalte NULL nicht zulässt, erhalten Sie die von Ihnen erwähnte Ausnahme.

Lösungen:

1- Wenn Sie einen starken Grund haben, keinen zusammengesetzten Schlüssel verwenden zu wollen, müssen Sie die untergeordneten Objekte explizit löschen. Und dies kann einfacher erfolgen als die zuvor vorgeschlagenen Lösungen:

context.Children.RemoveRange(parent.Children);

2- Andernfalls sieht Ihr Code aussagekräftiger aus, wenn Sie den richtigen Primärschlüssel für Ihre untergeordnete Tabelle festlegen:

parent.Children.Clear();
Mosh
quelle
9
Ich fand diese Erklärung sehr hilfreich.
Booji Boy
7
Gute Erklärung für Komposition vs. Aggregation und wie sich das Entity Framework darauf bezieht.
Chrysalis
# 1 war die geringste Menge an Code, die zur Behebung des Problems erforderlich war. Danke dir!
Ryanulit
73

Dies ist ein sehr großes Problem. Was in Ihrem Code tatsächlich passiert, ist Folgendes:

  • Sie laden Parentaus der Datenbank und erhalten eine angehängte Entität
  • Sie ersetzen die untergeordnete Sammlung durch eine neue Sammlung losgelöster Kinder
  • Sie speichern Änderungen, aber während dieses Vorgangs werden alle untergeordneten Elemente als hinzugefügt betrachtet, da EF bis zu diesem Zeitpunkt nichts über sie wusste. Daher versucht EF, den Fremdschlüssel alter Kinder auf null zu setzen und alle neuen Kinder einzufügen => doppelte Zeilen.

Jetzt hängt die Lösung wirklich davon ab, was Sie tun möchten und wie Sie es tun möchten?

Wenn Sie ASP.NET MVC verwenden, können Sie versuchen, UpdateModel oder TryUpdateModel zu verwenden .

Wenn Sie vorhandene Kinder nur manuell aktualisieren möchten, können Sie einfach Folgendes tun:

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

context.SaveChanges();

Das Anhängen ist eigentlich nicht erforderlich ( Modifiedwenn Sie den Status auf festlegen , wird auch die Entität angehängt), aber ich mag es, weil es den Prozess offensichtlicher macht.

Wenn Sie vorhandene ändern, vorhandene löschen und neue untergeordnete Elemente einfügen möchten, müssen Sie Folgendes tun:

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
quelle
1
Aber es gibt Ihre interessante Bemerkung über die Verwendung .Clone(). Denken Sie daran, dass a ChildItemandere untergeordnete Navigationseigenschaften hat? Aber möchten wir in diesem Fall nicht, dass der gesamte Untergraph an den Kontext angehängt wird, da wir erwarten würden, dass alle Unterkinder neue Objekte sind, wenn das Kind selbst neu ist? (Nun, könnte von Modell zu Modell unterschiedlich sein, aber nehmen wir den Fall an, dass die Unterkinder vom Kind "abhängig" sind, wie die Kinder vom Elternteil abhängig sind.)
Slauma
Es würde wahrscheinlich einen "intelligenten" Klon erfordern.
Ladislav Mrnka
1
Was ist, wenn Sie keine Kindersammlung in Ihrem Kontext haben möchten? http://stackoverflow.com/questions/20233994/do-i-need-to-create-a-dbset-for-every-table-so-that-i-can-persist-child-entitie
Kirsten Gier
1
parent.ChildItems.Remove (Kind); context.Childs.Remove (Kind); Diese doppelte Entfernung behoben kann Problem, DANKE. Warum brauchen wir beide Entfernungen? Warum nur aus parent.ChildItems entfernen ist nicht genug, da Kinder nur als Kinder leben?
Fernando Torres
40

Ich fand diese Antwort für den gleichen Fehler viel hilfreicher. Es scheint, dass EF es nicht mag, wenn Sie entfernen, es bevorzugt Löschen.

Sie können eine Sammlung von Datensätzen löschen, die an einen solchen Datensatz angehängt sind.

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

In diesem Beispiel ist für alle an eine Bestellung angehängten Detaildatensätze der Status "Löschen" festgelegt. (In Vorbereitung auf das Zurücksetzen aktualisierter Details als Teil eines Bestellupdates)

Greg Little
quelle
Ich glaube, es ist die richtige Antwort.
Desmati
logische und unkomplizierte Lösung.
Sairfan
19

Ich habe keine Ahnung, warum die beiden anderen Antworten so beliebt sind!

Ich glaube, Sie haben zu Recht davon ausgegangen, dass das ORM-Framework damit umgehen sollte - schließlich verspricht es, dies zu erreichen. Andernfalls wird Ihr Domain-Modell durch Persistenzprobleme beschädigt. NHibernate schafft dies problemlos, wenn Sie die Kaskadeneinstellungen korrekt einrichten. In Entity Framework ist es auch möglich, dass sie nur erwarten, dass Sie beim Einrichten Ihres Datenbankmodells bessere Standards befolgen, insbesondere wenn sie ableiten müssen, welche Kaskadierung durchgeführt werden soll:

Sie müssen die Eltern-Kind-Beziehung korrekt definieren, indem Sie eine " identifizierende Beziehung " verwenden.

Wenn Sie dies tun, weiß Entity Framework, dass das untergeordnete Objekt vom übergeordneten Objekt identifiziert wird, und daher muss es sich um eine "Cascade-Delete-Orphans" -Situation handeln.

Anders als oben müssen Sie möglicherweise (aus NHibernate-Erfahrung)

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

anstatt die Liste vollständig zu ersetzen.

AKTUALISIEREN

@ Slaumas Kommentar erinnerte mich daran, dass getrennte Einheiten ein weiterer Teil des Gesamtproblems sind. Um dies zu lösen, können Sie einen benutzerdefinierten Modellordner verwenden, der Ihre Modelle erstellt, indem Sie versuchen, sie aus dem Kontext zu laden. Dieser Blog-Beitrag zeigt ein Beispiel dafür, was ich meine.

Andre Luus
quelle
Das Einrichten als identifizierende Beziehung hilft hier nicht weiter, da das Szenario in der Frage sich mit getrennten Entitäten befassen muss ( "meine neue Liste, die aus der MVC-Ansicht stammt" ). Sie müssen weiterhin die ursprünglichen untergeordneten Elemente aus der Datenbank laden, die entfernten Elemente in dieser Sammlung basierend auf der getrennten Sammlung suchen und dann aus der Datenbank entfernen. Der einzige Unterschied besteht darin, dass Sie mit einer identifizierenden Beziehung parent.ChildItems.Removeanstelle von anrufen können _dbContext.ChildItems.Remove. Es gibt immer noch (EF <= 6) keine integrierte Unterstützung von EF, um langen Code wie den in den anderen Antworten zu vermeiden.
Slauma
Ich verstehe deine Meinung. Ich glaube jedoch, dass mit einem benutzerdefinierten Modellordner, der die Entität aus dem Kontext lädt oder eine neue Instanz zurückgibt, der oben beschriebene Ansatz funktionieren würde. Ich werde meine Antwort aktualisieren, um diese Lösung vorzuschlagen.
Andre Luus
Ja, Sie könnten einen Modellordner verwenden, aber Sie mussten jetzt die anderen Antworten im Modellordner ausführen. Das Problem wird lediglich von der Repo- / Serviceschicht in den Modellordner verschoben. Zumindest sehe ich keine wirkliche Vereinfachung.
Slauma
Die Vereinfachung ist das automatische Löschen verwaister Entitäten. Alles, was Sie im return context.Items.Find(id) ?? new Item()
Modellordner
Gutes Feedback für das EF-Team, aber Ihre vorgeschlagene Lösung löst leider nichts im EF-Land.
Chris Moschini
9

Wenn Sie AutoMapper mit Entity Framework für dieselbe Klasse verwenden, tritt dieses Problem möglicherweise auf. Zum Beispiel, wenn Ihre Klasse ist

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

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

Dadurch wird versucht, beide Eigenschaften zu kopieren. In diesem Fall ist ClassBId nicht nullbar. Da AutoMapper kopiert destination.ClassB = input.ClassB;, verursacht dies ein Problem.

Stellen Sie Ihren AutoMapper auf Ignorieren ein ClassB.

 cfg.CreateMap<A, A>()
     .ForMember(m => m.ClassB, opt => opt.Ignore()); // We use the ClassBId
jsgoupil
quelle
Ich habe ein ähnliches Problem mit AutoMapper, aber das funktioniert bei mir nicht :( Siehe stackoverflow.com/q/41430679/613605
J86
4

Ich hatte gerade den gleichen Fehler. Ich habe zwei Tabellen mit einer übergeordneten untergeordneten Beziehung, aber ich habe in der Fremdschlüsselspalte in der Tabellendefinition der untergeordneten Tabelle eine "Kaskade beim Löschen" konfiguriert. Wenn ich also die übergeordnete Zeile (über SQL) in der Datenbank manuell lösche, werden die untergeordneten Zeilen automatisch gelöscht.

In EF funktionierte dies jedoch nicht. Der in diesem Thread beschriebene Fehler wurde angezeigt. Der Grund dafür war, dass in meinem Entitätsdatenmodell (edmx-Datei) die Eigenschaften der Zuordnung zwischen der übergeordneten und der untergeordneten Tabelle nicht korrekt waren. Die End1 OnDeleteOption wurde wie nonefolgt konfiguriert ("End1" in meinem Modell ist das Ende mit einer Multiplizität von 1).

Ich habe die End1 OnDeleteOption manuell auf Cascadeund dann geändert . Ich weiß nicht, warum EF dies nicht erfassen kann, wenn ich das Modell aus der Datenbank aktualisiere (ich habe ein erstes Datenbankmodell).

Der Vollständigkeit halber sieht mein zu löschender Code folgendermaßen aus:

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

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

Wenn ich keine Kaskadenlöschung definiert hätte, müsste ich die untergeordneten Zeilen manuell löschen, bevor ich die übergeordnete Zeile lösche.

Martin
quelle
4

Dies liegt daran, dass die untergeordnete Entität als "Geändert" anstatt als "Gelöscht" markiert ist.

Und die Änderung, die EF an der untergeordneten Entität vornimmt, wenn sie parent.Remove(child)ausgeführt wird, setzt einfach den Verweis auf die übergeordnete Entität auf null.

Sie können den EntityState des Kindes überprüfen, indem Sie den folgenden Code in das Direktfenster von Visual Studio eingeben, wenn die Ausnahme nach der Ausführung auftritt SaveChanges():

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

Dabei sollte X durch die gelöschte Entität ersetzt werden.

Wenn Sie keinen Zugriff auf die ObjectContextauszuführende Funktion haben _context.ChildEntity.Remove(child), können Sie dieses Problem beheben , indem Sie den Fremdschlüssel zu einem Teil des Primärschlüssels in der untergeordneten Tabelle machen.

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

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

Auf diese Weise parent.Remove(child)markiert EF bei der Ausführung die Entität korrekt als gelöscht.

Mauricio Ramalho
quelle
2

Diese Art von Lösung hat den Trick für mich getan:

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

Es ist wichtig zu sagen, dass dadurch alle Datensätze gelöscht und erneut eingefügt werden. Aber für meinen Fall (weniger als 10) ist es in Ordnung.

Ich hoffe, es hilft.

Wagner Bertolini Junior
quelle
Kommt das erneute Einfügen mit neuen IDs vor oder werden die IDs des Kindes beibehalten, die es ursprünglich hatte?
Pepito Fernandez
2

Ich bin heute auf dieses Problem gestoßen und wollte meine Lösung teilen. In meinem Fall bestand die Lösung darin, die untergeordneten Elemente zu löschen, bevor das übergeordnete Element aus der Datenbank abgerufen wurde.

Zuvor habe ich es wie im folgenden Code gemacht. Ich werde dann den gleichen Fehler in dieser Frage aufgelistet bekommen.

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

Was für mich funktioniert hat, ist, zuerst die untergeordneten Elemente mit der parentId (Fremdschlüssel) abzurufen und diese Elemente dann zu löschen. Dann kann ich das übergeordnete Element aus der Datenbank abrufen und zu diesem Zeitpunkt sollte es keine untergeordneten Elemente mehr enthalten und ich kann neue untergeordnete Elemente hinzufügen.

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
quelle
2

Sie müssen die ChildItems-Auflistung manuell löschen und neue Elemente anhängen:

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

Danach können Sie die DeleteOrphans-Erweiterungsmethode aufrufen, die mit verwaisten Entitäten umgehen kann (sie muss zwischen den DetectChanges- und SaveChanges-Methoden aufgerufen werden).

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);
    }
}
Sa.
quelle
Das hat bei mir gut funktioniert. Ich musste nur hinzufügen context.DetectChanges();.
Andy Edinborough
1

Ich habe diese und viele andere Lösungen ausprobiert, aber keine davon hat ganz geklappt. Da dies die erste Antwort auf Google ist, füge ich hier meine Lösung hinzu.

Die Methode, die für mich gut funktionierte, bestand darin, während der Commits Beziehungen aus dem Bild zu entfernen, sodass EF nichts vermasseln konnte. Dazu habe ich das übergeordnete Objekt im DBContext erneut gefunden und gelöscht. Da die Navigationseigenschaften des wiedergefundenen Objekts alle null sind, werden die Beziehungen der Kinder während des Festschreibens ignoriert.

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

Beachten Sie, dass dies voraussetzt, dass die Fremdschlüssel mit ON DELETE CASCADE eingerichtet wurden. Wenn also die übergeordnete Zeile entfernt wird, werden die untergeordneten Zeilen von der Datenbank bereinigt.

Steve
quelle
1

Ich habe Moshs Lösung verwendet , aber es war mir nicht klar, wie ich den Kompositionsschlüssel zuerst korrekt in Code implementieren sollte.

Hier ist also die Lösung:

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
quelle
1

Ich hatte das gleiche Problem, aber ich wusste, dass es in anderen Fällen in Ordnung war, also reduzierte ich das Problem auf Folgendes:

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 hatte einen zusammengesetzten Primärschlüssel (parentId + eine lokale Spalte) und funktionierte einwandfrei
  • ProblematicItems hatte einen eigenen einspaltigen Primärschlüssel, und die parentId war nur eine FK. Dies verursachte die Ausnahme nach Clear ().

Alles, was ich tun musste, war, die ParentId zu einem Teil der zusammengesetzten PK zu machen, um anzuzeigen, dass die Kinder ohne Eltern nicht existieren können. Ich habe das DB-first-Modell verwendet, die PK hinzugefügt und die parentId-Spalte als EntityKey markiert (daher musste ich sie sowohl in DB als auch in EF aktualisieren - nicht sicher, ob EF allein ausreichen würde).

Ich habe RequestId zu einem Teil der PK gemacht Aktualisieren Sie anschließend das EF-Modell und legen Sie die andere Eigenschaft als Teil des Entitätsschlüssels fest

Wenn Sie einmal darüber nachgedacht haben, ist es eine sehr elegante Unterscheidung, die EF verwendet, um zu entscheiden, ob Kinder ohne Eltern "sinnvoll" sind (in diesem Fall löscht Clear () sie nicht und löst eine Ausnahme aus, es sei denn, Sie setzen die ParentId auf etwas anderes / Besonderes ) oder - wie in der ursprünglichen Frage - erwarten wir, dass die Elemente gelöscht werden, sobald sie vom übergeordneten Element entfernt werden.

Ekus
quelle
0

Dieses Problem tritt auf, weil wir versuchen, die übergeordnete Tabelle zu löschen, obwohl noch untergeordnete Tabellendaten vorhanden sind. Wir lösen das Problem mit Hilfe der Kaskadenlöschung.

Im Modell Create-Methode in der dbcontext-Klasse.

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

Danach in unserem API-Aufruf

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

Kaskadenlöschoption Löschen Sie die übergeordnete sowie die übergeordnete untergeordnete untergeordnete Tabelle mit diesem einfachen Code. Versuchen Sie es auf diese einfache Weise.

Entfernen Sie den Bereich, der zum Löschen der Liste der Datensätze in der Datenbank verwendet wurde. Danke

Sowmiya V.
quelle
0

Ich löste auch mein Problem mit Mosh Antwort und ich dachte PeterB Antwort ein wenig war , da es eine ENUM als Fremdschlüssel verwendet. Denken Sie daran, dass Sie nach dem Hinzufügen dieses Codes eine neue Migration hinzufügen müssen.

Ich kann diesen Blog-Beitrag auch für andere Lösungen empfehlen:

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

Code:

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
quelle
0

Mit der Lösung von Slauma habe ich einige allgemeine Funktionen erstellt, mit denen untergeordnete Objekte und Sammlungen von untergeordneten Objekten aktualisiert werden können.

Alle meine persistenten Objekte implementieren diese Schnittstelle

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

Damit habe ich diese beiden Funktionen in mein Repository implementiert

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

Um es zu benutzen, mache ich folgendes:

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

Hoffe das hilft


EXTRA: Sie können auch eine separate DbContextExtentions-Klasse (oder Ihre eigene Kontext-Inferface) erstellen:

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

und benutze es wie:

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
quelle
Sie können mit diesen Funktionen auch eine Erweiterungsklasse für Ihren Kontext erstellen:
Bluemoon74
0

Ich hatte das gleiche Problem, als ich meinen Datensatz löschen wollte, als ein Problem aufgetreten ist. Diese Problemlösung besteht darin, dass Sie beim Löschen Ihres Datensatzes etwas verpassen, bevor Sie den Header / Master-Datensatz löschen, für den Sie in den Code schreiben müssen Löschen Sie die Details vor dem Header / Master. Ich hoffe, Ihr Problem wird behoben.

Ghazi Hur
quelle
-1

Ich habe dieses Problem vor einigen Stunden kennengelernt und alles versucht, aber in meinem Fall war die Lösung anders als oben aufgeführt.

Wenn Sie eine bereits abgerufene Entität aus der Datenbank verwenden und versuchen, die untergeordneten Elemente zu ändern, tritt der Fehler auf. Wenn Sie jedoch eine neue Kopie der Entität aus der Datenbank erhalten, sollten keine Probleme auftreten. Verwenden Sie dies nicht:

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

Benutze das:

 public void CheckUsersCount(Guid companyProductId)
 {
      CompanyProduct companyProduct = CompanyProductManager.Get(companyProductId);
      companyProduct.Name = "Test";
 }
Tanyo Ivanov
quelle