Wie kann ich verhindern, dass Entity Framework versucht, untergeordnete Objekte zu speichern / einzufügen?

100

Wenn ich eine Entität mit einem Entitätsframework speichere, gehe ich natürlich davon aus, dass nur versucht wird, die angegebene Entität zu speichern. Es wird jedoch auch versucht, die untergeordneten Entitäten dieser Entität zu speichern. Dies verursacht alle möglichen Integritätsprobleme. Wie zwinge ich EF, nur die Entität zu speichern, die ich speichern möchte, und ignoriere daher alle untergeordneten Objekte?

Wenn ich die Eigenschaften manuell auf null setze, wird die Fehlermeldung "Der Vorgang ist fehlgeschlagen: Die Beziehung konnte nicht geändert werden, da eine oder mehrere der Fremdschlüsseleigenschaften nicht nullwertfähig sind." Dies ist äußerst kontraproduktiv, da ich das untergeordnete Objekt speziell auf null gesetzt habe, damit EF es in Ruhe lässt.

Warum möchte ich die untergeordneten Objekte nicht speichern / einfügen?

Da dies in den Kommentaren hin und her diskutiert wird, werde ich begründen, warum ich möchte, dass meine untergeordneten Objekte in Ruhe gelassen werden.

In der Anwendung, die ich erstelle, wird das EF-Objektmodell nicht aus der Datenbank geladen, sondern als Datenobjekt verwendet, das ich beim Parsen einer Einfachdatei auffülle. Bei den untergeordneten Objekten beziehen sich viele davon auf Nachschlagetabellen, die verschiedene Eigenschaften der übergeordneten Tabelle definieren. Zum Beispiel der geografische Standort der primären Entität.

Da ich diese Objekte selbst ausgefüllt habe, geht EF davon aus, dass es sich um neue Objekte handelt, die zusammen mit dem übergeordneten Objekt eingefügt werden müssen. Diese Definitionen sind jedoch bereits vorhanden und ich möchte keine Duplikate in der Datenbank erstellen. Ich verwende das EF-Objekt nur zum Nachschlagen und Auffüllen des Fremdschlüssels in meiner Haupttabellenentität.

Selbst bei den untergeordneten Objekten, bei denen es sich um echte Daten handelt, muss ich zuerst das übergeordnete Objekt speichern und einen Primärschlüssel erhalten, oder EF scheint nur ein Durcheinander zu verursachen. Hoffe das gibt eine Erklärung.

Mark Micallef
quelle
Soweit ich weiß, müssen Sie die untergeordneten Objekte auf Null setzen.
Johan
Hallo Johan. Funktioniert nicht Es wirft Fehler aus, wenn ich die Sammlung auf Null setze. Je nachdem, wie ich es mache, beschwert es sich darüber, dass Schlüssel null sind oder dass meine Sammlung geändert wurde. Natürlich sind diese Dinge wahr, aber ich habe das absichtlich getan, damit die Objekte, die es nicht berühren soll, in Ruhe gelassen werden.
Mark Micallef
Euphorisch, das ist völlig wenig hilfreich.
Mark Micallef
@Euphoric Auch wenn untergeordnete Objekte nicht geändert werden, versucht EF, sie standardmäßig einzufügen und sie nicht zu ignorieren oder zu aktualisieren.
Johan
Was mich wirklich ärgert, ist, dass wenn ich mir Mühe gebe, diese Objekte tatsächlich auf Null zu setzen, es sich beschwert, anstatt zu realisieren, dass ich möchte, dass es sie einfach in Ruhe lässt. Gibt es eine Möglichkeit, EF zu zwingen, zu vergessen, dass ich diese Objekte hatte, da diese untergeordneten Objekte alle optional sind (in der Datenbank nullwertfähig)? dh seinen Kontext löschen oder irgendwie zwischenspeichern?
Mark Micallef

Antworten:

55

