Partager via


Change Tracking dans EF Core

Chaque instance DbContext suit les modifications apportées aux entités. Ces entités suivies à son tour entraînent les modifications apportées à la base de données lorsque SaveChanges est appelé.

Ce document présente une vue d’ensemble du suivi des modifications d’Entity Framework Core (EF Core) et de sa relation avec les requêtes et les mises à jour.

Conseil

Vous pouvez exécuter et déboguer dans tout le code de ce document en téléchargeant l’exemple de code à partir de GitHub.

Conseil

Pour plus de simplicité, ce document utilise et référence des méthodes synchrones telles que SaveChanges plutôt que leurs équivalents asynchrones tels que SaveChangesAsync. L’appel et l’attente de la méthode asynchrone peuvent être remplacés, sauf indication contraire.

Guide pratique pour suivre les entités

Les instances d’entité deviennent suivies lorsqu’elles sont :

  • Retourné à partir d’une requête exécutée sur la base de données
  • Attaché explicitement à dbContext par Add, Attach, Update, ou des méthodes similaires
  • Détecté en tant que nouvelles entités connectées à des entités suivies existantes

Les instances d’entité ne sont plus suivies lorsque :

  • DbContext est supprimé
  • Le suivi des modifications est effacé
  • Les entités sont explicitement détachées

DbContext est conçu pour représenter une unité de travail de courte durée, comme décrit dans Initialisation et configuration de DbContext. Cela signifie que la suppression de DbContext est le moyen normal d’arrêter les entités de suivi. En d’autres termes, la durée de vie d’un DbContext doit être :

  1. Créer l’instance DbContext
  2. Suivre certaines entités
  3. Apporter des modifications aux entités
  4. Appeler SaveChanges pour mettre à jour la base de données
  5. Supprimer l’instance DbContext

Conseil

Il n’est pas nécessaire d’effacer le suivi des modifications ou de détacher explicitement les instances d’entité lors de cette approche. Toutefois, si vous avez besoin de détacher des entités, l’appel ChangeTracker.Clear est plus efficace que de détacher des entités un par un.

États des entités

Chaque entité est associée à un élément donné EntityState :

  • les entités Detached ne sont pas suivies par le DbContext.
  • les entités Added sont nouvelles et n’ont pas encore été insérées dans la base de données. Cela signifie qu’ils seront insérés lorsque SaveChanges est appelé.
  • les entités Unchanged n’ont pas été modifiées depuis qu’elles ont été interrogées à partir de la base de données. Toutes les entités retournées à partir de requêtes sont initialement dans cet état.
  • les entités Modified ont été modifiées depuis qu’elles ont été interrogées à partir de la base de données. Cela signifie qu’ils seront mis à jour lorsque SaveChanges est appelé.
  • les entités Deleted existent dans la base de données, mais sont marquées pour être supprimées lorsque SaveChanges est appelé.

EF Core suit les modifications au niveau de la propriété. Par exemple, si une seule valeur de propriété est modifiée, une mise à jour de base de données change uniquement cette valeur. Toutefois, les propriétés peuvent uniquement être marquées comme modifiées lorsque l’entité elle-même est dans l’état Modifié. (Ou, d’un autre point de vue, l’état Modifié signifie qu’au moins une valeur de propriété a été marquée comme modifiée.)

Le tableau suivant en résume les différents états :

État de l’entité Suivi par DbContext Existe dans la base de données Propriétés modifiées Action sur SaveChanges
Detached Non - - -
Added Oui Non - Insérer
Unchanged Oui Oui No -
Modified Oui Oui Oui Mettre à jour
Deleted Oui Oui - Supprimer

Remarque

Ce texte utilise des termes de base de données relationnelles pour la clarté. Les bases de données NoSQL prennent généralement en charge des opérations similaires, mais éventuellement avec des noms différents. Pour plus d’informations, consultez la documentation de votre fournisseur de base de données.

Suivi à partir de requêtes

Le suivi des modifications EF Core fonctionne le mieux lorsque la même instance DbContext est utilisée pour rechercher des entités et les mettre à jour en appelant SaveChanges. Cela est dû au fait que EF Core effectue automatiquement le suivi de l’état des entités interrogées, puis détecte les modifications apportées à ces entités lorsque SaveChanges est appelé.

