Partager via


ExecuteUpdate et ExecuteDelete

Remarque

Cette fonctionnalité a été introduite dans EF Core 7.0.

ExecuteUpdate et ExecuteDeletepermettent tous deux d’enregistrer des données dans la base de données sans utiliser le suivi traditionnel de EF ni la méthode SaveChanges(). Pour une comparaison de ces deux techniques, consultez la page vue d’ensemble sur l’enregistrement des données.

ExecuteDelete

Imaginons que vous devez supprimer tous les blogs ayant une évaluation inférieure à un certain seuil. L’approche traditionnelle SaveChanges() vous oblige à effectuer les opérations suivantes :

foreach (var blog in context.Blogs.Where(b => b.Rating < 3))
{
    context.Blogs.Remove(blog);
}

context.SaveChanges();

Il s’agit d’un moyen peu efficace d’effectuer cette tâche ; il nous faut interroger la base de données pour tous les blogs correspondant à notre filtre, puis interroger, matérialiser et suivre toutes ces instances. Le nombre d’entités correspondantes peut être énorme. Nous devons ensuite dire au suivi des modifications de EF de supprimer chaque blog et d’appliquer ces modifications en appelant SaveChanges(), ce qui génère une instruction DELETE pour chacun d’entre eux.

Voici la même tâche effectuée via l’API ExecuteDelete :

context.Blogs.Where(b => b.Rating < 3).ExecuteDelete();

Cette méthode utilise les opérateurs LINQ habituels pour déterminer quels blogs doivent être affectés, comme si nous les interrogions, puis indique à EF d’exécuter une commande SQL DELETE sur la base de données :

DELETE FROM [b]
FROM [Blogs] AS [b]
WHERE [b].[Rating] < 3

En plus d’être simple et rapide, la tâche s’exécute de manière très efficace dans la base de données, sans charger de données à partir de la base de données ni impliquer le suivi des modifications de EF. Notez que vous pouvez utiliser des opérateurs LINQ arbitraires pour sélectionner les blogs que vous souhaitez supprimer. Ceux-ci sont traduits en langage SQL pour l’exécution sur la base de données, comme si vous interrogiez ces blogs.

ExecuteUpdate

Au lieu de supprimer ces blogs, supposons que nous voulons plutôt modifier une propriété pour indiquer qu’il doivent être masqués. ExecuteUpdate offre un moyen similaire d’exprimer une instruction SQL UPDATE :

context.Blogs
    .Where(b => b.Rating < 3)
    .ExecuteUpdate(setters => setters.SetProperty(b => b.IsVisible, false));

Comme avec ExecuteDelete, nous utilisons d’abord LINQ pour déterminer quels blogs doivent être affectés ; mais avec ExecuteUpdate nous devons également exprimer la modification à appliquer aux blogs correspondants. Pour ce faire, appelez SetProperty dans l’appel ExecuteUpdate et spécifiez les deux arguments suivants : la propriété à modifier (IsVisible), et la nouvelle valeur qu’elle doit avoir (false). Cela entraîne l’exécution du code SQL suivant :

UPDATE [b]
SET [b].[IsVisible] = CAST(0 AS bit)
FROM [Blogs] AS [b]
WHERE [b].[Rating] < 3

Mise à jour de plusieurs propriétés

ExecuteUpdate permet de mettre à jour plusieurs propriétés dans un appel unique. Par exemple, pour définir en même temps la valeur false pour IsVisible et la valeur zéro pour Rating, il suffit de chaîner des appels supplémentaires ensemble via la commande SetProperty :

context.Blogs
    .Where(b => b.Rating < 3)
    .ExecuteUpdate(setters => setters
        .SetProperty(b => b.IsVisible, false)
        .SetProperty(b => b.Rating, 0));

Cela exécute le code SQL suivant :

UPDATE [b]
SET [b].[Rating] = 0,
    [b].[IsVisible] = CAST(0 AS bit)
FROM [Blogs] AS [b]
WHERE [b].[Rating] < 3

Référencement de la valeur d’une propriété existante