Soweit ich weiß, haben Sie zwei Möglichkeiten.

Option 1)

Setzen Sie alle untergeordneten Objekte auf Null, um sicherzustellen, dass EF nichts hinzufügt. Es wird auch nichts aus Ihrer Datenbank gelöscht.

Option 2)

Stellen Sie die untergeordneten Objekte mithilfe des folgenden Codes als vom Kontext getrennt ein

 context.Entry(yourObject).State = EntityState.Detached

Beachten Sie, dass Sie ein List/ nicht trennen können Collection. Sie müssen Ihre Liste durchlaufen und jedes Element in Ihrer Liste wie folgt trennen

foreach (var item in properties)
{
     db.Entry(item).State = EntityState.Detached;
}
Johan
quelle
Hallo Johan, ich habe versucht, eine der Sammlungen zu entfernen, und es wurde der folgende Fehler ausgegeben: Der Entitätstyp HashSet`1 ist nicht Teil des Modells für den aktuellen Kontext.
Mark Micallef
@MarkyMark Trennen Sie die Sammlung nicht. Sie müssen die Sammlung durchlaufen und Objekt für Objekt trennen (ich werde meine Antwort jetzt aktualisieren).
Johan
11
Leider scheint EF trotz einer Liste von Schleifen, die alles abfangen, immer noch zu versuchen, in einige der zugehörigen Tabellen einzufügen. An diesem Punkt bin ich bereit, EF herauszureißen und wieder zu SQL zu wechseln, das sich zumindest vernünftig verhält. Was für ein Schmerz.
Mark Micallef
2
Kann ich context.Entry (yourObject) .State verwenden, bevor ich es hinzufüge?
Thomas Klammer
1
@ mirind4 Ich habe State nicht verwendet. Wenn ich ein Objekt einfüge, stelle ich sicher, dass alle Kinder null sind. Beim Update bekomme ich das Objekt zuerst ohne die Kinder.
Thomas Klammer
41

Lange Rede, kurzer Sinn: Verwenden Sie den Fremdschlüssel, um Ihren Tag zu retten.

Angenommen , Sie haben Schule Einheit und eine Stadt Einheit, und dies ist ein Viele-zu-eins - Beziehung , wo eine Stadt viele Schulen und eine Schule gehören in einer Stadt hat. Angenommen, die Städte sind bereits in der Nachschlagetabelle vorhanden, sodass Sie NICHT möchten, dass sie beim Einfügen einer neuen Schule erneut eingefügt werden.

Zunächst können Sie Ihre Entitäten wie folgt definieren:

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

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

    [Required]
    public City City { get; set; }
}

Und Sie könnten das tun Schule Einsetzen wie folgt aus (vorausgesetzt , dass Sie bereits über Stadt - Eigenschaft auf dem zugewiesenen newItem ):

public School Insert(School newItem)
{
    using (var context = new DatabaseContext())
    {
        context.Set<School>().Add(newItem);
        // use the following statement so that City won't be inserted
        context.Entry(newItem.City).State = EntityState.Unchanged;
        context.SaveChanges();
        return newItem;
    }
}

Der obige Ansatz mag in diesem Fall perfekt funktionieren, ich bevorzuge jedoch den Fremdschlüsselansatz , der für mich klarer und flexibler ist. Siehe die aktualisierte Lösung unten:

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

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

    [ForeignKey("City_Id")]
    public City City { get; set; }

    [Required]
    public int City_Id { get; set; }
}

Auf diese Weise definieren Sie explizit, dass die Schule einen Fremdschlüssel City_Id hat und sich auf die Entität City bezieht . Wenn es also um das Einfügen von Schule geht , können Sie Folgendes tun:

    public School Insert(School newItem, int cityId)
    {
        if(cityId <= 0)
        {
            throw new Exception("City ID no provided");
        }

        newItem.City = null;
        newItem.City_Id = cityId;

        using (var context = new DatabaseContext())
        {
            context.Set<School>().Add(newItem);
            context.SaveChanges();
            return newItem;
        }
    }

In diesem Fall geben Sie die City_Id des neuen Datensatzes explizit an und entfernen die City aus dem Diagramm, damit EF sie nicht zusammen mit School zum Kontext hinzufügt .

Auf den ersten Blick scheint der Fremdschlüsselansatz komplizierter zu sein, aber glauben Sie mir, diese Mentalität spart Ihnen viel Zeit, wenn Sie eine Viele-zu-Viele-Beziehung einfügen (im Bild haben Sie eine Schul- und Schülerbeziehung und den Schüler hat ein Stadteigentum) und so weiter.

Hoffe das ist hilfreich für dich.

lenglei
quelle
Tolle Antwort, hat mir sehr geholfen! Aber wäre es nicht besser, den Mindestwert 1 für City_Id mit dem [Range(1, int.MaxValue)]Attribut anzugeben ?
Dan Rayson
Wie ein Traum!! Tausend Dank!
CJH
Dies würde den Wert aus dem School-Objekt im Aufrufer entfernen. Lädt SaveChanges den Wert der Nullnavigationseigenschaften neu? Andernfalls sollte der Aufrufer das City-Objekt nach dem Aufrufen der Insert () -Methode neu laden, wenn er diese Informationen benötigt. Dies ist das Muster, das ich oft verwendet habe, aber ich bin immer noch offen für bessere Muster, wenn jemand ein gutes hat.
Einseitig
1
Das war sehr hilfreich für mich. EntityState.Unchaged ist genau das, was ich brauchte, um ein Objekt zuzuweisen, das einen Fremdschlüssel für die Nachschlagetabelle in einem großen Objektdiagramm darstellt, das ich als einzelne Transaktion gespeichert habe. EF Core gibt eine weniger als intuitive Fehlermeldung IMO aus. Ich habe meine Nachschlagetabellen zwischengespeichert, die sich aus Leistungsgründen selten ändern. Sie gehen davon aus, dass die PK des Nachschlagetabellenobjekts mit der in der Datenbank bereits vorhandenen identisch ist und dass sie die FK nur dem vorhandenen Element zuweist. Anstatt zu versuchen, das Nachschlagetabellenobjekt als neu einzufügen.
tnk479
Für mich funktioniert keine Kombination aus Nullen oder unverändertem Status. EF besteht darauf, dass ein neuer untergeordneter Datensatz eingefügt werden muss, wenn nur ein Skalarfeld im übergeordneten Feld aktualisiert werden soll.
BobRz
21

Wenn Sie nur Änderungen an einem übergeordneten Objekt speichern und das Speichern von Änderungen an einem der untergeordneten Objekte vermeiden möchten, gehen Sie wie folgt vor:

using (var ctx = new MyContext())
{
    ctx.Parents.Attach(parent);
    ctx.Entry(parent).State = EntityState.Added;  // or EntityState.Modified
    ctx.SaveChanges();
}

In der ersten Zeile werden das übergeordnete Objekt und das gesamte Diagramm der abhängigen untergeordneten Objekte an den Kontext im UnchangedStatus angehängt.

In der zweiten Zeile wird der Status nur für das übergeordnete Objekt geändert, sodass die untergeordneten Objekte im UnchangedStatus verbleiben .

Beachten Sie, dass ich einen neu erstellten Kontext verwende, damit keine anderen Änderungen an der Datenbank gespeichert werden.

keykey76
quelle
2
Diese Antwort hat den Vorteil, dass, wenn jemand später vorbeikommt und ein untergeordnetes Objekt hinzufügt, der vorhandene Code nicht beschädigt wird. Dies ist eine "Opt-In" -Lösung, bei der die anderen verlangen, dass Sie untergeordnete Objekte explizit ausschließen.
Jim
Dies funktioniert im Kern nicht mehr. "Anhängen: Hängt jede erreichbare Entität an, außer wenn eine erreichbare Entität einen vom Geschäft generierten Schlüssel hat und kein Schlüsselwert zugewiesen ist. Diese werden als hinzugefügt markiert." Wenn auch Kinder neu sind, werden sie hinzugefügt.
mmix
14

Eine der vorgeschlagenen Lösungen besteht darin, die Navigationseigenschaft aus demselben Datenbankkontext zuzuweisen. In dieser Lösung wird die von außerhalb des Datenbankkontexts zugewiesene Navigationseigenschaft ersetzt. Zur Veranschaulichung siehe folgendes Beispiel.

class Company{
    public int Id{get;set;}
    public Virtual Department department{get; set;}
}
class Department{
    public int Id{get; set;}
    public String Name{get; set;}
}

In Datenbank speichern:

 Company company = new Company();
 company.department = new Department(){Id = 45}; 
 //an Department object with Id = 45 exists in database.    

 using(CompanyContext db = new CompanyContext()){
      Department department = db.Departments.Find(company.department.Id);
      company.department = department;
      db.Companies.Add(company);
      db.SaveChanges();
  }

Microsoft schreibt dies als Feature ein, aber ich finde das ärgerlich. Wenn das dem Firmenobjekt zugeordnete Abteilungsobjekt eine ID hat, die bereits in der Datenbank vorhanden ist, warum ordnet EF dann nicht einfach das Firmenobjekt dem Datenbankobjekt zu? Warum sollten wir uns selbst um den Verein kümmern müssen? Die Pflege der Navigationseigenschaft beim Hinzufügen eines neuen Objekts ist so etwas wie das Verschieben der Datenbankoperationen von SQL nach C #, was für die Entwickler umständlich ist.

Bikash Bishwokarma
quelle
Ich stimme zu 100% zu, dass es lächerlich ist, dass EF versucht, einen neuen Datensatz zu erstellen, wenn die ID angegeben wird. Wenn es eine Möglichkeit gäbe, Auswahllistenoptionen mit dem tatsächlichen Objekt zu füllen, wären wir im Geschäft.
T3.0
10

Zunächst müssen Sie wissen, dass es zwei Möglichkeiten gibt, die Entität in EF zu aktualisieren.

  • Angehängte Objekte

Wenn Sie die Beziehung der an den Objektkontext angehängten Objekte mithilfe einer der oben beschriebenen Methoden ändern, muss das Entity Framework Fremdschlüssel, Verweise und Sammlungen synchron halten.

  • Getrennte Objekte

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

In der Anwendung, die ich erstelle, wird das EF-Objektmodell nicht aus der Datenbank geladen, sondern als Datenobjekt verwendet, das ich beim Parsen einer Einfachdatei auffülle.

Das bedeutet, dass Sie mit nicht verbundenen Objekten arbeiten, aber es ist unklar, ob Sie eine unabhängige Zuordnung oder eine Fremdschlüsselzuordnung verwenden .

  • Hinzufügen

    Wenn beim Hinzufügen einer neuen Entität mit einem vorhandenen untergeordneten Objekt (Objekt, das in der Datenbank vorhanden ist) das untergeordnete Objekt nicht von EF verfolgt wird, wird das untergeordnete Objekt erneut eingefügt. Es sei denn, Sie hängen das untergeordnete Objekt zuerst manuell an.

      db.Entity(entity.ChildObject).State = EntityState.Modified;
      db.Entity(entity).State = EntityState.Added;
  • Aktualisieren

    Sie können die Entität einfach als geändert markieren, dann werden alle Skalareigenschaften aktualisiert und die Navigationseigenschaften werden einfach ignoriert.

      db.Entity(entity).State = EntityState.Modified;

Graph Diff

Wenn Sie den Code bei der Arbeit mit nicht verbundenen Objekten vereinfachen möchten, können Sie versuchen, die Diff-Bibliothek grafisch darzustellen .

Hier ist die Einführung: Einführung in GraphDiff für Entity Framework-Code zuerst - Ermöglichen automatisierter Aktualisierungen eines Diagramms von getrennten Entitäten .

Beispielcode

  • Entität einfügen, falls nicht vorhanden, andernfalls aktualisieren.

      db.UpdateGraph(entity);
  • Entität einfügen , wenn sie nicht vorhanden ist, andernfalls aktualisieren UND untergeordnetes Objekt einfügen, falls nicht vorhanden, andernfalls aktualisieren.

      db.UpdateGraph(entity, map => map.OwnedEntity(x => x.ChildObject));
Yuliam Chandra
quelle
3

Der beste Weg, dies zu tun, besteht darin, die SaveChanges-Funktion in Ihrem Datenkontext zu überschreiben.

    public override int SaveChanges()
    {
        var added = this.ChangeTracker.Entries().Where(e => e.State == System.Data.EntityState.Added);

        // Do your thing, like changing the state to detached
        return base.SaveChanges();
    }
Wouter Schut
quelle
2

Ich habe das gleiche Problem, wenn ich versuche, ein Profil zu speichern. Ich habe bereits eine Tabellenanrede und ein neues Profil erstellt. Wenn ich ein Profil einfüge, wird es auch in die Anrede eingefügt. Also habe ich es vor savechanges () so versucht.

db.Entry (Profile.Salutation) .State = EntityState.Unchanged;

ZweHtatNaing
quelle
1

Das hat bei mir funktioniert:

// temporarily 'detach' the child entity/collection to have EF not attempting to handle them
var temp = entity.ChildCollection;
entity.ChildCollection = new HashSet<collectionType>();

.... do other stuff

context.SaveChanges();

entity.ChildCollection = temp;
Klaus
quelle
0

Wir haben vor dem Hinzufügen des übergeordneten Elements zum dbset die untergeordneten Sammlungen vom übergeordneten getrennt, die vorhandenen Sammlungen auf andere Variablen verschoben, um später mit ihnen arbeiten zu können, und anschließend die aktuellen untergeordneten Sammlungen durch neue leere Sammlungen ersetzt. Das Setzen der untergeordneten Sammlungen auf null / nichts schien für uns fehlzuschlagen. Fügen Sie anschließend das übergeordnete Element zum DBSet hinzu. Auf diese Weise werden die Kinder erst hinzugefügt, wenn Sie dies wünschen.

Shaggie
quelle
0

Ich weiß, dass es ein alter Beitrag ist. Wenn Sie jedoch den Code-First-Ansatz verwenden, können Sie das gewünschte Ergebnis erzielen, indem Sie den folgenden Code in Ihrer Zuordnungsdatei verwenden.

Ignore(parentObject => parentObject.ChildObjectOrCollection);

Dadurch wird EF grundsätzlich angewiesen, die Eigenschaft "ChildObjectOrCollection" vom Modell auszuschließen, damit sie nicht der Datenbank zugeordnet wird.

Syed Dänisch
quelle
In welchem ​​Kontext wird "Ignorieren" verwendet? Es scheint im vermuteten Kontext nicht zu existieren.
T3.0
0

Ich hatte eine ähnliche Herausforderung mit Entity Framework Core 3.1.0, meine Repo-Logik ist ziemlich allgemein.

Das hat bei mir funktioniert:

builder.Entity<ChildEntity>().HasOne(c => c.ParentEntity).WithMany(l =>
       l.ChildEntity).HasForeignKey("ParentEntityId");

Bitte beachten Sie, dass "ParentEntityId" der Name der Fremdschlüsselspalte in der untergeordneten Entität ist. Ich habe die oben genannte Codezeile zu dieser Methode hinzugefügt:

protected override void OnModelCreating(ModelBuilder builder)...
Manelisi Nodada
quelle