Laden...

EF6 Objekte mit NavigationProperties dürfen als Referenz nicht dasselbe Objekt haben

Erstellt von sugar76 vor 5 Jahren Letzter Beitrag vor 5 Jahren 1.727 Views
S
sugar76 Themenstarter:in
69 Beiträge seit 2017
vor 5 Jahren
EF6 Objekte mit NavigationProperties dürfen als Referenz nicht dasselbe Objekt haben

verwendetes Datenbanksystem: SQL Server 2017 Express, EF6

Hallo allerseits,

ich habe eine Datenbank mit ca. 70 Tabellen und verwende EF6 (z.Zt. Database First).

Der relevante Code:


public class Person
{
    public int Id { get; set; }
    public string Name { get; set; }
}

public class Aufgabe
{
    public int Id { get; set; }
    public string Titel { get; set; }
    public int VonPersonId { get; set; }
    public int AnPersonId { get; set; }
    public virtual Person VonPerson { get; set; }
    public virtual Person AnPerson { get; set; }
}

public class AufgabeRepository
{
    // Methoden Get, Exists, Delete, ...

    public void Save(Aufgabe aufgabe)
    {
        using (var context = CreateContext())
        {
            context.Entry(aufgabe).State = System.Data.Entity.EntityState.Added;
            context.Entry(aufgabe.VonPerson).State = System.Data.Entity.EntityState.Unchanged;
            context.Entry(aufgabe.AnPerson).State = System.Data.Entity.EntityState.Unchanged; // FEHLER, wenn AnPerson dieselbe Id wie VonPerson besitzt
            context.SaveChanges();
        }
    }
}

Es geht um den Aufruf Save(): ich übergebe zum Speichern eine Aufgabe im Zustand detached. Da ich nur die Aufgabe speichern will, setze ich den State von VonPerson und AnPerson auf Unchanged.

Mein Problem:
Es ist ein normaler Anwendungsfall, dass der Ersteller der Aufgabe (VonPerson) und der Empfänger (AnPerson) dieselbe Person sind (also dieselbe Id besitzen), aber zwei unterschiedliche Objekt-Instanzen.

Ist das der Fall, erhalte ich bei oben gekennzeichnten Zeile den Fehler:> Fehlermeldung:

System.InvalidOperationException: "Fehler beim Speichern oder Übernehmen der Änderungen, weil mehrere Entitäten des Typs 'Data.Person' den gleichen Primärschlüsselwert aufweisen. Stellen Sie sicher, dass explizit festgelegte Primärschlüsselwerte eindeutig sind.

Workaround:
Diesen Fehler kann ich vermeiden, indem ich für die Referenzen auf Person nur die Id-Werte setze:

using (var context = Entities.CreateContext())
{
    aufgabe.VonPersonId = aufgabe.VonPerson.Id;
    aufgabe.VonPerson = null;
    aufgabe.AnPersonId = aufgabe.AnPerson.Id;
    aufgabe.AnPerson = null;
    context.Entry(aufgabe).State = System.Data.Entity.EntityState.Added;
    context.SaveChanges();
}

Meine Frage:
Ich finde diesen Workaround total umständlich. Das Problem betrifft auch viele andere Entitäten. Ich habe es im Moment so gelöst, dass Per Code-Generator alle Entitäten eine Methode PrepareForSave() haben, welche den Workaournd ausführt.

Geht das nicht einfacher?

Gruß

W
955 Beiträge seit 2010
vor 5 Jahren

Hallo,

  1. ein Context sollte von jedem Entity nur ein Exemplar zurückgeben. Wenn du zweimal Anfragen an ein Context machst sollten alle erneut ausgelesen Entities die bereits in der ersten Anfrage geliefert wurden verworfen und statt dessen die bereits ausgegebenen Exemplare erneut geliefert werden. Versuche also die beiden Personen-Instanzen mit denselben Context auszulesen.
  2. Du könntest versuchen IEquatable<TEntity> zu implementieren bei dem du sagst dass zwei Exemplare mit derselben Id identisch sind. Aber sei vorsichtig bei Anfügen von Objekten da hier die Id geändert wird.
S
sugar76 Themenstarter:in
69 Beiträge seit 2017
vor 5 Jahren

Hallo,

  1. ein Context sollte von jedem Entity nur ein Exemplar zurückgeben. Wenn du zweimal Anfragen an ein Context machst sollten alle erneut ausgelesen Entities die bereits in der ersten Anfrage geliefert wurden verworfen und statt dessen die bereits ausgegebenen Exemplare erneut geliefert werden. Versuche also die beiden Personen-Instanzen mit denselben Context auszulesen.
  2. Du könntest versuchen IEquatable<TEntity> zu implementieren bei dem du sagst dass zwei Exemplare mit derselben Id identisch sind. Aber sei vorsichtig bei Anfügen von Objekten da hier die Id geändert wird.