Dans les exemples ci-dessus, la propriété est mise à jour en une nouvelle valeur constante. ExecuteUpdate permet également de référencer la valeur de propriété existante lors du calcul de la nouvelle valeur. Par exemple, pour augmenter l’évaluation de tous les blogs correspondants de un, utilisez les éléments suivants :

context.Blogs
    .Where(b => b.Rating < 3)
    .ExecuteUpdate(setters => setters.SetProperty(b => b.Rating, b => b.Rating + 1));

Notez que le deuxième argument deSetProperty est maintenant une fonction lambda, et non une constante comme précédemment. Son paramètre b représente le blog en cours de mise à jour ; dans cette lambda, b.Rating contient donc la note avant que tout changement n’ait eu lieu. Cela exécute le code SQL suivant :

UPDATE [b]
SET [b].[Rating] = [b].[Rating] + 1
FROM [Blogs] AS [b]
WHERE [b].[Rating] < 3

ExecuteUpdate ne prend actuellement pas en charge le référencement des navigations au sein de l’expression lambda SetProperty. Par exemple, supposons que nous voulons mettre à jour les évaluations de chaque blog afin qu’elles correspondent à la moyenne de toutes les évaluations des billets associés. Nous pouvons essayer d’utiliser ExecuteUpdate comme suit :

context.Blogs.ExecuteUpdate(
    setters => setters.SetProperty(b => b.Rating, b => b.Posts.Average(p => p.Rating)));

Toutefois, EF permet d’effectuer cette opération en utilisant d’abord Select pour calculer l’évaluation moyenne et la projeter sur un type anonyme, puis en utilisant ExecuteUpdate sur cela :

context.Blogs
    .Select(b => new { Blog = b, NewRating = b.Posts.Average(p => p.Rating) })
    .ExecuteUpdate(setters => setters.SetProperty(b => b.Blog.Rating, b => b.NewRating));

Cela exécute le code SQL suivant :

UPDATE [b]
SET [b].[Rating] = CAST((
    SELECT AVG(CAST([p].[Rating] AS float))
    FROM [Post] AS [p]
    WHERE [b].[Id] = [p].[BlogId]) AS int)
FROM [Blogs] AS [b]

Suivi des modifications

Les utilisateurs familiarisés avec SaveChanges sont habitués à effectuer plusieurs changements, puis à appeler SaveChanges pour appliquer toutes ces modifications à la base de données. Cela est rendu possible par le suivi des modifications de EF qui accumule, ou qui suit ces modifications.

ExecuteUpdate et ExecuteDelete fonctionnent différemment : ils prennent effet immédiatement, à compter du moment où ils sont appelés. Cela signifie que si une seule opération ExecuteUpdate ou ExecuteDelete peut affecter de nombreuses lignes, il n’est pas possible de cumuler plusieurs opérations de ce type et de les appliquer en même temps, par exemple lors de l’appel SaveChanges. En fait, ces fonctions ignorent complètement le suivi des modifications de EF et n’ont aucune interaction avec elle. Cela entraîne plusieurs conséquences importantes.

Prenez le code suivant :

// 1. Query the blog with the name `SomeBlog`. Since EF queries are tracking by default, the Blog is now tracked by EF's change tracker.
var blog = context.Blogs.Single(b => b.Name == "SomeBlog");

// 2. Increase the rating of all blogs in the database by one. This executes immediately.
context.Blogs.ExecuteUpdate(setters => setters.SetProperty(b => b.Rating, b => b.Rating + 1));

// 3. Increase the rating of `SomeBlog` by two. This modifies the .NET `Rating` property and is not yet persisted to the database.
blog.Rating += 2;

// 4. Persist tracked changes to the database.
context.SaveChanges();

Essentiellement, lorsque l’appel ExecuteUpdate est effectué et que tous les blogs sont mis à jour dans la base de données, le suivi des modifications de EF n’est pas mis à jour et l’instance suivie .NET a toujours sa valeur d’évaluation d’origine, c’est à dire celle qu’elle avait au moment où elle a été interrogée. Supposons que l’évaluation du blog était initialement 5 ; après l’exécution de la 3e ligne, l’évaluation dans la base de données est maintenant 6 (en raison du ExecuteUpdate), tandis que l’évaluation dans l’instance .NET suivie est 7. Lorsque la commande SaveChanges est appelée, EF détecte que la nouvelle valeur 7 est différente de la valeur d’origine 5 et conserve cette modification. La modification effectuée avec ExecuteUpdate est remplacée et n’est pas prise en compte.

