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

115

Lorsque je suis dans un scénario détaché et que je reçois un dto du client que je mappe dans une entité pour l'enregistrer, je fais ceci:

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

Car quel est alors le DbSet.Attach(entity)

ou pourquoi devrais-je utiliser la méthode .Attach lorsque EntityState.Modified attache déjà l'entité?

Élisabeth
la source
Mieux vaut ajouter des informations sur la version, cela a déjà été demandé. Je ne sais pas si cela mérite une nouvelle question.
Henk Holterman

Réponses:

278

Lorsque vous le faites context.Entry(entity).State = EntityState.Modified;, vous n'attachez pas seulement l'entité au DbContext, vous marquez également l'entité entière comme sale. Cela signifie que lorsque vous le faites context.SaveChanges(), EF générera une instruction de mise à jour qui mettra à jour tous les champs de l'entité.

Ce n'est pas toujours souhaité.

D'autre part, DbSet.Attach(entity)attache l'entité au contexte sans la marquer comme sale. C'est équivalent à fairecontext.Entry(entity).State = EntityState.Unchanged;

Lors de l'attachement de cette manière, à moins que vous ne procédiez ensuite à la mise à jour d'une propriété sur l'entité, la prochaine fois que vous appelez context.SaveChanges(), EF ne générera pas de mise à jour de base de données pour cette entité.

Même si vous prévoyez de mettre à jour une entité, si l'entité a beaucoup de propriétés (colonnes de base de données) mais que vous ne souhaitez en mettre à jour que quelques-unes, vous trouverez peut-être avantageux de faire une DbSet.Attach(entity), puis de mettre à jour uniquement les quelques propriétés qui ont besoin de mise à jour. Cela générera une instruction de mise à jour plus efficace à partir d'EF. EF ne mettra à jour que les propriétés que vous avez modifiées (contrairement à context.Entry(entity).State = EntityState.Modified;ce qui entraînera la mise à jour de toutes les propriétés / colonnes)

Documentation pertinente: Ajouter / Joindre et États d'entité .

Exemple de code

Disons que vous avez l'entité suivante:

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

Si votre code ressemble à ceci:

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

Le SQL généré ressemblera à ceci:

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

Remarquez comment l'instruction de mise à jour ci-dessus mettra à jour toutes les colonnes, que vous ayez réellement modifié les valeurs ou non.

En revanche, si votre code utilise l'attachement "normal" comme ceci:

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

Ensuite, l'instruction de mise à jour générée est différente:

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

Comme vous pouvez le voir, l'instruction de mise à jour ne met à jour que les valeurs qui ont été réellement modifiées après avoir attaché l'entité au contexte. Selon la structure de votre table, cela peut avoir un impact positif sur les performances.

Maintenant, quelle option est la meilleure pour vous dépend entièrement de ce que vous essayez de faire.

