Partager via


Entités de suivi explicites

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é.

Le suivi des modifications Entity Framework Core (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 est abordée dans Change Tracking dans EF Core.

Conseil

Ce document suppose que les états d’entité et les principes de base du suivi des modifications EF Core sont compris. Pour plus d’informations sur ces rubriques, consultez Suivi des modifications dans EF Core.

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.

Présentation

Les entités peuvent être explicitement « attachées » à un DbContext de manière à ce que le contexte suive ces entités. Ceci est principalement utile lors :

  1. de la création de nouvelles entités qui seront insérées dans la base de données.
  2. du rattachement d’entités déconnectées qui ont été interrogées précédemment par une instance DbContext différente.

La première d’entre elles est nécessaire pour la plupart des applications et est principalement gérée par les méthodes DbContext.Add.

La seconde n’est nécessaire que pour les applications qui modifient les entités ou leurs relations alors que les entités ne sont pas suivies. Par exemple, une application web peut envoyer des entités au client web où l’utilisateur apporte des modifications et renvoie les entités. Ces entités sont appelées « déconnectées », car elles ont été interrogées à l’origine à partir d’un DbContext, mais ont ensuite été déconnectées de ce contexte lorsqu’elles ont été envoyées au client.

L’application web doit maintenant rattacher ces entités afin qu’elles soient de nouveau suivies et indiquent les modifications apportées afin que SaveChanges puisse apporter des mises à jour appropriées à la base de données. Ceci est principalement géré par les méthodes DbContext.Attach et DbContext.Update.

Conseil

L’attachement d’entités à la même instance DbContext à partir de laquelle elles ont été interrogées ne doit pas normalement être nécessaire. N’effectuez pas régulièrement une requête sans suivi, puis attachez les entités retournées au même contexte. Cela sera plus lent que d’utiliser une requête de suivi et peut également entraîner des problèmes tels que l’absence de valeurs de propriété cachées, ce qui rend l’opération plus difficile à réaliser.

Valeurs de clé générées ou explicites

Par défaut, les propriétés de clé entière et GUID sont configurées pour utiliser des valeurs de clé générées automatiquement. Cela présente un avantage majeur pour le suivi des modifications : une valeur de clé non définie indique que l’entité est « nouvelle ». Par « nouvelle », nous entendons qu’elle n’a pas encore été insérée dans la base de données.

Deux modèles sont utilisés dans les sections suivantes. Le premier est configuré pour ne pas utiliser les valeurs de clé générées :

public class Blog
{
    [DatabaseGenerated(DatabaseGeneratedOption.None)]
    public int Id { get; set; }

    public string Name { get; set; }

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

public class Post
{
    [DatabaseGenerated(DatabaseGeneratedOption.None)]
    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; }
}

Les valeurs de clé non générées (c’est-à-dire définies explicitement) sont affichées en premier dans chaque exemple, car tout est très explicite et facile à suivre. Un exemple d’utilisation de valeurs de clés générées est ensuite présenté :

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; }
}

Notez que les propriétés de clé dans ce modèle n’ont pas besoin d’une configuration supplémentaire ici, car l’utilisation de valeurs de clé générées est la valeur par défaut pour les clés entières simples.

Insertion de nouvelles entités

Valeurs de clé explicites

Une entité doit être suivie dans l’état Added pour être insérée par SaveChanges. Les entités sont généralement placées dans l’état Added en appelant l’une des méthodes DbContext.Add, DbContext.AddRange, DbContext.AddAsync, DbContext.AddRangeAsync ou les méthodes équivalentes sur DbSet<TEntity>.

Conseil

Ces méthodes fonctionnent toutes de la même façon dans le contexte du suivi des modifications. Pour plus d’informations, consultez les Autres fonctionnalités de suivi des modifications.

Par exemple, pour commencer à suivre un nouveau blog :

context.Add(
    new Blog { Id = 1, Name = ".NET Blog", });

L’inspection de la vue de débogage du suivi des modifications suivant cet appel montre que le contexte suit la nouvelle entité dans l’état Added :

Blog {Id: 1} Added
  Id: 1 PK
  Name: '.NET Blog'
  Posts: []

Cependant, les méthodes Add ne font pas que fonctionner sur une entité individuelle. Elles commencent à suivre un graphique entier d’entités connexes, en les plaçant toutes dans l’état Added. Par exemple, pour insérer un nouveau blog et les nouvelles publications associées :