Par conséquent, il est généralement judicieux d’éviter de mélanger les modifications suivies SaveChanges et les modifications non suivies via ExecuteUpdate/ExecuteDelete.

Transactions

Pour continuer avec les points évoqués ci-dessus, il est important de comprendre que ExecuteUpdate et ExecuteDelete ne lancent pas implicitement une transaction qu’elles ont appelée. Prenez le code suivant :

context.Blogs.ExecuteUpdate(/* some update */);
context.Blogs.ExecuteUpdate(/* another update */);

var blog = context.Blogs.Single(b => b.Name == "SomeBlog");
blog.Rating += 2;
context.SaveChanges();

Chaque appel ExecuteUpdate entraîne l’envoi d’une seule commande SQL UPDATE à la base de données. Étant donné qu’aucune transaction n’est créée, si une quelconque défaillance empêche le second ExecuteUpdate de se terminer correctement, les effets du premier sont toujours conservés dans la base de données. En fait, chacune des quatre opérations ci-dessus : les deux appels de ExecuteUpdate, une requête et SaveChanges, s’exécute dans sa propre transaction. Pour encapsuler plusieurs opérations dans une seule transaction, vous devez démarrer explicitement une transaction avec DatabaseFacade :

using (var transaction = context.Database.BeginTransaction())
{
    context.Blogs.ExecuteUpdate(/* some update */);
    context.Blogs.ExecuteUpdate(/* another update */);

    ...
}

Pour plus d’informations sur la gestion des transactions, consultez la rubrique Utilisation des transactions.

Contrôle d’accès concurrentiel et lignes affectées

SaveChanges fournit automatiquement un jeton d'authentification pour le contrôle d’accès concurrentiel afin de vous assurer qu’aucune ligne n’a été modifiée entre le moment où vous l’avez chargée et le moment où vous enregistrez les modifications. Comme ExecuteUpdate et ExecuteDelete n’interagissent pas avec le suivi des modifications, ils ne peuvent pas appliquer automatiquement le contrôle d’accès concurrentiel.

Toutefois, ces deux méthodes retournent le nombre de lignes affectées par l’opération ; ce qui peut être particulièrement utile pour implémenter vous-même le contrôle d’accès concurrentiel :

// (load the ID and concurrency token for a Blog in the database)

var numUpdated = context.Blogs
    .Where(b => b.Id == id && b.ConcurrencyToken == concurrencyToken)
    .ExecuteUpdate(/* ... */);
if (numUpdated == 0)
{
    throw new Exception("Update failed!");
}

Dans ce code, nous utilisons un opérateur LINQ Where pour appliquer une mise à jour à un blog spécifique, et seulement si son jeton d’accès concurrentiel a une valeur spécifique (par exemple, celle que nous avons vue lors de l’interrogation du blog à partir de la base de données). Nous vérifions ensuite le nombre de lignes réellement mises à jour par ExecuteUpdate ; si le résultat est égal à zéro, aucune ligne n’a été mise à jour et le jeton d’accès concurrentiel a probablement été modifié suite à une mise à jour simultanée.

Limites

  • Actuellement, seules la mise à jour et la suppression sont prises en charge. Pour l’insertion, vous devez utiliser DbSet<TEntity>.Add et SaveChanges().
  • Bien que les instructions SQL UPDATE et DELETE autorisent la récupération des valeurs d’origine d’une colonne pour les lignes affectées, cela n’est actuellement pas pris en charge par ExecuteUpdate et ExecuteDelete.
  • Les appels multiples de ces méthodes ne peuvent pas être traités par lots. Chaque appel effectue son propre circuit aller-retour vers la base de données.
  • Généralement, les bases de données autorisent la modification d’une seule table avec UPDATE ou DELETE.
  • Ces méthodes fonctionnent actuellement uniquement avec les fournisseurs de base de données relationnelle.

Ressources supplémentaires