Zu 1.):
VonPerson ist immer der aktuell im System angemeldete Benutzer, während AnPerson in der GUI aus einer Liste von Benutzern ausgewählt wurde. Daher sind es immer zwei Instanzen, die aus aus zwei Contexten kommen.

Ich müsste also eine Routine implementieren, welche vor dem Speichern alle NavigationProperties checkt und bei Duplikaten dafür sorgt, dass die betreffenden NavigationProperties auf dieselbe Instanz verweisen.

Zu 2.)
Das habe ich eben mal ausprobiert, der Fehler kommt leider trotzdem. Die Equals()-Methode wird gar nicht aufgerufen.

Nachtrag: Was ich nicht kapiere: EF "weiß" doch, dass Id der Primärschlüssel von Person ist. Da der State von VonPerson und AnPerson explizit auf Unchanged gesetzt wird, sollte es EF doch gar nicht interessieren, dass das zwei unterschiedliche Instanzen sind.

16.834 Beiträge seit 2008
vor 5 Jahren

Wenn Du via Fluent API alle Relationen korrekt bekannt machst, dann kannst Du einfach Referenzen verwenden und musst nicht dieses Gegurke mit dem manuellen ID-setzen betreiben.

Darüber hinaus müssen alle Entitäten dem identischen Kontext bekannt sein - das scheint bei Dir nicht der Fall zu sein.
Ein Kontext scoped in der Methode zu erstellen; das war aber auch noch nie eine gute Idee.

S
sugar76 Themenstarter:in
69 Beiträge seit 2017
vor 5 Jahren

Ein Kontext scoped in der Methode zu erstellen; das war aber auch noch nie eine gute Idee.

Wie würdest Du es denn machen? Am Anfang des Bearbeitungszyklus den Kontext erstellen und auf diesem Kontext arbeiten, bis der Benutzer seine Änderungen gespeichert hat?

Gruß

16.834 Beiträge seit 2008
vor 5 Jahren

Das ist der korrekte Weg - auch in dieser Form in der Doku zu finden.
Dank Dependency Injection kann man das auch vollkommen automatisieren - im UI Bereich auch sehr bequem via Reactive Extensions.

Bzw. via MediatR ebenfalls einfach Kontext-basierend über Injection.

S
sugar76 Themenstarter:in
69 Beiträge seit 2017
vor 5 Jahren

Das ist der korrekte Weg - auch in dieser Form in der Doku zu finden ... Dank Dependency Injection kann man das auch vollkommen automatisieren

Also erstmal Danke für die Hinweise.

Einige Konzepte sind für mich neu. Ich habe bisher immer mit detached-Objekten gearbeitet (nicht nur in WPF, sondern z.B. auch in Webanwendungen mit JEE).

Ich verstehe Dich so: Der Context wird mittels UnitOfWork erstellt und am Ende des Bearbeitungszyklus comitted.

Nicht ganz klar ist mir, wie Du das mit dem Automatisieren meinst. So wie ich das verstanden habe, kann man per RX dafür sorgen, dass alle Änderungen im ViewModel automatisch auf das Datenobjekt (in meinem Fall Aufgabe) übertragen werden.

Hab ich Dich richtig verstanden?

Gruß

16.834 Beiträge seit 2008
vor 5 Jahren

Prinzipiell ist der DbContext des EF bereits Dein UnitOfWork.
Manchmal lohnt es sich dieses zusätzlich zu abstrahieren (nicht oft).

Bei WebAnwendungen hat man eigentlich nie detached objects, da beim Request ein Kontext für diesen Request geöffnet wird (Scoped) - und alle Entitäten innerhalb dieser Kontext-Zeit erstellt oder manipuliert werden.
Das ist die einfachste Art, wie man mit EF arbeiten kann.

Bei EF + Rx ist es so, dass die Instanz einer Anwendung prinzipiell dauerhaft einen Kontext hält - und jede Kommunikation (bzw. bei Rx spricht man von Events) anschließend über diesen Kontext durchgeführt wird.

Ob man pro ViewModel oder pro Applikationinstanz dann den Kontext hält.... manchmal auch eine Glaubensfrage.
Im Falle von MediatR (also CQRS Pattern) wäre es standardmäßig in den einfachen Beispielen ebenfalls so, dass die Anwendung nur eine einzige, dauerhafte Verbindung (=Kontext) erhält (also Singleton Dependency Registrierung).

S
sugar76 Themenstarter:in
69 Beiträge seit 2017
vor 5 Jahren

Nochmals Danke für die Erklärung.

Ich arbeite aktuell an einer Desktopanwendung und kämpfe damit, einen sauberen Bearbeitungszyklus zu implementieren. Das geht über die Ursprungsfrage hinaus, deswegen habe ich dafür mal einen neuen Thread erstellt.