context.Add(
    new Blog
    {
        Id = 1,
        Name = ".NET Blog",
        Posts =
        {
            new Post
            {
                Id = 1,
                Title = "Announcing the Release of EF Core 5.0",
                Content = "Announcing the release of EF Core 5.0, a full featured cross-platform..."
            },
            new Post
            {
                Id = 2,
                Title = "Announcing F# 5",
                Content = "F# 5 is the latest version of F#, the functional programming language..."
            }
        }
    });

Le contexte suit désormais toutes ces entités comme Added :

Blog {Id: 1} Added
  Id: 1 PK
  Name: '.NET Blog'
  Posts: [{Id: 1}, {Id: 2}]
Post {Id: 1} Added
  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} Added
  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 les valeurs explicites ont été définies pour les propriétés de clé Id dans les exemples ci-dessus. Cela est dû au fait que le modèle a été configuré pour utiliser des valeurs de clés définies explicitement, plutôt que des valeurs de clé générées automatiquement. Lorsque vous n’utilisez pas de clés générées, les propriétés de clé doivent être définies explicitement avant d’appeler Add. Ces valeurs de clé sont ensuite insérées lorsque SaveChanges est appelé. Par exemple, lors de l’utilisation de SQLite :

-- Executed DbCommand (0ms) [Parameters=[@p0='1' (DbType = String), @p1='.NET Blog' (Size = 9)], CommandType='Text', CommandTimeout='30']
INSERT INTO "Blogs" ("Id", "Name")
VALUES (@p0, @p1);

-- Executed DbCommand (0ms) [Parameters=[@p2='1' (DbType = String), @p3='1' (DbType = String), @p4='Announcing the release of EF Core 5.0, a full featured cross-platform...' (Size = 72), @p5='Announcing the Release of EF Core 5.0' (Size = 37)], CommandType='Text', CommandTimeout='30']
INSERT INTO "Posts" ("Id", "BlogId", "Content", "Title")
VALUES (@p2, @p3, @p4, @p5);

-- Executed DbCommand (0ms) [Parameters=[@p0='2' (DbType = String), @p1='1' (DbType = String), @p2='F# 5 is the latest version of F#, the functional programming language...' (Size = 72), @p3='Announcing F# 5' (Size = 15)], CommandType='Text', CommandTimeout='30']
INSERT INTO "Posts" ("Id", "BlogId", "Content", "Title")
VALUES (@p0, @p1, @p2, @p3);

Toutes ces entités sont suivies dans l’état Unchanged une fois SaveChanges terminé, car ces entités existent désormais dans la base de données :

Blog {Id: 1} Unchanged
  Id: 1 PK
  Name: '.NET Blog'
  Posts: [{Id: 1}, {Id: 2}]
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} Unchanged
  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}

Valeurs de clé générées

Comme mentionné ci-dessus, les propriétés de clé entière et GUID sont configurées pour utiliser des valeurs de clé générées automatiquement par défaut. Cela signifie que l’application ne doit pas définir explicitement de valeur de clé. Par exemple, pour insérer de nouveaux blog et publications avec des valeurs clés générées :

context.Add(
    new Blog
    {
        Name = ".NET Blog",
        Posts =
        {
            new Post
            {
                Title = "Announcing the Release of EF Core 5.0",
                Content = "Announcing the release of EF Core 5.0, a full featured cross-platform..."
            },
            new Post
            {
                Title = "Announcing F# 5",
                Content = "F# 5 is the latest version of F#, the functional programming language..."
            }
        }
    });

Comme avec les valeurs de clé explicites, le contexte suit désormais toutes ces entités comme Added :

Blog {Id: -2147482644} Added
  Id: -2147482644 PK Temporary
  Name: '.NET Blog'
  Posts: [{Id: -2147482637}, {Id: -2147482636}]
Post {Id: -2147482637} Added
  Id: -2147482637 PK Temporary
  BlogId: -2147482644 FK Temporary
  Content: 'Announcing the release of EF Core 5.0, a full featured cross...'
  Title: 'Announcing the Release of EF Core 5.0'
  Blog: {Id: -2147482644}
Post {Id: -2147482636} Added
  Id: -2147482636 PK Temporary
  BlogId: -2147482644 FK Temporary
  Content: 'F# 5 is the latest version of F#, the functional programming...'
  Title: 'Announcing F# 5'
  Blog: {Id: -2147482644}