sstan
la source
1
EF ne génère pas la clause WHERE de cette manière. Si vous avez attaché une entité créée avec new (c'est-à-dire new Entity ()) et la définissez comme modifiée, vous devez définir tous les champs d'origine en raison du verrouillage optimiste. La clause WHERE générée dans la requête UPDATE contient généralement tous les champs d'origine (pas seulement Id) donc si vous ne le faites pas, EF lèvera une exception de concurrence.
bubi
3
@budi: Merci pour vos commentaires. J'ai retesté pour en être sûr, et pour une entité de base, elle se comporte comme je l'ai décrit, avec la WHEREclause contenant uniquement la clé primaire, et sans aucun contrôle de concurrence. Pour avoir la vérification de la concurrence, je dois configurer explicitement une colonne en tant que jeton de concurrence ou rowVersion. Dans ce cas, la WHEREclause n'aura que la clé primaire et la colonne de jeton d'accès concurrentiel, pas tous les champs. Si vos tests montrent le contraire, j'aimerais en entendre parler.
sstan
comment puis-je trouver dynamiquement la propriété de la sorcière modifiée?
Navid_pdp11
2
@ Navid_pdp11 DbContext.Entry(person).CurrentValueset DbContext.Entry(person).OriginalValues.
Shimmy Weitzhandler
peut être légèrement hors sujet, mais si j'utilise un modèle de référentiel, je dois créer un référentiel pour chaque modèle car chaque modèle a une entité qui doit être dans un état non suivi lors de l'insertion d'un nouvel enregistrement dans db, donc je ne peux pas avoir un référentiel générique qui attache des entités au contexte lors de l'insertion. Comment gérez-vous cela au mieux?
jayasurya_j
3

Lorsque vous utilisez la DbSet.Updateméthode, Entity Framework marque toutes les propriétés de votre entité comme EntityState.Modified, afin de les suivre. Si vous souhaitez modifier uniquement certaines de vos propriétés, pas toutes, utilisez DbSet.Attach. Cette méthode crée toutes vos propriétés EntityState.Unchanged, vous devez donc définir vos propriétés que vous souhaitez mettre à jour EntityState.Modified. Ainsi, lorsque l'application atteint DbContext.SaveChanges, elle n'opérera que les propriétés modifiées.

Orhun
la source
0

En plus (à la réponse marquée), il existe une différence importante entre context.Entry(entity).State = EntityState.Unchangedet context.Attach(entity)(dans EF Core):

J'ai fait quelques tests pour le comprendre plus par moi-même (cela inclut donc également des tests de référence généraux), voici donc mon scénario de test:

  • J'ai utilisé EF Core 3.1.3
  • j'ai utilisé QueryTrackingBehavior.NoTracking
  • J'ai utilisé uniquement des attributs pour le mappage (voir ci-dessous)
  • J'ai utilisé différents contextes pour obtenir la commande et mettre à jour la commande
  • J'ai effacé toute la base de données pour chaque test

Voici les modèles:

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

Voici les données de test (d'origine) dans la base de données: entrez la description de l'image ici

Pour obtenir la commande:

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

Maintenant les tests:

Mise à jour simple avec 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

Mise à jour simple avec Attach :

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

Mise à jour avec modification des identifiants enfants avec 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

Mise à jour avec modification des identifiants enfants avec Attach :

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

Remarque: cela génère une exception, peu importe si l'ID a été modifié ou a été défini sur la valeur d'origine, il semble que l'état de l'ID soit défini sur «modifié» et ce n'est pas autorisé (car c'est la clé primaire)

Mise à jour avec modification des identifiants enfants comme nouveaux (aucune différence entre EntityState et 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

Remarque: voyez la différence avec la mise à jour avec EntityState sans nouveau (ci-dessus). Cette fois, le nom sera mis à jour, en raison de la nouvelle instance utilisateur.

Mettre à jour avec la modification des ID de référence avec 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

Mettre à jour avec la modification des ID de référence avec Attach :

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

Remarque: la référence sera changée en utilisateur 3, mais aussi l'utilisateur 1 sera mis à jour, je suppose que c'est parce que le order.OrderedByUser.Idest inchangé (c'est toujours 1).

Conclusion Avec EntityState, vous avez plus de contrôle, mais vous devez mettre à jour les sous-propriétés (de deuxième niveau) par vous-même. Avec Attach, vous pouvez tout mettre à jour (je suppose avec tous les niveaux de propriétés), mais vous devez garder un œil sur les références. Juste par exemple: si User (OrderedByUser) était un dropDown, changer la valeur via un dropDown pourrait écraser tout l'objet User. Dans ce cas, la valeur dropDown d'origine serait écrasée à la place de la référence.

Pour moi, le meilleur des cas est de définir des objets comme OrderedByUser sur null et de définir uniquement order.OrderedByUserId sur la nouvelle valeur, si je veux uniquement modifier la référence (peu importe si EntityState ou Attach).

J'espère que cela aide, je sais que c'est beaucoup de texte: D

StewieG
la source