.Net

Entity Framework Core : optimisation sur les update

Déc 10, 2020

Nicolas Bailly

Dans cet article nous allons aborder une subtilité de Entity Framework Core qui permet d’optimiser les requêtes de mises à jour envoyées à la base de données.
Entity Framework est devenu un ORM puissant qui facilite le travaille des développeurs. L’inconvénient est qu’il camoufle ce qu’il se passe derrière donnant l’impression que c’est magique. Une certaine maîtrise du SQL semble nécessaire pour bien comprendre comment l’utiliser.

Cas concret

Si vous n’y prenez pas garde, certains conflits peuvent survenir lors de la mise à jour de vos données. Et ces conflits ont toutes les chances de se produire sur l’environnement de production sans que vous ne le détectiez avant, si vous n’avez pas les tests suffisants.

Ceci arrive, si vous avez plusieurs applications qui mettent à jour en même temps la même ligne d’une table. La solution que je vous propose permet de cibler la colonne à mettre à jour.

Prenons l’exemple d’un objet Order qui représente la table des commandes. Dans cette table, nous avons l’identifiant de la commande, l’identifiant de l’utilisateur, l’identifiant du statut du paiement et l’adresse de facturation :

public class Order
{
    public Order(){}

    [Key]
    public int Id { get; set; }

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

    public int PaymentStatusId { get; set; }

    public string BillingAddress { get; set; }
}

En base de données, nous pouvons voir la table correspondante :

Maintenant, récupérons la ligne ayant l’identifiant 1 et mettons à jour le champ BillingAddress :

//Récupération de l'objet d'ID 1
int id = 1;
using ExampleContext dbContext = new ExampleContext(options);

var entity = await dbContext.Set<Order>().FindAsync(id).ConfigureAwait(false);

//Mise à jour de l'objet
entity.BillingAddress = "10 rue de la paix";

var updatedEntity = dbContext.Set<Order>().Update(entity);
await dbContext.SaveChangesAsync();

Regardons ce que donne les traces :

Nous pouvons voir dans la requête SQL, que les champs BillingAddress, PaymentStatusId et UserId ont été mis à jour alors que dans le code C#, seul le champ BillingAddress a été mis à jour.

C’est là où les conflits peuvent se faire. Si une autre application met à jour le champ PaymentStatusId de la même manière, elle va écraser la ligne que l’on vient de mettre à jour.

Simulons, ce conflit en mettant un point d’arrêt juste avant la mise à jour :

Nous avons ici récupéré notre objet avec les bonnes informations.
Tout en maintenant le point d’arrêt, mettons à jour cette ligne directement en base de données :

Nous avons mis à jour, uniquement le champ PaymentStatus. Notre application se trouve donc désynchronisée de la base de données car elle n’a pas cette mise à jour.
Continuons l’exécution de notre application en débloquant le point d’arrêt. Entity Framework va donc lancer l’Update.

Regardons ce que ça donne en base de données :

Nous remarquons que notre application a écrasé la mise jour effectuée juste avant puisque le champs PaymentStatusId est revenu à 1.

Pour éviter ce problème, Entity Framework nous permet de ne mettre à jour qu’une seule colonne dans notre table en utilisant le code suivant :

dbContext.Entry(order).Property(o=>o.BillingAddress).IsModified = true;

Grâce à ceci, nous précisons que seule la propriété BillingAddress de notre objet order a été mise à jour et ainsi seule la colonne concernée sera mise à jour.
Nous pouvons même nous permettre de ne pas récupérer l’objet en entier, à partir du moment où nous avons son identifiant :

using ExampleContext dbContext = new ExampleContext(options);
Order order = new Order()
{
    Id = 1,
    BillingAddress = "10 rue de la paix"
};

dbContext.Entry(order).Property(o=>o.BillingAddress).IsModified = true;
await dbContext.SaveChangesAsync();

Si nous regardons ce que donnent les traces :

Nous voyons bien que seule la colonne BillingAddress a été mise à jour.
A noter, que nous mettons à jour une seule colonne, mais il est possible d’en préciser autant que l’on souhaite.
Voici une méthode qui permet de rendre générique les mises à jour :

public async Task UpdateAsync(Order entity, CancellationToken ct,
            params Expression<Func<Order, object>>[] propertyExpressions)
{
   if (entity == null)
      throw new ArgumentNullException(nameof(entity));

   foreach (var propertyExpression in propertyExpressions)
   {
      _dbContext.Entry(entity).Property(propertyExpression).IsModified = true;
   }

   await _dbContext.SaveChangesAsync(ct).ConfigureAwait(false);
}

Ainsi, on peut appeler cette méthode de la manière suivante :

await _repository.UpdateAsync(objectToUpdate, ct, x => x.BillingAddress)

Conclusion

Dans cet article, nous avons simplifié le cas mais cela peut se retrouver dans des systèmes complexes. Le fait de mettre à jour tout le temps tous les champs de la table s’il n’y en a pas besoin peut également générer des problèmes de performances s’il y a un gros trafic.

L’utilisation d’un ORM demande donc une certaine connaissance du SQL pour bien comprendre les requêtes qui sont générées.

2 Commentaires

  1. Simon G

    Merci pour cet article pratique et facile à comprendre ! Je viens d’en appliquer les principes à un de mes projets.

    Réponse
  2. Dali

    Merci. ça m’a aidé dans mon projet.

    Réponse

Soumettre un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Découvrez nos autres articles

git et la face cachée du Rebase

git et la face cachée du Rebase

"Faire une rebase ? *sight* heu... ok..." Jean-Michel Fullstack - Développeur fébrile Jean-Michel est inquiet. En effet, lorsque nous collaborons à plusieurs sur un projet, quelque soit les technologies utilisées, il est important de garder à l'esprit que notre...

lire plus
Aller au contenu principal