Notez dans ce cas que les valeurs de clé temporaires ont été générées pour chaque entité. Ces valeurs sont utilisées par EF Core jusqu’à ce que SaveChanges soit appelé, moment où les valeurs réelles des clés sont relues à partir de la base de données. Par exemple, lors de l’utilisation de SQLite :

-- Executed DbCommand (0ms) [Parameters=[@p0='.NET Blog' (Size = 9)], CommandType='Text', CommandTimeout='30']
INSERT INTO "Blogs" ("Name")
VALUES (@p0);
SELECT "Id"
FROM "Blogs"
WHERE changes() = 1 AND "rowid" = last_insert_rowid();

-- Executed DbCommand (0ms) [Parameters=[@p1='1' (DbType = String), @p2='Announcing the release of EF Core 5.0, a full featured cross-platform...' (Size = 72), @p3='Announcing the Release of EF Core 5.0' (Size = 37)], CommandType='Text', CommandTimeout='30']
INSERT INTO "Posts" ("BlogId", "Content", "Title")
VALUES (@p1, @p2, @p3);
SELECT "Id"
FROM "Posts"
WHERE changes() = 1 AND "rowid" = last_insert_rowid();

-- Executed DbCommand (0ms) [Parameters=[@p0='1' (DbType = String), @p1='F# 5 is the latest version of F#, the functional programming language...' (Size = 72), @p2='Announcing F# 5' (Size = 15)], 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();

Une fois SaveChanges terminé, toutes les entités ont été mises à jour avec leurs valeurs de clé réelles et sont suivies dans l’état Unchanged, car elles correspondent désormais à l’état dans la base de données :

Blog {Id: 1} Unchanged
  Id: 1 PK
  Name: '.NET Blog'
  Posts: [{Id: 1}, {Id: 2}]
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} Unchanged
  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}

Il s’agit exactement du même état final que l’exemple précédent qui a utilisé des valeurs de clé explicites.

Conseil

Une valeur de clé explicite peut toujours être définie même lors de l’utilisation de valeurs de clé générées. EF Core tente ensuite d’insérer à l’aide de cette valeur de clé. Certaines configurations de base de données, y compris SQL Server avec des colonnes d’identité, ne prennent pas en charge ces insertions et lèveront (consultez ces documents pour obtenir une solution de contournement).

Attachement d’entités existantes

Valeurs de clé explicites

Les entités retournées à partir de requêtes sont suivies dans l’état Unchanged. L’état Unchanged signifie que l’entité n’a pas été modifiée depuis son interrogation. Une entité déconnectée, peut-être retournée à partir d’un client web dans une requête HTTP, peut être placée dans cet état à l’aide de DbContext.Attach, DbContext.AttachRange, ou des méthodes équivalentes sur DbSet<TEntity>. Par exemple, pour commencer à suivre un blog existant :

context.Attach(
    new Blog { Id = 1, Name = ".NET Blog", });

Remarque

Les exemples présentés ici créent des entités explicitement avec new par souci de simplicité. Normalement, les instances d’entité proviennent d’une autre source, comme la désérialisation à partir d’un client ou la création à partir de données dans une publication HTTP.

L’inspection de la vue de débogage du suivi des modifications suivant cet appel montre que l’entité est suivie dans l’état Unchanged :

Blog {Id: 1} Unchanged
  Id: 1 PK
  Name: '.NET Blog'
  Posts: []

Tout comme Add, Attach définit en fait un graphique entier d’entités connectées dans l’état Unchanged. Par exemple, pour joindre un blog existant et des publications existantes associées :

context.Attach(
    new Blog
    {
        Id = 1,
        Name = ".NET Blog",
        Posts =
        {
            new Post
            {
                Id = 1,
                Title = "Announcing the Release of EF Core 5.0",
                Content = "Announcing the release of EF Core 5.0, a full featured cross-platform..."
            },
            new Post
            {
                Id = 2,
                Title = "Announcing F# 5",
                Content = "F# 5 is the latest version of F#, the functional programming language..."
            }
        }
    });

Le contexte suit désormais toutes ces entités comme Unchanged :

Blog {Id: 1} Unchanged
  Id: 1 PK
  Name: '.NET Blog'
  Posts: [{Id: 1}, {Id: 2}]
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} Unchanged
  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}

Appeler SaveChanges n’aura aucun effet à ce stade. Toutes les entités sont marquées comme Unchanged : il n’y a donc rien à mettre à jour dans la base de données.

Valeurs de clé générées

