Aktualisieren Sie die Beziehungen, wenn Sie Änderungen an EF4-POCO-Objekten speichern

107

Entity Framework 4, POCO-Objekte und ASP.Net MVC2. Ich habe viele zu viele Beziehungen, sagen wir zwischen BlogPost- und Tag-Entitäten. Dies bedeutet, dass ich in meiner von T4 generierten POCO BlogPost-Klasse Folgendes habe:

public virtual ICollection<Tag> Tags {
    // getter and setter with the magic FixupCollection
}
private ICollection<Tag> _tags;

Ich fordere einen BlogPost und die zugehörigen Tags von einer Instanz des ObjectContext an und sende ihn an eine andere Ebene (Ansicht in der MVC-Anwendung). Später erhalte ich den aktualisierten BlogPost mit geänderten Eigenschaften und geänderten Beziehungen zurück. Zum Beispiel hatte es die Tags "A", "B" und "C", und die neuen Tags sind "C" und "D". In meinem speziellen Beispiel gibt es keine neuen Tags und die Eigenschaften der Tags ändern sich nie. Das einzige, was gespeichert werden sollte, sind die geänderten Beziehungen. Jetzt muss ich dies in einem anderen ObjectContext speichern. (Update: Jetzt habe ich versucht, in der gleichen Kontextinstanz zu tun und auch fehlgeschlagen.)