Cette approche présente plusieurs avantages sur les instances d’entité de suivi explicite :

  • Elle est simple. Les états d’entité doivent rarement être manipulés explicitement--EF Core s’occupe des changements d’état.
  • Les mises à jour sont limités uniquement aux valeurs qui ont réellement changé.
  • Les valeurs des propriétés d’ombre sont conservées et utilisées selon les besoins. Cela est particulièrement pertinent lorsque les clés étrangères sont stockées dans l’état d’ombre.
  • Les valeurs d’origine des propriétés sont conservées automatiquement et utilisées pour des mises à jour efficaces.

Requête et mise à jour simples

Par exemple, considérez un modèle de blog/billet de blog simple :

public class Blog
{
    public int Id { get; set; }
    public string Name { get; set; }

    public IList<Post> Posts { get; } = new List<Post>();
}

public class Post
{
    public int Id { get; set; }
    public string Title { get; set; }
    public string Content { get; set; }

    public int? BlogId { get; set; }
    public Blog Blog { get; set; }
}

Nous pouvons utiliser ce modèle pour interroger des blogs et des publications, puis apporter des mises à jour à la base de données :

using var context = new BlogsContext();

var blog = context.Blogs.Include(e => e.Posts).First(e => e.Name == ".NET Blog");

blog.Name = ".NET Blog (Updated!)";

foreach (var post in blog.Posts.Where(e => !e.Title.Contains("5.0")))
{
    post.Title = post.Title.Replace("5", "5.0");
}

context.SaveChanges();

L’appel de SaveChanges entraîne les mises à jour de base de données suivantes, à l’aide de SQLite comme exemple de base de données :

-- Executed DbCommand (0ms) [Parameters=[@p1='1' (DbType = String), @p0='.NET Blog (Updated!)' (Size = 20)], CommandType='Text', CommandTimeout='30']
UPDATE "Blogs" SET "Name" = @p0
WHERE "Id" = @p1;
SELECT changes();

-- Executed DbCommand (0ms) [Parameters=[@p1='2' (DbType = String), @p0='Announcing F# 5.0' (Size = 17)], CommandType='Text', CommandTimeout='30']
UPDATE "Posts" SET "Title" = @p0
WHERE "Id" = @p1;
SELECT changes();

La vue de débogage du suivi des modifications est un excellent moyen de visualiser les entités qui sont suivies et ce que leurs états sont. Par exemple, insérez le code suivant dans l’exemple ci-dessus avant d’appeler SaveChanges :

context.ChangeTracker.DetectChanges();
Console.WriteLine(context.ChangeTracker.DebugView.LongView);

Voici la sortie générée :

Blog {Id: 1} Modified
  Id: 1 PK
  Name: '.NET Blog (Updated!)' Modified Originally '.NET Blog'
  Posts: [{Id: 1}, {Id: 2}, {Id: 3}]
Post {Id: 1} Unchanged
  Id: 1 PK
  BlogId: 1 FK
  Content: 'Announcing the release of EF Core 5.0, a full featured cross...'
  Title: 'Announcing the Release of EF Core 5.0'
  Blog: {Id: 1}
Post {Id: 2} Modified
  Id: 2 PK
  BlogId: 1 FK
  Content: 'F# 5 is the latest version of F#, the functional programming...'
  Title: 'Announcing F# 5.0' Modified Originally 'Announcing F# 5'
  Blog: {Id: 1}

Remarquez spécifiquement :

  • La propriété Blog.Name est marquée comme modifiée (Name: '.NET Blog (Updated!)' Modified Originally '.NET Blog'), ce qui entraîne l’état Modified pour le blog.
  • La propriété Post.Title du billet 2 est marquée comme modifiée (Title: 'Announcing F# 5.0' Modified Originally 'Announcing F# 5'), ce qui entraîne l’affichage dans l’état Modified.
  • Les autres valeurs de propriété du billet 2 n’ont pas changé et ne sont donc pas marquées comme modifiées. C’est pourquoi ces valeurs ne sont pas incluses dans la mise à jour de la base de données.
  • L’autre billet n’a pas été modifié de quelque manière que ce soit. C’est pourquoi il est toujours dans l’état Unchanged et n’est pas inclus dans la mise à jour de la base de données.

Requête puis insertion, mise à jour et suppression