Comme mentionné ci-dessus, les propriétés de clé entière et GUID sont configurées pour utiliser des valeurs de clé générées automatiquement par défaut. Cela présente un avantage majeur lors de l’utilisation d’entités déconnectées : une valeur de clé non définie indique que l’entité n’a pas encore été insérée dans la base de données. Cela permet au suivi des modifications de détecter automatiquement les nouvelles entités et de les placer dans l’état Added. Par exemple, envisagez d’attacher ce graphique d’un blog et des publications :

context.Attach(
    new Blog
    {
        Id = 1,
        Name = ".NET Blog",
        Posts =
        {
            new Post
            {
                Id = 1,
                Title = "Announcing the Release of EF Core 5.0",
                Content = "Announcing the release of EF Core 5.0, a full featured cross-platform..."
            },
            new Post
            {
                Id = 2,
                Title = "Announcing F# 5",
                Content = "F# 5 is the latest version of F#, the functional programming language..."
            },
            new Post
            {
                Title = "Announcing .NET 5.0",
                Content = ".NET 5.0 includes many enhancements, including single file applications, more..."
            },
        }
    });

Le blog a une valeur clé de 1, indiquant qu’il existe déjà dans la base de données. Deux des publications ont également des valeurs clés définies, mais pas la troisième. EF Core voit cette valeur de clé comme 0, la valeur CLR par défaut pour un entier. Cela entraîne le marquage d’EF Core de la nouvelle entité comme Added lieu de Unchanged :

Blog {Id: 1} Unchanged
  Id: 1 PK
  Name: '.NET Blog'
  Posts: [{Id: 1}, {Id: 2}, {Id: -2147482636}]
Post {Id: -2147482636} Added
  Id: -2147482636 PK Temporary
  BlogId: 1 FK
  Content: '.NET 5.0 includes many enhancements, including single file a...'
  Title: 'Announcing .NET 5.0'
  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} Unchanged
  Id: 2 PK
  BlogId: 1 FK
  Content: 'F# 5 is the latest version of F#, the functional programming...'

Appeler SaveChanges à ce stade est sans effet sur les entités Unchanged, mais il insère la nouvelle entité dans la base de données. Par exemple, lors de l’utilisation de SQLite :

