DbSet.Attach (Entität) vs DbContext.Entry (Entität) .State = EntityState.Modified

115

Wenn ich mich in einem getrennten Szenario befinde und vom Client ein Dto erhalte, das ich einer Entität zuordne, um es zu speichern, gehe ich folgendermaßen vor:

context.Entry(entity).State = EntityState.Modified;
context.SaveChanges();

Für was ist dann die DbSet.Attach(entity)

oder warum sollte ich die .Attach-Methode verwenden, wenn EntityState.Modified die Entität bereits anfügt?

Elisabeth
quelle
Fügen Sie besser einige Versionsinformationen hinzu, dies wurde bereits zuvor gefragt. Mir ist nicht klar, ob dies eine neue Frage verdient.
Henk Holterman

Antworten:

277

Wenn Sie dies tun context.Entry(entity).State = EntityState.Modified;, hängen Sie die Entität nicht nur an die an DbContext, sondern markieren auch die gesamte Entität als fehlerhaft. Dies bedeutet, dass context.SaveChanges()EF in diesem Fall eine Aktualisierungsanweisung generiert, die alle Felder der Entität aktualisiert .

Dies ist nicht immer erwünscht.

Hängt andererseits DbSet.Attach(entity)die Entität an den Kontext an, ohne sie als schmutzig zu markieren. Es ist gleichbedeutend mit Tuncontext.Entry(entity).State = EntityState.Unchanged;

Wenn Sie beim Anhängen auf diese Weise eine Eigenschaft für die Entität context.SaveChanges()aktualisieren, generiert EF beim nächsten Aufruf keine Datenbankaktualisierung für diese Entität.

Selbst wenn Sie vorhaben, eine Entität zu aktualisieren, wenn die Entität viele Eigenschaften (DB-Spalten) hat, Sie aber nur einige aktualisieren möchten, ist es möglicherweise vorteilhaft, eine DbSet.Attach(entity)durchzuführen und dann nur die wenigen Eigenschaften zu aktualisieren das muss aktualisiert werden. Auf diese Weise wird eine effizientere Update-Anweisung von EF generiert. EF aktualisiert nur die von Ihnen geänderten Eigenschaften (im Gegensatz dazu context.Entry(entity).State = EntityState.Modified;werden alle Eigenschaften / Spalten aktualisiert).

Relevante Dokumentation: Add / Attach und Entity States .

Codebeispiel

Angenommen, Sie haben die folgende Entität:

public class Person
{
    public int Id { get; set; } // primary key
    public string FirstName { get; set; }
    public string LastName { get; set; }
}

Wenn Ihr Code so aussieht:

context.Entry(personEntity).State = EntityState.Modified;
context.SaveChanges();

Das generierte SQL sieht ungefähr so ​​aus:

UPDATE person
SET FirstName = 'whatever first name is',
    LastName = 'whatever last name is'
WHERE Id = 123; -- whatever Id is.

Beachten Sie, wie die obige Update-Anweisung alle Spalten aktualisiert, unabhängig davon, ob Sie die Werte tatsächlich geändert haben oder nicht.

Im Gegensatz dazu, wenn Ihr Code den "normalen" Anhang wie folgt verwendet:

context.People.Attach(personEntity); // State = Unchanged
personEntity.FirstName = "John"; // State = Modified, and only the FirstName property is dirty.
context.SaveChanges();

Dann ist die generierte Update-Anweisung anders:

UPDATE person
SET FirstName = 'John'
WHERE Id = 123; -- whatever Id is.

Wie Sie sehen können, aktualisiert die Update-Anweisung nur die Werte, die tatsächlich geändert wurden, nachdem Sie die Entität an den Kontext angehängt haben. Abhängig von der Struktur Ihrer Tabelle kann sich dies positiv auf die Leistung auswirken.

Welche Option für Sie besser ist, hängt ganz davon ab, was Sie versuchen.