Mises à jour comme ceux de l’exemple précédent peuvent être combinés avec des insertions et des suppressions dans la même unité de travail. Par exemple :

using var context = new BlogsContext();

var blog = context.Blogs.Include(e => e.Posts).First(e => e.Name == ".NET Blog");

// Modify property values
blog.Name = ".NET Blog (Updated!)";

// Insert a new Post
blog.Posts.Add(
    new Post
    {
        Title = "What’s next for System.Text.Json?", Content = ".NET 5.0 was released recently and has come with many..."
    });

// Mark an existing Post as Deleted
var postToDelete = blog.Posts.Single(e => e.Title == "Announcing F# 5");
context.Remove(postToDelete);

context.ChangeTracker.DetectChanges();
Console.WriteLine(context.ChangeTracker.DebugView.LongView);

context.SaveChanges();

Dans cet exemple :

  • Un blog et des billets connexes sont interrogés à partir de la base de données et suivis
  • La propriété Blog.Name est modifiée
  • Un nouveau billet est ajouté à la collection de billets existants pour le blog
  • Un billet existant est marqué pour la suppression en appelant DbContext.Remove

En examinant à nouveau la vue de débogage du suivi des modifications avant d’appeler SaveChanges, indique comment EF Core effectue le suivi de ces modifications :

Blog {Id: 1} Modified
  Id: 1 PK
  Name: '.NET Blog (Updated!)' Modified Originally '.NET Blog'
  Posts: [{Id: 1}, {Id: 2}, {Id: 3}, {Id: -2147482638}]
Post {Id: -2147482638} Added
  Id: -2147482638 PK Temporary
  BlogId: 1 FK
  Content: '.NET 5.0 was released recently and has come with many...'
  Title: 'What's next for System.Text.Json?'
  Blog: {Id: 1}
Post {Id: 1} Unchanged
  Id: 1 PK
  BlogId: 1 FK
  Content: 'Announcing the release of EF Core 5.0, a full featured cross...'
  Title: 'Announcing the Release of EF Core 5.0'
  Blog: {Id: 1}
Post {Id: 2} Deleted
  Id: 2 PK
  BlogId: 1 FK
  Content: 'F# 5 is the latest version of F#, the functional programming...'
  Title: 'Announcing F# 5'
  Blog: {Id: 1}

Notez que :

  • Le blog est marqué comme Modified. Cela génère une mise à jour de base de données.
  • Le billet 2 est marqué comme Deleted. Cela génère une suppression de base de données.
  • Un nouveau billet avec un ID temporaire est associé au blog 1 et est marqué comme Added. Cela génère une insertion de base de données.

Cela entraîne l’appel des commandes de base de données suivantes (à l’aide de SQLite) lorsque SaveChanges est appelé :

-- Executed DbCommand (0ms) [Parameters=[@p1='1' (DbType = String), @p0='.NET Blog (Updated!)' (Size = 20)], CommandType='Text', CommandTimeout='30']
UPDATE "Blogs" SET "Name" = @p0
WHERE "Id" = @p1;
SELECT changes();

-- Executed DbCommand (0ms) [Parameters=[@p0='2' (DbType = String)], CommandType='Text', CommandTimeout='30']
DELETE FROM "Posts"
WHERE "Id" = @p0;
SELECT changes();

-- Executed DbCommand (0ms) [Parameters=[@p0='1' (DbType = String), @p1='.NET 5.0 was released recently and has come with many...' (Size = 56), @p2='What's next for System.Text.Json?' (Size = 33)], CommandType='Text', CommandTimeout='30']
INSERT INTO "Posts" ("BlogId", "Content", "Title")
VALUES (@p0, @p1, @p2);
SELECT "Id"
FROM "Posts"
WHERE changes() = 1 AND "rowid" = last_insert_rowid();

Pour plus d’informations sur l’insertion et la suppression d’entités, consultez Entités de suivi explicite. Pour plus d’informations sur la façon dont EF Core détecte automatiquement les modifications comme celle-ci, consultez Détection et notifications des modifications.

Conseil

Appelez ChangeTracker.HasChanges() pour déterminer si des modifications ont été apportées qui entraînent SaveChanges à apporter des mises à jour à la base de données. Si HasChanges retourne false, SaveChanges est un non-op.