-- Executed DbCommand (0ms) [Parameters=[@p0='1' (DbType = String), @p1='.NET 5.0 includes many enhancements, including single file applications, more...' (Size = 80), @p2='Announcing .NET 5.0' (Size = 19)], 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();

Le point important à noter ici est que, avec des valeurs de clé générées, EF Core est en mesure de distinguer automatiquement les nouvelles entités des entités existantes dans un graphique déconnecté.. En un mot, lorsque vous utilisez des clés générées, EF Core insère toujours une entité lorsque cette entité n’a pas de valeur de clé définie.

Mise à jour des entités existantes

Valeurs de clé explicites

DbContext.Update, DbContext.UpdateRange et les méthodes équivalentes sur DbSet<TEntity> se comportent exactement comme les méthodes Attach décrites ci-dessus, excepté que les entités sont placées dans l’état Modified au lieu de l’état Unchanged. Par exemple, pour commencer à suivre un blog existant en tant que Modified :

context.Update(
    new Blog { Id = 1, Name = ".NET Blog", });

L’inspection de la vue de débogage du suivi des modifications suivant cet appel montre que le contexte suit cette entité dans l’état Modified :

Blog {Id: 1} Modified
  Id: 1 PK
  Name: '.NET Blog' Modified
  Posts: []

Tout comme avec Add et Attach, Update marque en fait un graphique entier d’entités connexes comme Modified. Par exemple, pour joindre un blog existant et des publications existantes associées comme Modified :

context.Update(
    new Blog
    {
        Id = 1,
        Name = ".NET Blog",
        Posts =
        {
            new Post
            {
                Id = 1,
                Title = "Announcing the Release of EF Core 5.0",
                Content = "Announcing the release of EF Core 5.0, a full featured cross-platform..."
            },
            new Post
            {
                Id = 2,
                Title = "Announcing F# 5",
                Content = "F# 5 is the latest version of F#, the functional programming language..."
            }
        }
    });

Le contexte suit désormais toutes ces entités comme Modified :

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

Appeler SaveChanges à ce stade entraîne l’envoi des mises à jour à la base de données pour toutes ces entités. Par exemple, lors de l’utilisation de SQLite :

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

-- Executed DbCommand (0ms) [Parameters=[@p3='1' (DbType = String), @p0='1' (DbType = String), @p1='Announcing the release of EF Core 5.0, a full featured cross-platform...' (Size = 72), @p2='Announcing the Release of EF Core 5.0' (Size = 37)], CommandType='Text', CommandTimeout='30']
UPDATE "Posts" SET "BlogId" = @p0, "Content" = @p1, "Title" = @p2
WHERE "Id" = @p3;
SELECT changes();

-- Executed DbCommand (0ms) [Parameters=[@p3='2' (DbType = String), @p0='1' (DbType = String), @p1='F# 5 is the latest version of F#, the functional programming language...' (Size = 72), @p2='Announcing F# 5' (Size = 15)], CommandType='Text', CommandTimeout='30']
UPDATE "Posts" SET "BlogId" = @p0, "Content" = @p1, "Title" = @p2
WHERE "Id" = @p3;
SELECT changes();

Valeurs de clé générées

Comme avec Attach, les valeurs de clé générées présentent le même avantage majeur pour Update : une valeur de clé non définie indique que l’entité est nouvelle et n’a pas encore été insérée dans la base de données. Comme avec Attach, cela permet au DbContext de détecter automatiquement les nouvelles entités et de les placer dans l’état Added. Par exemple, envisagez d’appeler Update avec ce graphique d’un blog et de publications :

context.Update(
    new Blog
    {
        Id = 1,
        Name = ".NET Blog",
        Posts =
        {
            new Post
            {
                Id = 1,
                Title = "Announcing the Release of EF Core 5.0",
                Content = "Announcing the release of EF Core 5.0, a full featured cross-platform..."
            },
            new Post
            {
                Id = 2,
                Title = "Announcing F# 5",
                Content = "F# 5 is the latest version of F#, the functional programming language..."
            },
            new Post
            {
                Title = "Announcing .NET 5.0",
                Content = ".NET 5.0 includes many enhancements, including single file applications, more..."
            },
        }
    });

Comme dans l’exemple Attach, la publication sans valeur de clé est détectée comme nouvelle et définie sur l’état Added. Les autres entités sont marquées comme Modified :

Blog {Id: 1} Modified
  Id: 1 PK
  Name: '.NET Blog' Modified
  Posts: [{Id: 1}, {Id: 2}, {Id: -2147482633}]
Post {Id: -2147482633} Added
  Id: -2147482633 PK Temporary
  BlogId: 1 FK
  Content: '.NET 5.0 includes many enhancements, including single file a...'
  Title: 'Announcing .NET 5.0'
  Blog: {Id: 1}
Post {Id: 1} Modified
  Id: 1 PK
  BlogId: 1 FK Modified Originally <null>
  Content: 'Announcing the release of EF Core 5.0, a full featured cross...' Modified
  Title: 'Announcing the Release of EF Core 5.0' Modified
  Blog: {Id: 1}
Post {Id: 2} Modified
  Id: 2 PK
  BlogId: 1 FK Modified Originally <null>
  Content: 'F# 5 is the latest version of F#, the functional programming...' Modified
  Title: 'Announcing F# 5' Modified
  Blog: {Id: 1}

Appeler SaveChanges à ce stade entraîne l’envoi des mises à jour à la base de données pour toutes les entités existantes, tandis que la nouvelle entité est insérée. Par exemple, lors de l’utilisation de SQLite :

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

-- Executed DbCommand (0ms) [Parameters=[@p3='1' (DbType = String), @p0='1' (DbType = String), @p1='Announcing the release of EF Core 5.0, a full featured cross-platform...' (Size = 72), @p2='Announcing the Release of EF Core 5.0' (Size = 37)], CommandType='Text', CommandTimeout='30']
UPDATE "Posts" SET "BlogId" = @p0, "Content" = @p1, "Title" = @p2
WHERE "Id" = @p3;
SELECT changes();

-- Executed DbCommand (0ms) [Parameters=[@p3='2' (DbType = String), @p0='1' (DbType = String), @p1='F# 5 is the latest version of F#, the functional programming language...' (Size = 72), @p2='Announcing F# 5' (Size = 15)], CommandType='Text', CommandTimeout='30']
UPDATE "Posts" SET "BlogId" = @p0, "Content" = @p1, "Title" = @p2
WHERE "Id" = @p3;
SELECT changes();

-- Executed DbCommand (0ms) [Parameters=[@p0='1' (DbType = String), @p1='.NET 5.0 includes many enhancements, including single file applications, more...' (Size = 80), @p2='Announcing .NET 5.0' (Size = 19)], 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();

Il s’agit d’un moyen très simple de générer des mises à jour et des insertions à partir d’un graphique déconnecté. Toutefois, elle entraîne l’envoi de mises à jour ou d’insertions à la base de données pour chaque propriété de chaque entité suivie, même si certaines valeurs de propriété n’ont pas été modifiées. Ne soyez pas trop intimidés par cela : cela peut être une manière simple et pragmatique de générer des mises à jour pour de nombreuses applications avec de petits graphiques. Cela dit, d’autres modèles plus complexes peuvent parfois entraîner des mises à jour plus efficaces, comme décrit dans La résolution des identités dans EF Core.

Suppression d’entités existantes

Pour qu’une entité soit supprimée par SaveChanges, elle doit être suivie dans l’état Deleted. Les entités sont généralement placées dans l’état Deleted en appelant l’une des méthodes DbContext.Remove, DbContext.RemoveRange, ou les méthodes équivalentes sur DbSet<TEntity>. Par exemple, pour marquer une publication existante comme Deleted :

context.Remove(
    new Post { Id = 2 });

L’inspection de la vue de débogage du suivi des modifications suivant cet appel montre que le contexte suit l’entité dans l’état Deleted :

Post {Id: 2} Deleted
  Id: 2 PK
  BlogId: <null> FK
  Content: <null>
  Title: <null>
  Blog: <null>

Cette entité est supprimée lorsque SaveChanges est appelé. Par exemple, lors de l’utilisation de SQLite :

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

Une fois SaveChanges terminé, l’entité supprimée est détachée du DbContext, car elle n’existe plus dans la base de données. La vue de débogage est donc vide, car aucune entité n’est suivie.

Suppression d’entités dépendantes/enfants

Supprimer des entités dépendantes/enfants d’un graphique est plus simple que de supprimer des entités principales/parentes. Pour plus d’informations, consultez la section suivante et Modification des clés étrangères et des navigations.

Il est inhabituel d’appeler Remove sur une entité créée avec new. En outre, contrairement à Add, Attach et Update, il est rare d’appeler Remove sur une entité qui n’est pas déjà suivie dans l’état Unchanged ou Modified. Au lieu de cela, il est courant de suivre une entité unique ou un graphique d’entités connexes, puis d’appeler Remove sur les entités qui doivent être supprimées. Ce graphique d’entités suivies est généralement créé par :

  1. l’exécution d’une requête pour les entités, ou
  2. l’utilisation des méthodes Attach ou Update sur un graphique d’entités déconnectées, comme décrit dans les sections précédentes.

Par exemple, le code de la section précédente est plus susceptible d’obtenir une publication d’un client, puis de faire quelque chose comme :

context.Attach(post);
context.Remove(post);

Le comportement est exactement le même que dans l’exemple précédent, car appeler Remove sur une entité non suivie l’amène d’abord à être attachée, puis marquée comme Deleted.

Dans des exemples plus réalistes, un graphique d’entités est d’abord attaché, puis certaines de ces entités sont marquées comme supprimées. Par exemple :

// Attach a blog and associated posts
context.Attach(blog);

// Mark one post as Deleted
context.Remove(blog.Posts[1]);

Toutes les entités sont marquées comme Unchanged, à l’exception de celle sur laquelle Remove a été appelé :

Blog {Id: 1} Unchanged
  Id: 1 PK
  Name: '.NET Blog'
  Posts: [{Id: 1}, {Id: 2}]
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}

Cette entité est supprimée lorsque SaveChanges est appelé. Par exemple, lors de l’utilisation de SQLite :

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

Une fois SaveChanges terminé, l’entité supprimée est détachée du DbContext, car elle n’existe plus dans la base de données. Les autres entités restent dans l’état Unchanged :

Blog {Id: 1} Unchanged
  Id: 1 PK
  Name: '.NET Blog'
  Posts: [{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}

Suppression d’entités principales/parentes

Chaque relation qui connecte deux types d’entités a une terminaison parente ou principale et une terminaison dépendante ou enfant. L’entité dépendante/enfant est celle avec la propriété de clé étrangère. Dans une relation un-à-plusieurs, la principale/parente se trouve du côté « un » et la dépendante/enfant est du côté « plusieurs ». Pour plus d’informations, consultez Relations.

Dans les exemples précédents, nous avons supprimé une publication, qui est une entité dépendante/enfant dans la relation un-à-plusieurs blog-publications. Cela est relativement simple, car la suppression d’une entité dépendante/enfant n’a aucun impact sur d’autres entités. En revanche, la suppression d’une entité principale/parente doit également avoir un impact sur les entités dépendantes/enfants. Dans le cas contraire, une valeur de clé étrangère ferait référence à une valeur de clé primaire qui n’existe plus. Il s’agit d’un état de modèle non valide et génère une erreur de contrainte référentielle dans la plupart des bases de données.

Cet état de modèle non valide peut être géré de deux façons :

  1. en définissant les valeurs FK sur Null. Cela indique que les dépendantes/enfants ne sont plus associées à aucune principale/parente. Il s’agit de la valeur par défaut pour les relations facultatives où la clé étrangère doit pouvoir accepter la valeur Null. Définir la clé FK sur Null n’est pas valide pour les relations requises, où la clé étrangère est généralement non-nullable.
  2. Suppression des dépendantes/enfants. Il s’agit de la valeur par défaut pour les relations requises et est également valide pour les relations facultatives.

Pour plus d’informations sur le suivi des modifications et les relations, consultez la section Modification des clés étrangères et des navigations.

Relations facultatives

La propriété de clé étrangère Post.BlogId peut accepter la valeur Null dans le modèle que nous utilisons. Cela signifie que la relation est facultative et, par conséquent, le comportement par défaut d’EF Core consiste à définir les propriétés de clé étrangère BlogId sur Null lorsque le blog est supprimé. Par exemple :

// Attach a blog and associated posts
context.Attach(blog);

// Mark the blog as deleted
context.Remove(blog);

L’inspection de la vue de débogage du suivi des modifications suivant l’appel à Remove montre que, comme prévu, le blog est désormais marqué comme Deleted :

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

Plus intéressant encore, toutes les publications associées sont maintenant marquées comme Modified. Cela est dû au fait que la propriété de clé étrangère de chaque entité a été définie sur Null. Appeler SaveChanges met à jour la valeur de clé étrangère pour chaque publication sur Null dans la base de données, avant de supprimer le blog :

-- Executed DbCommand (0ms) [Parameters=[@p1='1' (DbType = String), @p0=NULL], CommandType='Text', CommandTimeout='30']
UPDATE "Posts" SET "BlogId" = @p0
WHERE "Id" = @p1;
SELECT changes();

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

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

Une fois SaveChanges terminé, l’entité supprimée est détachée du DbContext, car elle n’existe plus dans la base de données. D’autres entités sont désormais marquées comme Unchanged avec des valeurs de clé étrangère Null, qui correspondent à l’état de la base de données :

Post {Id: 1} Unchanged
  Id: 1 PK
  BlogId: <null> FK
  Content: 'Announcing the release of EF Core 5.0, a full featured cross...'
  Title: 'Announcing the Release of EF Core 5.0'
  Blog: <null>
Post {Id: 2} Unchanged
  Id: 2 PK
  BlogId: <null> FK
  Content: 'F# 5 is the latest version of F#, the functional programming...'
  Title: 'Announcing F# 5'
  Blog: <null>

Relations requises

Si la propriété de clé étrangère Post.BlogId est non-nullable, la relation entre les blogs et les publications devient « obligatoire ». Dans ce cas, EF Core supprime par défaut les entités dépendantes/enfants lorsque la principale/parente est supprimée. Par exemple, supprimer un blog avec des publications associées comme dans l’exemple précédent :

// Attach a blog and associated posts
context.Attach(blog);

// Mark the blog as deleted
context.Remove(blog);

L’inspection de la vue de débogage du suivi des modifications suivant l’appel à Remove montre que, comme prévu, le blog est à nouveau marqué comme Deleted :

Blog {Id: 1} Deleted
  Id: 1 PK
  Name: '.NET Blog'
  Posts: [{Id: 1}, {Id: 2}]
Post {Id: 1} Deleted
  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}

Ce qui est plus intéressant dans ce cas, c’est que toutes les publications associées ont également été marquées comme Deleted. Appeler SaveChanges entraîne la suppression du blog et de toutes les publications associées de la base de données :

-- Executed DbCommand (0ms) [Parameters=[@p0='1' (DbType = String)], CommandType='Text', CommandTimeout='30']
DELETE FROM "Posts"
WHERE "Id" = @p0;
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=[@p1='1' (DbType = String)], CommandType='Text', CommandTimeout='30']
DELETE FROM "Blogs"
WHERE "Id" = @p1;

