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.

One Comment

Laisser un commentaire

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

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur comment les données de vos commentaires sont utilisées.