Remarque
L’accès à cette page nécessite une autorisation. Vous pouvez essayer de vous connecter ou de modifier des répertoires.
L’accès à cette page nécessite une autorisation. Vous pouvez essayer de modifier des répertoires.
Chaque DbContext instance suit les modifications apportées aux entités. Ces entités suivies entraînent à leur tour 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 DbContext instance est utilisée pour interroger des entités et les mettre à jour en appelant SaveChanges. Cela est dû au fait qu’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.
Tip
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 Change Tracking in EF Core .
Tip
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.
Introduction
Les entités peuvent être explicitement « attachées » à un DbContext tel que le contexte effectue ensuite le suivi de ces entités. Ceci est principalement utile lorsque :
- Création de nouvelles entités qui seront insérées dans la base de données.
- Rattacher à nouveau des entités déconnectées qui ont été interrogées précédemment par une autre instance DbContext.
La première d’entre elles sera nécessaire par la plupart des applications et est principalement gérée par les DbContext.Add méthodes.
La seconde est nécessaire uniquement par 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 sont 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 de SaveChanges pouvoir 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.
Tip
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 l’utilisation d’une requête de suivi et peut également entraîner des problèmes tels que des valeurs de propriété fantôme manquantes, ce qui complique les choses.
Valeurs de clé générées et 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 « nouveau », nous entendons qu’il n’a pas encore été inséré 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. Ensuite, un exemple est donné où les valeurs de clé générées sont utilisées :
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é de 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 Ajouté en appelant l'une des méthodes DbContext.Add, DbContext.AddRange, DbContext.AddAsync, DbContext.AddRangeAsync ou les méthodes équivalentes sur DbSet<TEntity>.
Tip
Ces méthodes fonctionnent toutes de la même façon dans le contexte du suivi des modifications. Pour plus d’informations, consultez les fonctionnalités de suivi des modifications supplémentaires.
Par exemple, pour commencer à suivre un nouveau blog :
context.Add(
new Blog { Id = 1, Name = ".NET Blog", });
En inspectant la vue de débogage du suivi des modifications après cet appel, on constate que le contexte suit la nouvelle entité dans l'état Added :
Blog {Id: 1} Added
Id: 1 PK
Name: '.NET Blog'
Posts: []
Toutefois, les méthodes Add ne fonctionnent pas uniquement sur une entité individuelle. Ils commencent en fait à suivre un graphique entier d’entités associées, en les plaçant tous à l’état Added . Par exemple, pour insérer un nouveau blog et les nouveaux billets associés :
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 Id propriétés de clé dans les exemples ci-dessus. Cela est dû au fait que le modèle ici a été configuré pour utiliser explicitement définir des valeurs de clé, 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ées, 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 les 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 un nouveau blog et des articles, tous 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ée, à quel moment les valeurs de clé réelles sont lues à 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.
Tip
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 tentera alors d'insérer l'entité en utilisant cette valeur de clé explicite. Certaines configurations de base de données, y compris SQL Server avec des colonnes Identity, échouent à traiter ces insertions et généreront une erreur (consultez ces documents pour obtenir une solution de contournement).
Attachement d’entités existantes
Valeurs de clé explicites
Les entités renvoyées par des 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, par exemple reçue 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", });
Note
Les exemples ici créent explicitement des entités avec new pour plus de simplicité. Normalement, les instances d'entité proviennent d'une autre source, comme par la désérialisation à partir d'un client ou par la création à partir de données issues d'une requête HTTP POST.
L'inspection de la vue de débogage du change tracker après 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 à l’état Unchanged . Par exemple, pour joindre un blog existant et des billets existants associés :
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}
L’appel de SaveChanges à ce stade n’aura aucun effet. 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 les 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 billets :
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 une clé définie, mais la troisième n'en a pas. EF Core voit cette valeur de clé comme 0, la valeur clR par défaut pour un entier. Cela entraîne le marquage EF Core de la nouvelle entité au 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...'
L’appel de SaveChanges à ce stade ne fait rien avec les Unchanged entités, mais 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 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.UpdateRangeet les méthodes équivalentes sur DbSet<TEntity> se comportent exactement comme les Attach méthodes décrites ci-dessus, sauf 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", });
En inspectant la vue de débogage du suivi des modifications après cet appel, on constate 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 associées comme Modified. Par exemple, pour joindre un blog existant et les billets existants associés 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}
L’appel de 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 : Updateune 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 à DbContext de détecter automatiquement les nouvelles entités et de les placer dans l’état Added . Par exemple, en appelant Update sur le graphe suivant 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, le post sans valeur de clé est détecté comme nouveau et passé à 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}
L’appel 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 effrayé par cela ; pour de nombreuses applications avec de petits graphiques, il peut s’agir d’un moyen simple et pragmatique de générer des mises à jour. 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 un billet existant comme Deleted, procédez comme suit :
context.Remove(
new Post { Id = 2 });
En inspectant la vue de débogage du suivi des modifications après cet appel, on constate 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 de 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
La suppression d’entités dépendantes/enfants d’un graphique est plus simple que la suppression d’entités principal/parent. Pour plus d’informations, consultez la section suivante et Modification des clés étrangères et des navigations.
Il est inhabituel d’appeler Remove 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 associées, puis d’appeler Remove les entités qui doivent être supprimées. Ce graphique d’entités suivies est généralement créé par :
- L'exécution d'une requête pour récupérer les entités
- Utilisation des méthodes
AttachouUpdatesur 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 un post d'un client, puis de faire quelque chose comme ceci :
context.Attach(post);
context.Remove(post);
Cela se comporte exactement de la même façon que l’exemple précédent, car l’appel 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é appliqué :
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 de 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 extrémité principale ou parent, et une extrémité dépendante ou enfant. L'entité dépendante/enfant est celle qui possède la propriété de clé étrangère. Dans une relation un-à-plusieurs, le principal/parent se trouve du côté « un » et le dépendant/enfant est du côté « plusieurs ». Pour plus d’informations, consultez Relations .
Dans les exemples précédents, nous supprimions un post, qui est une entité dépendante/enfant dans la relation un-à-plusieurs entre blog et posts. 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/parent doit également avoir un impact sur les entités dépendantes/enfants. Ne pas le faire laisserait une valeur de clé étrangère référencer 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 :
- Mise à null des valeurs FK. Cela indique que les dépendants/enfants ne sont plus liés à aucun principal/parent. Il s’agit de la valeur par défaut pour les relations facultatives où la clé étrangère doit être nullable. La définition de la clé FK sur null n’est pas valide pour les relations requises, où la clé étrangère est généralement non nullable.
- Suppression des personnes 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 Post.BlogId propriété de clé étrangère est nullable 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 BlogId propriétés de clé étrangère 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 change tracker après l'appel à Remove montre que, comme prévu, le blog est maintenant 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, tous les billets connexes sont maintenant marqués comme Modified. Cela est dû au fait que la propriété de clé étrangère de chaque entité a été définie sur Null. L’appel de SaveChanges met à jour la valeur de clé étrangère pour chaque billet 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 de 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 Post.BlogId propriété de clé étrangère n’est pas 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 le principal/parent est supprimé. Par exemple, la suppression d’un blog avec des billets connexes 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 change tracker après 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}
Plus intéressant dans ce cas est que tous les postes connexes ont également été marqués comme Deleted. L’appel de SaveChanges entraîne la suppression du blog et de tous les billets connexes 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 de DbContext, car elles n’existent plus dans la base de données. La sortie de la vue de débogage est donc vide.
Note
Ce document ne fait qu'effleurer la gestion des relations dans EF Core. Consultez Relations pour plus d’informations sur la modélisation des relations et la 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 à une logique personnalisée d’être utilisée lors de la détermination de la façon de suivre des entités individuelles dans un graphique.
Par exemple, considérez la règle QU’EF Core utilise 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 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 async Task 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}");
});
await context.SaveChangesAsync();
}
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 normalement ce qu’EF Core ferait. 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. Toutefois, si une valeur de clé négative est trouvée, 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
Note
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 IEntityType métadonnées afin que ce code fonctionne avec n’importe quel type d’entité.
TrackGraph dispose de deux surcharges. Dans la surcharge simple utilisée ci-dessus, EF Core détermine quand arrêter de parcourir le graphique. Plus précisément, il cesse de visiter de nouvelles entités associées à partir d’une entité donnée lorsque cette entité est déjà suivie, ou lorsque le rappel ne démarre pas le suivi de l’entité.
La surcharge avancée, ChangeTracker.TrackGraph<TState>(Object, TState, Func<EntityEntryGraphNode<TState>,Boolean>), inclut un rappel qui renvoie un booléen. Si le callback renvoie la valeur false, alors la traversée du graphe s'arrête. Sinon, elle continue. Il convient d'éviter les boucles infinies lors de l'utilisation de cette surcharge.
Cette surcharge avancée permet aussi de fournir un état à TrackGraph, qui est ensuite passé à chaque callback.