sstan
quelle
1
EF generiert die WHERE-Klausel nicht auf diese Weise. Wenn Sie eine mit new (dh new Entity ()) erstellte Entität angehängt und auf modifiziert gesetzt haben, müssen Sie aufgrund der optimistischen Sperre alle ursprünglichen Felder festlegen. Die in der UPDATE-Abfrage generierte WHERE-Klausel enthält normalerweise alle ursprünglichen Felder (nicht nur die ID). Wenn Sie dies nicht tun, löst EF eine Parallelitätsausnahme aus.
Bubi
3
@budi: Danke für dein Feedback. Ich habe erneut getestet, um sicherzugehen, dass sich eine grundlegende Entität wie beschrieben verhält, wobei die WHEREKlausel nur den Primärschlüssel enthält und keine Parallelitätsprüfung durchgeführt wird. Um die Parallelität zu überprüfen, muss ich eine Spalte explizit als Parallelitätstoken oder rowVersion konfigurieren. In diesem Fall enthält die WHEREKlausel nur den Primärschlüssel und die Spalte für das Parallelitätstoken, nicht alle Felder. Wenn Ihre Tests etwas anderes zeigen, würde ich gerne davon hören.
Sstan
Wie kann ich dynamisch feststellen, dass die Eigenschaft geändert wurde?
Navid_pdp11
2
@ Navid_pdp11 DbContext.Entry(person).CurrentValuesund DbContext.Entry(person).OriginalValues.
Shimmy Weitzhandler
Vielleicht etwas abseits des Themas, aber wenn ich ein Repository-Muster verwende, muss ich für jedes Modell ein Repository erstellen, da jedes Modell eine Entität hat, die sich beim Einfügen eines neuen Datensatzes in db in einem nicht verfolgten Zustand befinden muss Ein generisches Repository, das Entitäten beim Einfügen an den Kontext anfügt. Wie gehst du am besten damit um?
Jayasurya_j
3

Wenn Sie die DbSet.UpdateMethode verwenden, markiert Entity Framework alle Eigenschaften Ihrer Entität als EntityState.Modifiedund verfolgt sie so. Wenn Sie nur einige Ihrer Eigenschaften ändern möchten, nicht alle DbSet.Attach. Diese Methode erstellt alle Ihre Eigenschaften EntityState.Unchanged, daher müssen Sie Ihre Eigenschaften erstellen, die Sie aktualisieren möchten EntityState.Modified. Wenn die App auf trifft DbContext.SaveChanges, werden nur geänderte Eigenschaften ausgeführt.

Orhun
quelle
0

Nur zusätzlich (zu der markierten Antwort) gibt es einen wichtigen Unterschied zwischen context.Entry(entity).State = EntityState.Unchangedund context.Attach(entity)(in EF Core):

Ich habe einige Tests durchgeführt, um es selbst besser zu verstehen (daher umfasst dies auch einige allgemeine Referenztests). Dies ist also mein Testszenario:

  • Ich habe EF Core 3.1.3 verwendet
  • ich benutzte QueryTrackingBehavior.NoTracking
  • Ich habe nur Attribute für die Zuordnung verwendet (siehe unten)
  • Ich habe verschiedene Kontexte verwendet, um die Bestellung zu erhalten und die Bestellung zu aktualisieren
  • Ich habe die ganze Datenbank für jeden Test abgewischt

Dies sind die Modelle:

public class Order
{
    public int Id { get; set; }
    public string Comment { get; set; }
    public string ShippingAddress { get; set; }
    public DateTime? OrderDate { get; set; }
    public List<OrderPos> OrderPositions { get; set; }
    [ForeignKey("OrderedByUserId")]
    public User OrderedByUser { get; set; }
    public int? OrderedByUserId { get; set; }
}

public class OrderPos
{
    public int Id { get; set; }
    public string ArticleNo { get; set; }
    public int Quantity { get; set; }
    [ForeignKey("OrderId")]
    public Order Order { get; set; }
    public int? OrderId { get; set; }
}