Une fois SaveChanges terminé, toutes les entités supprimées sont détachées du DbContext, car elles n’existent plus dans la base de données. La sortie de la vue de débogage est donc vide.

Remarque

Ce document ne fait qu’effleurer la question du travail avec les relations dans EF Core. Consultez Relations pour plus d’informations sur la modélisation des relations et Modification des clés étrangères et des navigations pour plus d’informations sur la mise à jour/suppression d’entités dépendantes/enfants lors de l’appel de SaveChanges.

Suivi personnalisé avec TrackGraph

ChangeTracker.TrackGraph fonctionne comme Add, Attach et Update, sauf qu’il génère un rappel pour chaque instance d’entité avant de le suivre. Cela permet l’utilisation d’une logique personnalisée pour déterminer comment suivre les entités individuelles dans un graphique.

Par exemple, considérez la règle utilisée par EF Core lors du suivi des entités avec des valeurs de clé générées : si la valeur de clé est égale à zéro, l’entité est nouvelle et doit être insérée. Étendons cette règle pour dire que si la valeur de clé est négative, l’entité doit être supprimée. Cela nous permet de modifier les valeurs de clé primaire dans les entités d’un graphique déconnecté pour marquer les entités supprimées :

blog.Posts.Add(
    new Post
    {
        Title = "Announcing .NET 5.0",
        Content = ".NET 5.0 includes many enhancements, including single file applications, more..."
    }
);