Das Problem: Ich kann die Beziehungen nicht richtig speichern. Ich habe alles versucht, was ich gefunden habe:

  • Controller.UpdateModel und Controller.TryUpdateModel funktionieren nicht.
  • Es funktioniert nicht, den alten BlogPost aus dem Kontext zu holen und dann die Sammlung zu ändern. (mit verschiedenen Methoden ab dem nächsten Punkt)
  • Dies würde wahrscheinlich funktionieren, aber ich hoffe, dies ist nur eine Problemumgehung, nicht die Lösung :(.
  • Versucht, Attach / Add / ChangeObjectState-Funktionen für BlogPost und / oder Tags in allen möglichen Kombinationen. Gescheitert.
  • Das sieht so aus, wie ich es brauche, aber es funktioniert nicht (ich habe versucht, es zu beheben, kann es aber nicht für mein Problem).
  • Versucht ChangeState / Add / Attach / ... die Beziehungsobjekte des Kontexts. Gescheitert.

"Funktioniert nicht" bedeutet in den meisten Fällen, dass ich an der angegebenen "Lösung" gearbeitet habe, bis sie keine Fehler mehr erzeugt und zumindest die Eigenschaften von BlogPost speichert. Was mit den Beziehungen passiert, ist unterschiedlich: Normalerweise werden Tags erneut mit neuen PKs zur Tag-Tabelle hinzugefügt, und der gespeicherte BlogPost verweist auf diese und nicht auf die ursprünglichen. Natürlich haben die zurückgegebenen Tags PKs, und vor den Speicher- / Aktualisierungsmethoden überprüfe ich die PKs und sie entsprechen denen in der Datenbank, sodass EF wahrscheinlich denkt, dass es sich um neue Objekte handelt und diese PKs die temporären sind.

Ein Problem, das ich kenne und das es möglicherweise unmöglich macht, eine automatisierte einfache Lösung zu finden: Wenn die Sammlung eines POCO-Objekts geändert wird, sollte dies durch die oben erwähnte Eigenschaft der virtuellen Sammlung geschehen, da der Trick FixCollection die umgekehrten Verweise am anderen Ende aktualisiert der Viele-zu-Viele-Beziehung. Wenn eine Ansicht jedoch ein aktualisiertes BlogPost-Objekt "zurückgibt", ist dies nicht geschehen. Dies bedeutet, dass es vielleicht keine einfache Lösung für mein Problem gibt, aber das würde mich sehr traurig machen und ich würde den EF4-POCO-MVC-Triumph hassen :(. Auch das würde bedeuten, dass EF dies in der MVC-Umgebung überhaupt nicht tun kann Es werden EF4-Objekttypen verwendet :(. Ich denke, die auf Snapshots basierende Änderungsverfolgung sollte herausfinden, dass der geänderte BlogPost Beziehungen zu Tags mit vorhandenen PKs hat.

Übrigens: Ich denke, das gleiche Problem tritt bei Eins-zu-Viele-Beziehungen auf (Google und mein Kollege sagen es). Ich werde es zu Hause versuchen, aber selbst wenn das funktioniert, hilft mir das in meinen sechs vielen-zu-vielen-Beziehungen in meiner App nicht weiter :(.

Peterfoldi
quelle
Bitte posten Sie Ihren Code. Dies ist ein häufiges Szenario.
John Farrell
1
Ich habe eine automatische Lösung für dieses Problem, es ist in den Antworten unten versteckt, so viele würden es vermissen, aber bitte werfen Sie einen Blick darauf, da es Ihnen eine verdammt
gute
@brentmckendrick Ich denke, ein anderer Ansatz ist besser. Warum nicht einfach das Delta senden, anstatt den gesamten modifizierten Objektgraphen über die Leitung zu senden? In diesem Fall würden Sie nicht einmal generierte DTO-Klassen benötigen. Wenn Sie eine Meinung dazu haben, wenden Sie sich bitte an stackoverflow.com/questions/1344066/calculate-object-delta .
HappyNomad

Antworten:

145

Versuchen wir es so:

  • Hängen Sie BlogPost an den Kontext an. Nach dem Anhängen des Objekts an den Kontext wird der Status des Objekts, aller zugehörigen Objekte und aller Beziehungen auf Unverändert gesetzt.
  • Verwenden Sie context.ObjectStateManager.ChangeObjectState, um Ihren BlogPost auf Modified zu setzen
  • Durchlaufen Sie die Tag-Sammlung
  • Verwenden Sie context.ObjectStateManager.ChangeRelationshipState, um den Status für die Beziehung zwischen dem aktuellen Tag und BlogPost festzulegen.
  • Änderungen speichern

Bearbeiten:

Ich denke, einer meiner Kommentare hat Ihnen die falsche Hoffnung gegeben, dass EF die Fusion für Sie durchführen wird. Ich habe viel mit diesem Problem gespielt und meine Schlussfolgerung besagt, dass EF dies nicht für Sie tun wird. Ich denke, Sie haben meine Frage auch auf MSDN gefunden . In Wirklichkeit gibt es im Internet viele solcher Fragen. Das Problem ist, dass nicht klar angegeben ist, wie mit diesem Szenario umgegangen werden soll. Schauen wir uns also das Problem an:

Problemhintergrund

EF muss Änderungen an Entitäten verfolgen, damit die Persistenz weiß, welche Datensätze aktualisiert, eingefügt oder gelöscht werden müssen. Das Problem ist, dass es in der Verantwortung von ObjectContext liegt, Änderungen zu verfolgen. ObjectContext kann Änderungen nur für angehängte Entitäten verfolgen. Entitäten, die außerhalb des ObjectContext erstellt werden, werden überhaupt nicht verfolgt.

Problembeschreibung

Basierend auf der obigen Beschreibung können wir klar sagen, dass EF besser für verbundene Szenarien geeignet ist, in denen die Entität immer an den Kontext gebunden ist - typisch für WinForm-Anwendungen. Webanwendungen erfordern ein getrenntes Szenario, in dem der Kontext geschlossen wird, nachdem die Anforderungsverarbeitung und der Entitätsinhalt als HTTP-Antwort an den Client übergeben wurden. Die nächste HTTP-Anforderung enthält geänderten Inhalt der Entität, der neu erstellt, an einen neuen Kontext angehängt und beibehalten werden muss. Erholung findet normalerweise außerhalb des Kontextbereichs statt (geschichtete Architektur mit beharrlicher Ignoranz).

Lösung

Wie soll man mit einem solchen Szenario ohne Verbindung umgehen? Bei der Verwendung von POCO-Klassen haben wir drei Möglichkeiten, mit der Änderungsverfolgung umzugehen:

  • Schnappschuss - erfordert denselben Kontext = nutzlos für getrenntes Szenario
  • Dynamische Tracking-Proxys - erfordern denselben Kontext = nutzlos für nicht verbundene Szenarien
  • Manuelle Synchronisation.

Die manuelle Synchronisation auf einer einzelnen Entität ist eine einfache Aufgabe. Sie müssen nur eine Entität anhängen und AddObject zum Einfügen aufrufen, DeleteObject zum Löschen oder den Status in ObjectStateManager auf Modified zum Aktualisieren setzen. Der eigentliche Schmerz entsteht, wenn Sie sich mit Objektgraphen anstatt mit einzelnen Entitäten befassen müssen. Dieser Schmerz ist noch schlimmer, wenn Sie sich mit unabhängigen Assoziationen (solchen, die keine Fremdschlüsseleigenschaft verwenden) und vielen bis vielen Beziehungen befassen müssen. In diesem Fall müssen Sie jede Entität im Objektdiagramm, aber auch jede Beziehung im Objektdiagramm manuell synchronisieren.

Die manuelle Synchronisierung wird in der MSDN-Dokumentation als Lösung vorgeschlagen: Das Anhängen und Trennen von Objekten lautet:

Objekte werden unverändert an den Objektkontext angehängt. Wenn Sie den Status eines Objekts oder der Beziehung ändern müssen, weil Sie wissen, dass Ihr Objekt im getrennten Status geändert wurde, verwenden Sie eine der folgenden Methoden.

Erwähnte Methoden sind ChangeObjectState und ChangeRelationshipState von ObjectStateManager = manuelle Änderungsverfolgung. Ein ähnlicher Vorschlag ist in einem anderen MSDN-Dokumentationsartikel enthalten: Definieren und Verwalten von Beziehungen lautet:

Wenn Sie mit nicht verbundenen Objekten arbeiten, müssen Sie die Synchronisierung manuell verwalten.

Darüber hinaus gibt es einen Blog-Beitrag zu EF v1, der genau dieses Verhalten von EF kritisiert.

Grund für die Lösung

EF hat viele „hilfreich“ Operationen und Einstellungen wie Refresh , laden , applyCurrentValues , ApplyOriginalValues , MergeOption etc. Aber durch meine Untersuchung all diese Funktionen nur für einzelne Unternehmen und wirkt sich nur skalare preperties (= nicht Navigation Eigenschaften und Beziehungen). Ich teste diese Methoden lieber nicht mit komplexen Typen, die in Entitäten verschachtelt sind.

Andere vorgeschlagene Lösung

Anstelle der echten Merge-Funktionalität bietet das EF-Team sogenannte Self Tracking Entities (STE) an, die das Problem nicht lösen. Zunächst funktioniert STE nur, wenn dieselbe Instanz für die gesamte Verarbeitung verwendet wird. In Webanwendungen ist dies nur dann der Fall, wenn Sie die Instanz im Ansichtsstatus oder in der Sitzung speichern. Aus diesem Grund bin ich sehr unglücklich über die Verwendung von EF und werde die Funktionen von NHibernate überprüfen. Die erste Beobachtung besagt, dass NHibernate möglicherweise eine solche Funktionalität hat .

Fazit

Ich werde diese Annahmen mit einem einzigen Link zu einer anderen verwandten Frage im MSDN-Forum abschließen. Überprüfen Sie die Antwort von Zeeshan Hirani. Er ist Autor von Entity Framework 4.0-Rezepten . Wenn er sagt, dass das automatische Zusammenführen von Objektgraphen nicht unterstützt wird, glaube ich ihm.

Trotzdem besteht die Möglichkeit, dass ich völlig falsch liege und in EF einige automatische Zusammenführungsfunktionen vorhanden sind.

Bearbeiten 2:

Wie Sie sehen, wurde dies bereits 2007 als Vorschlag zu MS Connect hinzugefügt . MS hat es als etwas geschlossen, das in der nächsten Version zu tun ist, aber tatsächlich wurde nichts unternommen, um diese Lücke zu verbessern, außer STE.

Ladislav Mrnka
quelle
7
Dies ist eine der besten Antworten, die ich auf SO gelesen habe. Sie haben klar angegeben, was so viele MSDN-Artikel, Dokumentationen und Blog-Beiträge zu diesem Thema nicht vermitteln konnten. EF4 unterstützt nicht von Natur aus das Aktualisieren von Beziehungen von "getrennten" Entitäten. Es bietet nur Tools, mit denen Sie es selbst implementieren können. Danke dir!
Tyriker
1
Wie steht es also nach einigen Monaten mit dem NHibernate im Zusammenhang mit diesem Problem im Vergleich zu EF4?
CallMeLaNN
1
Dies wird in NHibernate sehr gut unterstützt :-) Sie müssen nicht manuell zusammengeführt werden. In meinem Beispiel handelt es sich um ein 3-Ebenen-Diagramm für tiefe Objekte. Die Frage enthält Antworten, jede Antwort enthält Kommentare und die Frage enthält auch Kommentare. NHibernate kann Ihr Objektdiagramm beibehalten / zusammenführen, egal wie komplex es ist. Ienablemuch.com/2011/01/nhibernate-saves-your-whole-object.html Ein weiterer zufriedener NHibernate-Benutzer: codinginstinct.com/2009/11/…
Michael Buen
2
Eine der besten Erklärungen, die ich je gelesen habe !! Vielen Dank
MarvelTracker
2
Das EF-Team plant, dieses Problem nach EF6 anzugehen. Sie können für entityframework.codeplex.com/workitem/864
Eric J.
19

Ich habe eine Lösung für das oben von Ladislav beschriebene Problem. Ich habe eine Erweiterungsmethode für den DbContext erstellt, die automatisch das Hinzufügen / Aktualisieren / Löschen basierend auf einem Unterschied zwischen dem bereitgestellten Diagramm und dem persistierten Diagramm ausführt.

Derzeit müssen Sie mit dem Entity Framework die Aktualisierungen der Kontakte manuell durchführen, prüfen, ob jeder Kontakt neu ist, und hinzufügen, prüfen, ob er aktualisiert und bearbeitet wurde, prüfen, ob er entfernt wurde, und ihn dann aus der Datenbank löschen. Sobald Sie dies für einige verschiedene Aggregate in einem großen System tun müssen, erkennen Sie, dass es einen besseren, allgemeineren Weg geben muss.

Bitte schauen Sie nach, ob dies hilfreich sein kann. Http://refactorthis.wordpress.com/2012/12/11/introducing-graphdiff-for-entity-framework-code-first-allowing-automated-updates-of-a- Graph-of-Detached-Entities /

Sie können direkt zum Code hier https://github.com/refactorthis/GraphDiff gehen

brentmckendrick
quelle
Ich bin sicher, Sie können diese Frage leicht lösen , ich habe eine schlechte Zeit damit.
Shimmy Weitzhandler
1
Hallo Shimmy, tut mir leid, ich habe endlich Zeit, einen Blick darauf zu werfen. Ich werde es heute Abend untersuchen.
Brentmckendrick
Diese Bibliothek ist großartig und hat mir so viel Zeit gespart! Vielen Dank!
Lordjeb
9

Ich weiß, dass es für das OP spät ist, aber da dies ein sehr häufiges Problem ist, habe ich dies veröffentlicht, falls es jemand anderem dient. Ich habe mit diesem Problem herumgespielt und ich denke, ich habe eine ziemlich einfache Lösung. Was ich tue, ist:

  1. Speichern Sie das Hauptobjekt (z. B. Blogs), indem Sie den Status auf Geändert setzen.
  2. Fragen Sie die Datenbank nach dem aktualisierten Objekt ab, einschließlich der Sammlungen, die ich aktualisieren muss.
  3. Fragen Sie die Entitäten ab, die meine Sammlung enthalten soll, und konvertieren Sie .ToList ().
  4. Aktualisieren Sie die Sammlung (en) des Hauptobjekts auf die Liste, die ich aus Schritt 3 erhalten habe.
  5. Änderungen speichern();

Im folgenden Beispiel sind "dataobj" und "_categories" die von meinem Controller empfangenen Parameter. "Dataobj" ist mein Hauptobjekt, und "_categories" ist eine IEnumerable, die die IDs der Kategorien enthält, die der Benutzer in der Ansicht ausgewählt hat.

    db.Entry(dataobj).State = EntityState.Modified;
    db.SaveChanges();
    dataobj = db.ServiceTypes.Include(x => x.Categories).Single(x => x.Id == dataobj.Id);
    var it = _categories != null ? db.Categories.Where(x => _categories.Contains(x.Id)).ToList() : null;
    dataobj.Categories = it;
    db.SaveChanges();

Es funktioniert sogar für mehrere Beziehungen

c0y0teX
quelle
7

Das Entity Framework-Team ist sich bewusst, dass dies ein Usability-Problem ist, und plant, es nach EF6 zu beheben.

Aus dem Entity Framework-Team:

Dies ist ein Usability-Problem, dessen wir uns bewusst sind und über das wir nachgedacht haben und das wir planen, nach EF6 weitere Arbeiten durchzuführen. Ich habe dieses Arbeitselement erstellt, um das Problem zu verfolgen: http://entityframework.codeplex.com/workitem/864 Das Arbeitselement enthält auch einen Link zum Sprachelement des Benutzers. Ich empfehle Ihnen, dafür zu stimmen, wenn Sie dies haben noch nicht so gemacht.

Wenn dies Auswirkungen auf Sie hat, wählen Sie die Funktion unter

http://entityframework.codeplex.com/workitem/864

Eric J.
quelle
nach EF6? Welches Jahr wird es dann im optimistischen Fall sein?
Quetzalcoatl
@quetzalcoatl: Zumindest ist es auf ihrem Radar :-) EF hat seit EF 1 einen langen Weg zurückgelegt, hat aber noch einen weiten Weg vor sich.
Eric J.
1

Alle Antworten waren großartig, um das Problem zu erklären, aber keine von ihnen hat das Problem wirklich für mich gelöst.

Ich stellte fest, dass alles gut funktionierte, wenn ich die Beziehung in der übergeordneten Entität nicht verwendete, sondern nur die untergeordneten Entitäten hinzufügte und entfernte.

Entschuldigung für die VB, aber genau darin ist das Projekt geschrieben, in dem ich arbeite.

Die übergeordnete Entität "Report" hat eine Eins-zu-Viele-Beziehung zu "ReportRole" und die Eigenschaft "ReportRoles". Die neuen Rollen werden durch eine durch Kommas getrennte Zeichenfolge aus einem Ajax-Aufruf übergeben.

In der ersten Zeile werden alle untergeordneten Entitäten entfernt. Wenn ich "report.ReportRoles.Remove (f)" anstelle von "db.ReportRoles.Remove (f)" verwenden würde, würde der Fehler angezeigt.

report.ReportRoles.ToList.ForEach(Function(f) db.ReportRoles.Remove(f))
Dim newRoles = If(String.IsNullOrEmpty(model.RolesString), New String() {}, model.RolesString.Split(","))
newRoles.ToList.ForEach(Function(f) db.ReportRoles.Add(New ReportRole With {.ReportId = report.Id, .AspNetRoleId = f}))
Alan Bridges
quelle