Aller au contenu principal
.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 :

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
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; }
}
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; } }
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 :

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
//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();
//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();
//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 :

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
dbContext.Entry(order).Property(o=>o.BillingAddress).IsModified = true;
dbContext.Entry(order).Property(o=>o.BillingAddress).IsModified = true;
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 :

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
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();
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();
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 :

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
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);
}
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); }
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 :

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
await _repository.UpdateAsync(objectToUpdate, ct, x => x.BillingAddress)
await _repository.UpdateAsync(objectToUpdate, ct, x => x.BillingAddress)
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

Laissez une réponse à Dali Annuler la réponse

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

Découvrez nos autres articles

Databricks AI Summit 2025

Databricks AI Summit 2025

Après le Snowflake Summit, Databricks a pris le relais au Data + AI Summit 2025 avec une évolution notable. La plateforme ne se limite plus à l’ingénierie ou à la science des données : elle se positionne désormais comme un système d’activation intelligent, où modèles,...

Lire la suite