var toDelete = blog.Posts.Single(e => e.Title == "Announcing F# 5");
toDelete.Id = -toDelete.Id;

Ce graphique déconnecté peut ensuite être suivi à l’aide de TrackGraph :

public static void UpdateBlog(Blog blog)
{
    using var context = new BlogsContext();

    context.ChangeTracker.TrackGraph(
        blog, node =>
        {
            var propertyEntry = node.Entry.Property("Id");
            var keyValue = (int)propertyEntry.CurrentValue;

            if (keyValue == 0)
            {
                node.Entry.State = EntityState.Added;
            }
            else if (keyValue < 0)
            {
                propertyEntry.CurrentValue = -keyValue;
                node.Entry.State = EntityState.Deleted;
            }
            else
            {
                node.Entry.State = EntityState.Modified;
            }

            Console.WriteLine($"Tracking {node.Entry.Metadata.DisplayName()} with key value {keyValue} as {node.Entry.State}");
        });

    context.SaveChanges();
}

Pour chaque entité du graphique, le code ci-dessus vérifie la valeur de clé primaire avant de suivre l’entité. Pour les valeurs de clé non définies (zéro), le code fait ce qu’EF Core ferait normalement. Autrement dit, si la clé n’est pas définie, l’entité est marquée comme Added. Si la clé est définie et que la valeur n’est pas négative, l’entité est marquée comme Modified. Cependant, si une valeur de clé négative est trouvée, alors sa valeur réelle et non négative est restaurée et l’entité est suivie comme Deleted.

