DbSet.Attach (entità) vs DbContext.Entry (entità) .State = EntityState.Modified


115

Quando sono in uno scenario distaccato e ottengo un dto dal client che mappo in un'entità per salvarlo, faccio questo:

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

Per quello che è allora il file DbSet.Attach(entity)

o perché dovrei usare il metodo .Attach quando EntityState.Modified allega già l'entità?


Meglio aggiungere alcune informazioni sulla versione, questo è stato chiesto prima. Non mi è chiaro se questo meriti una nuova domanda.
Henk Holterman

Risposte:


278

Quando lo fai context.Entry(entity).State = EntityState.Modified;, non stai solo attaccando l'entità a DbContext, ma stai anche contrassegnando l'intera entità come sporca. Ciò significa che quando lo fai context.SaveChanges(), EF genererà un'istruzione di aggiornamento che aggiornerà tutti i campi dell'entità.

Questo non è sempre desiderato.

D'altra parte, DbSet.Attach(entity)collega l'entità al contesto senza contrassegnarla come sporca. È equivalente al farecontext.Entry(entity).State = EntityState.Unchanged;

Quando si collega in questo modo, a meno che non si proceda ad aggiornare una proprietà sull'entità, la prossima volta che si chiama context.SaveChanges()EF non verrà generato un aggiornamento del database per questa entità.

Anche se stai pianificando di fare un aggiornamento a un'entità, se l'entità ha molte proprietà (colonne db) ma vuoi aggiornarne solo alcune, potresti trovare vantaggioso fare a DbSet.Attach(entity), e quindi aggiornare solo le poche proprietà che necessitano di aggiornamento. In questo modo si genererà un'istruzione di aggiornamento più efficiente da EF. EF aggiornerà solo le proprietà che hai modificato (al contrario di context.Entry(entity).State = EntityState.Modified;ciò causerà l'aggiornamento di tutte le proprietà / colonne)

Documentazione pertinente: Aggiungi / Allega e Stati entità .

Esempio di codice

Supponiamo che tu abbia la seguente entità:

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

Se il tuo codice ha questo aspetto:

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

L'SQL generato sarà simile a questo:

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

Nota come la dichiarazione di aggiornamento sopra aggiornerà tutte le colonne, indipendentemente dal fatto che tu abbia effettivamente modificato i valori o meno.

Al contrario, se il tuo codice utilizza l'allegato "normale" in questo modo:

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

Quindi l'istruzione di aggiornamento generata è diversa:

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

Come puoi vedere, la dichiarazione di aggiornamento aggiorna solo i valori che sono stati effettivamente modificati dopo aver collegato l'entità al contesto. A seconda della struttura della tabella, ciò può avere un impatto positivo sulle prestazioni.

Ora, quale opzione è migliore per te dipende interamente da ciò che stai cercando di fare.


1
EF non genera la clausola WHERE in questo modo. Se hai allegato un'entità creata con new (es. New Entity ()) e l'hai impostata su modificata devi impostare tutti i campi originali a causa del blocco ottimistico. La clausola WHERE generata nella query UPDATE di solito contiene tutti i campi originali (non solo l'ID), quindi se non lo fai, EF genererà un'eccezione di concorrenza.
bubi

3
@ budi: grazie per il tuo feedback. Ho ripetuto il test per sicurezza e, per un'entità di base, si comporta come descritto, con la WHEREclausola contenente solo la chiave primaria e senza alcun controllo di concorrenza. Per avere il controllo della concorrenza, è necessario configurare in modo esplicito una colonna come token di concorrenza o rowVersion. In tal caso, la WHEREclausola avrà solo la chiave primaria e la colonna del token di concorrenza, non tutti i campi. Se i tuoi test mostrano il contrario, mi piacerebbe saperne di più.
sstan

come posso trovare dinamicamente la proprietà della strega modificata?
Navid_pdp11

2
@ Navid_pdp11 DbContext.Entry(person).CurrentValuese DbContext.Entry(person).OriginalValues.
Shimmy Weitzhandler

potrebbe essere leggermente fuori tema, ma se utilizzo un modello di repository, devo creare un repository per ogni modello poiché ogni modello ha un'entità che deve essere in uno stato non tracciato durante l'inserimento di un nuovo record in db, quindi non posso avere un repository generico che collega le entità al contesto durante l'inserimento. Come gestisci questo al meglio?
jayasurya_j

3

Quando usi il DbSet.Updatemetodo, Entity Framework contrassegna tutte le proprietà dell'entità come EntityState.Modified, quindi ne tiene traccia. Se vuoi modificare solo alcune delle tue proprietà, non tutte, usa DbSet.Attach. Questo metodo crea tutte le tue proprietà EntityState.Unchanged, quindi devi creare le tue proprietà che desideri aggiornare EntityState.Modified. Pertanto, quando l'app raggiunge DbContext.SaveChanges, funzionerà solo le proprietà modificate.


0

Solo in aggiunta (alla risposta contrassegnata) c'è un'importante differenza tra context.Entry(entity).State = EntityState.Unchangede context.Attach(entity)(in EF Core):

Ho fatto alcuni test per capirlo di più da solo (quindi questo include anche alcuni test di riferimento generali), quindi questo è il mio scenario di test:

  • Ho usato EF Core 3.1.3
  • ero solito QueryTrackingBehavior.NoTracking
  • Ho usato solo attributi per la mappatura (vedi sotto)
  • Ho utilizzato contesti diversi per ottenere l'ordine e per aggiornare l'ordine
  • Ho cancellato l'intero db per ogni test

Questi sono i modelli:

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

Questi sono i dati del test (originale) nel database: inserisci qui la descrizione dell'immagine

Per ottenere l'ordine:

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

Ora i test:

Aggiornamento semplice con 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

Aggiornamento semplice con Allega :

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

Aggiorna con la modifica degli ID figlio con 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

Aggiorna con la modifica degli ID bambino con Allega :

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

Nota: questo genera un'eccezione, indipendentemente dal fatto che l'ID sia stato modificato o sia stato impostato sul valore originale, sembra che lo stato dell'ID sia impostato su "modificato" e ciò non è consentito (perché è la chiave primaria)

Aggiorna con la modifica degli ID figlio come nuovi (nessuna differenza tra EntityState e Allega):

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

Nota: vedere la differenza con l'aggiornamento con EntityState senza nuovo (sopra). Questa volta il nome verrà aggiornato, a causa della nuova istanza utente.

Aggiorna con la modifica degli ID di riferimento con 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

Aggiorna con la modifica degli ID di riferimento con Allega :

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

Nota: il riferimento verrà modificato in Utente 3, ma anche l'utente 1 verrà aggiornato, immagino che ciò sia dovuto al fatto che order.OrderedByUser.Idè invariato (è ancora 1).

Conclusione Con EntityState hai più controllo, ma devi aggiornare le proprietà secondarie (di secondo livello) da solo. Con Attach puoi aggiornare tutto (immagino con tutti i livelli di proprietà), ma devi tenere d'occhio i riferimenti. Solo per esempio: se User (OrderedByUser) fosse un dropDown, la modifica del valore tramite un dropDown potrebbe sovrascrivere l'intero oggetto User. In questo caso il valore dropDown originale verrebbe sovrascritto al posto del riferimento.

Per me il caso migliore è impostare oggetti come OrderedByUser su null e impostare solo order.OrderedByUserId sul nuovo valore, se voglio solo cambiare il riferimento (non importa se EntityState o Attach).

Spero che questo aiuti, so che è molto testo: D

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.