public class User
{
    public int Id { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
}

Dies sind die (ursprünglichen) Testdaten in der Datenbank: Geben Sie hier die Bildbeschreibung ein

So erhalten Sie die Bestellung:

order = db.Orders.Include(o => o.OrderPositions).Include(o => o.OrderedByUser).FirstOrDefault();

Nun die Tests:

Einfaches Update mit EntityState :

db.Entry(order).State = EntityState.Unchanged;
order.ShippingAddress = "Germany"; // will be UPDATED
order.OrderedByUser.FirstName = "William (CHANGED)"; // will be IGNORED
order.OrderPositions[0].ArticleNo = "K-1234 (CHANGED)"; // will be IGNORED
order.OrderPositions.Add(new OrderPos { ArticleNo = "T-5555 (NEW)", Quantity = 5 }); // will be INSERTED
db.SaveChanges();
// Will generate SQL in 2 Calls:
// INSERT INTO [OrderPositions] ([ArticleNo], [OrderId], [Quantity]) VALUES ('T-5555', 1, 5)
// UPDATE [Orders] SET [ShippingAddress] = 'Germany' WHERE [Id] = 1

Einfaches Update mit Anhängen :

db.Attach(order);
order.ShippingAddress = "Germany"; // will be UPDATED
order.OrderedByUser.FirstName = "William (CHANGED)"; // will be UPDATED
order.OrderPositions[0].ArticleNo = "K-1234 (CHANGED)"; // will be UPDATED
order.OrderPositions.Add(new OrderPos { ArticleNo = "T-5555 (NEW)", Quantity = 5 }); // will be INSERTED
db.SaveChanges();
// Will generate SQL in 1 Call:
// UPDATE [OrderPositions] SET [ArticleNo] = 'K-1234' WHERE [Id] = 1
// INSERT INTO [OrderPositions] ([ArticleNo], [OrderId], [Quantity]) VALUES ('T-5555 (NEW)', 1, 5)
// UPDATE [Orders] SET [ShippingAddress] = 'Germany' WHERE [Id] = 1
// UPDATE [Users] SET [FirstName] = 'William (CHANGED)' WHERE [Id] = 1

Update mit Änderung der untergeordneten IDs mit EntityState :

db.Entry(order).State = EntityState.Unchanged;
order.ShippingAddress = "Germany"; // will be UPDATED
order.OrderedByUser.Id = 3; // will be IGNORED
order.OrderedByUser.FirstName = "William (CHANGED)"; // will be IGNORED
order.OrderPositions[0].Id = 3; // will be IGNORED
order.OrderPositions[0].ArticleNo = "K-1234 (CHANGED)"; // will be IGNORED
order.OrderPositions.Add(new OrderPos { ArticleNo = "T-5555 (NEW)", Quantity = 5 }); // will be INSERTED
db.SaveChanges();
// Will generate SQL in 2 Calls:
// INSERT INTO [OrderPositions] ([ArticleNo], [OrderId], [Quantity]) VALUES ('T-5555', 1, 5)
// UPDATE [Orders] SET [ShippingAddress] = 'Germany' WHERE [Id] = 1

Update mit Änderung der untergeordneten IDs mit Anhängen :

db.Attach(order);
order.ShippingAddress = "Germany"; // would be UPDATED
order.OrderedByUser.Id = 3; // will throw EXCEPTION
order.OrderedByUser.FirstName = "William (CHANGED)"; // would be UPDATED
order.OrderPositions[0].Id = 3; // will throw EXCEPTION
order.OrderPositions[0].ArticleNo = "K-1234 (CHANGED)"; // would be UPDATED
order.OrderPositions.Add(new OrderPos { ArticleNo = "T-5555 (NEW)", Quantity = 5 }); // would be INSERTED
db.SaveChanges();
// Throws Exception: The property 'Id' on entity type 'User' is part of a key and so cannot be modified or marked as modified. To change the principal of an existing entity with an identifying foreign key first delete the dependent and invoke 'SaveChanges' then associate the dependent with the new principal.)

Hinweis: Dies löst eine Ausnahme aus, unabhängig davon, ob die ID geändert oder auf den ursprünglichen Wert gesetzt wurde. Der Status der ID scheint auf "geändert" gesetzt zu sein, und dies ist nicht zulässig (da es sich um den Primärschlüssel handelt).

Update mit Änderung der untergeordneten IDs als neu (kein Unterschied zwischen EntityState und Attach):

db.Attach(order); // or db.Entry(order).State = EntityState.Unchanged;
order.OrderedByUser = new User();
order.OrderedByUser.Id = 3; // // Reference will be UPDATED
order.OrderedByUser.FirstName = "William (CHANGED)"; // will be UPDATED (on User 3)
db.SaveChanges();
// Will generate SQL in 2 Calls:
// UPDATE [Orders] SET [OrderedByUserId] = 3, [ShippingAddress] = 'Germany' WHERE [Id] = 1
// UPDATE [Users] SET [FirstName] = 'William (CHANGED)' WHERE [Id] = 3

Hinweis: Sehen Sie den Unterschied zum Update mit EntityState ohne neues (oben). Dieses Mal wird der Name aufgrund der neuen Benutzerinstanz aktualisiert.

Update mit Änderung der Referenz- IDs mit EntityState :

db.Entry(order).State = EntityState.Unchanged;
order.ShippingAddress = "Germany"; // will be UPDATED
order.OrderedByUserId = 3; // will be UPDATED
order.OrderedByUser.Id = 2; // will be IGNORED
order.OrderedByUser.FirstName = "William (CHANGED)"; // will be IGNORED
order.OrderPositions[0].Id = 3; // will be IGNORED
order.OrderPositions[0].ArticleNo = "K-1234 (CHANGED)"; // will be IGNORED
order.OrderPositions.Add(new OrderPos { ArticleNo = "T-5555 (NEW)", Quantity = 5 }); // will be INSERTED
db.SaveChanges();
// Will generate SQL in 2 Calls:
// INSERT INTO [OrderPositions] ([ArticleNo], [OrderId], [Quantity]) VALUES ('T-5555', 1, 5)
// UPDATE [Orders] SET [OrderedByUserId] = 3, [ShippingAddress] = 'Germany' WHERE [Id] = 1

Update mit Änderung der Referenz-IDs mit Anhängen :

db.Attach(order);
order.ShippingAddress = "Germany"; // will be UPDATED
order.OrderedByUserId = 3; // will be UPDATED
order.OrderedByUser.FirstName = "William (CHANGED)"; // will be UPDATED (on FIRST User!)
order.OrderPositions[0].ArticleNo = "K-1234 (CHANGED)"; // will be UPDATED
order.OrderPositions.Add(new OrderPos { ArticleNo = "T-5555 (NEW)", Quantity = 5 }); // will be INSERTED
db.SaveChanges();
// Will generate SQL in 1 Call:
// UPDATE [OrderPositions] SET [ArticleNo] = 'K-1234' WHERE [Id] = 1
// INSERT INTO [OrderPositions] ([ArticleNo], [OrderId], [Quantity]) VALUES ('T-5555 (NEW)', 1, 5)
// UPDATE [Orders] SET [OrderedByUserId] = 3, [ShippingAddress] = 'Germany' WHERE [Id] = 1
// UPDATE [Users] SET [FirstName] = 'William (CHANGED)' WHERE [Id] = 1

Hinweis: Die Referenz wird in Benutzer 3 geändert, aber auch Benutzer 1 wird aktualisiert. Ich denke, dies liegt daran, dass der order.OrderedByUser.Idunverändert ist (es ist immer noch 1).

Fazit Mit EntityState haben Sie mehr Kontrolle, müssen jedoch die Untereigenschaften (zweite Ebene) selbst aktualisieren. Mit Anhängen können Sie alles aktualisieren (ich denke mit allen Eigenschaftenebenen), aber Sie müssen die Referenzen im Auge behalten. Nur zum Beispiel: Wenn User (OrderedByUser) ein DropDown wäre, könnte das Ändern des Werts über ein DropDown das gesamte User-Objekt überschreiben. In diesem Fall würde der ursprüngliche dropDown-Wert anstelle der Referenz überschrieben.

Für mich ist der beste Fall, Objekte wie OrderedByUser auf null zu setzen und nur die order.OrderedByUserId auf den neuen Wert zu setzen, wenn ich nur die Referenz ändern möchte (egal ob EntityState oder Attach).

Hoffe das hilft, ich weiß es ist viel Text: D.

StewieG
quelle