La sortie de l’exécution de ce code est la suivante :

Tracking Blog with key value 1 as Modified
Tracking Post with key value 1 as Modified
Tracking Post with key value -2 as Deleted
Tracking Post with key value 0 as Added

Remarque

Par souci de simplicité, ce code suppose que chaque entité a une propriété de clé primaire entière appelée Id. Cela peut être codifié dans une classe de base abstraite ou une interface. Vous pouvez également obtenir la propriété ou les propriétés de clé primaire à partir des métadonnées IEntityType afin que ce code fonctionne avec n’importe quel type d’entité.

TrackGraph a deux surcharges. Dans la surcharge simple utilisée ci-dessus, EF Core détermine quand arrêter de traverser le graphique. Plus précisément, il cesse de visiter les nouvelles entités connexes à partir d’une entité donnée lorsque cette entité est déjà suivie, ou lorsque le rappel ne commence pas à suivre l’entité.

La surcharge avancée, ChangeTracker.TrackGraph<TState>(Object, TState, Func<EntityEntryGraphNode<TState>,Boolean>), a un rappel qui retourne une valeur booléenne. Si le rappel retourne « false », la traversée du graphique s’arrête, sinon elle se poursuit. Veillez à éviter les boucles infinies lors de l’utilisation de cette surcharge.

La surcharge avancée permet également de fournir un état à TrackGraph, qui est ensuite transmis à chaque rappel.