每个 DbContext 实例跟踪对实体所做的更改。 当调用 SaveChanges 时,这些跟踪实体反过来会驱动对数据库的更改。
当同一 DbContext 实例用于查询实体并通过调用 SaveChanges来更新实体时,Entity Framework Core (EF Core) 更改跟踪效果最佳。 这是因为 EF Core 会自动跟踪查询实体的状态,然后在调用 SaveChanges 时检测对这些实体所做的任何更改。 EF Core 中的更改跟踪中介绍了此方法。
小窍门
本文档假定了解 EF Core 更改跟踪的实体状态和基础知识。 有关这些主题的详细信息,请参阅 EF Core 中的更改跟踪 。
小窍门
可以通过 从 GitHub 下载示例代码来运行和调试本文档中的所有代码。
小窍门
为简单起见,本文档使用并引用同步方法,例如 SaveChanges ,而不是其异步等效方法,例如 SaveChangesAsync。 调用和等待异步方法可以被替代,除非有另行说明。
介绍
实体可以显式地“附加到” DbContext 上,使上下文能够跟踪这些实体。 这在以下情况下主要有用:
- 创建将插入到数据库中的新实体。
- 将之前由 不同 DbContext 实例查询的断开连接的实体重新连接。
大多数应用程序都需要其中的第一个,主要由 DbContext.Add 方法处理。
只有在更改实体或其关系但未对实体进行跟踪时,应用程序才需要第二个。 例如,Web 应用程序可能会将实体发送到 Web 客户端,用户在其中进行更改并将实体发送回。 这些实体称为“已断开连接”,因为它们最初是从 DbContext 查询的,但随后在发送到客户端时与该上下文断开连接。
现在,Web 应用程序必须重新附加这些实体,以便再次追踪它们的变化,并清楚地表明已进行的更改,从而使 SaveChanges 可以对数据库进行适当的更新。 这主要由 DbContext.Attach 和 DbContext.Update 方法处理。
小窍门
通常不需要将实体附加到从中查询的 同一 DbContext 实例 。 不要定期执行无跟踪查询,然后将返回的实体附加到同一上下文。 这会比使用跟踪查询要慢,并可能导致影子属性值缺失等问题,从而更难准确获得。
生成的键值与显式键值
默认情况下,整数和 GUID 键属性 配置为使用 自动生成的键值。 这具有更改跟踪的主要优势:未设置键值指示实体为“new”。 通过“new”,我们表示尚未插入到数据库中。
以下各节使用了两个模型。 第一个配置为 不使用 生成的键值:
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; }
}
非生成(即显式设置)键值首先显示在每个示例中,因为一切都非常明确且易于遵循。 然后,下面是使用生成的键值的示例:
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; }
}
请注意,此模型中的键属性不需要在此处进行其他配置,因为使用生成的键值是 简单整数键的默认值。
插入新实体
显式键值
要插入前,实体必须在Added
状态中被SaveChanges跟踪。 实体通常通过调用DbContext.Add、DbContext.AddRange、DbContext.AddAsync、DbContext.AddRangeAsync或在DbSet<TEntity>上调用等效的方法,将其置于“已添加”状态。
小窍门
这些方法在更改跟踪的上下文中都以相同的方式工作。 有关详细信息,请参阅 其他更改跟踪功能 。
例如,若要开始跟踪新博客,
context.Add(
new Blog { Id = 1, Name = ".NET Blog", });
通过此调用检查 更改跟踪器调试视图 ,显示上下文正在跟踪状态中的 Added
新实体:
Blog {Id: 1} Added
Id: 1 PK
Name: '.NET Blog'
Posts: []
但是,Add 方法不仅适用于单个实体。 它们实际上开始跟踪 相关实体的整个图,并将其全部 Added
置于状态。 例如,若要插入新的博客和关联的新文章,请执行以下作:
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..."
}
}
});
上下文现在跟踪所有这些实体, 如下所示 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}
请注意,已为上述示例中的 Id
键属性设置显式值。 这是因为此处的模型已配置为使用显式设置的键值,而不是自动生成的键值。 如果不使用生成的密钥,则必须在调用Add
显式设置密钥属性。 然后,调用 SaveChanges 时插入这些键值。 例如,使用 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);
SaveChanges 完成后,所有这些实体都会在 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}
生成的键值
如上所述,整数和 GUID 键属性 配置为默认使用 自动生成的键值 。 这意味着应用程序 不得显式设置任何键值。 例如,若要插入新的博客,并发布所有包含生成的键值的文章:
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..."
}
}
});
与显式键值一样,上下文现在将跟踪所有这些实体,如下所示 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}
请注意,在这种情况下,已为每个实体生成 临时键值 。 EF Core 使用这些值,直到调用 SaveChanges,此时从数据库读取实际键值。 例如,使用 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();
SaveChanges 完成后,所有实体都已被更新为实际的键值,并且在 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}
这与使用显式键值的上一个示例完全相同的结束状态。
小窍门
即使使用生成的键值,仍可以设置显式键值。 然后,EF Core 将尝试使用此键值进行插入。 某些数据库配置(包括包含标识列的 SQL Server)不支持此类插入操作,并会引发错误(请参阅这些文档以获取解决方法)。
附加现有实体
显式键值
从查询返回的 Unchanged
实体在跟踪状态中进行记录。 状态 Unchanged
表示自查询实体以来尚未对其进行修改。 一个可能从 HTTP 请求中的 Web 客户端返回的断开连接的实体,可以通过使用DbContext.Attach、DbContext.AttachRange或在DbSet<TEntity>上的等效方法被置于这种状态。 例如,若要开始跟踪现有博客,
context.Attach(
new Blog { Id = 1, Name = ".NET Blog", });
注释
为了简单起见,此处的示例是通过显式使用new
创建实体。 通常,实体实例来自另一个源,例如从客户端反序列化,或从 HTTP Post 中的数据创建。
调用后检查 更改跟踪器调试视图,显示实体处于 Unchanged
状态:
Blog {Id: 1} Unchanged
Id: 1 PK
Name: '.NET Blog'
Posts: []
就像 Add
, Attach
实际上将连接实体的整个图设置为 Unchanged
状态。 例如,若要附加现有博客和关联的现有文章,
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..."
}
}
});
上下文现在跟踪所有这些实体, 如下所示 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}
此时调用 SaveChanges 将不起作用。 所有实体都标记为 Unchanged
,因此数据库中没有任何更新内容。
生成的键值
如上所述,整数和 GUID 键属性 配置为默认使用 自动生成的键值 。 这在处理断开连接的实体时具有主要优势:未设置键值表示实体尚未插入数据库中。 这允许更改跟踪器自动检测新实体并将其置于 Added
状态。 例如,请考虑附加博客和文章的此图:
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..."
},
}
});
博客的键值为 1,指示该博客已存在于数据库中。 其中两个帖子还设置了键值,但第三个帖子没有设置。 EF Core 会将此键值视为 0,这是整数的 CLR 默认值。 这会导致 EF Core 将新实体标记为 Added
而不是 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...'
此时调用 SaveChanges 不会对 Unchanged
实体执行任何作,但会将新实体插入数据库中。 例如,使用 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();
此处要注意的要点是,使用生成的键值,EF Core 能够 自动区分断开连接的图中的新实体和现有实体。 简言之,使用生成的键时,EF Core 将始终在该实体未设置键值时插入实体。
更新现有实体
显式键值
DbContext.Update、DbContext.UpdateRange以及DbSet<TEntity>上的等效方法的行为与上述Attach
方法完全一致,只是将实体放入Modified
而不是Unchanged
状态。 例如,若要开始将现有博客跟踪为 Modified
:
context.Update(
new Blog { Id = 1, Name = ".NET Blog", });
在调用此方法后检查 更改跟踪器调试视图,显示上下文正在跟踪此实体,并将其置于Modified
状态。
Blog {Id: 1} Modified
Id: 1 PK
Name: '.NET Blog' Modified
Posts: []
就像 Add
和 Attach
一样,Update
实际上将相关实体的整个图标识为 。 例如,若要将现有博客和关联的现有文章附加为 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..."
}
}
});
上下文现在跟踪所有这些实体, 如下所示 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}
此时调用 SaveChanges 会导致更新发送到数据库并应用于所有这些实体。 例如,使用 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();
生成的键值
与生成 Attach
键值一样,生成的键值具有相同的主要好处 Update
:未设置的键值指示实体是新的,尚未插入到数据库中。 与上一样 Attach
,这允许 DbContext 自动检测新实体并将其置于 Added
状态。 例如,请考虑使用此博客和博客文章的图来调用 Update
。
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..."
},
}
});
与示例 Attach
一样,没有键值的帖子被检测为新帖子,并设置为 Added
状态。 其他实体标记为 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}
此时调用 SaveChanges
将导致所有现有实体的更新发送到数据库,同时插入新实体。 例如,使用 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();
这是从断开连接的图形生成更新和插入的一种非常简单的方法。 即使某些属性值可能尚未更改,仍会导致每个被跟踪实体的所有属性更新或插入的请求被发送到数据库。 不要太害怕这个:对于具有小型图形的许多应用程序,这可以是生成更新的简单务实方法。 也就是说,其他更复杂的模式有时可能会导致更高效的更新,如 EF Core 中的标识解析中所述。
删除现有实体
对于要通过 SaveChanges 删除的实体,必须在Deleted
状态下进行跟踪。 实体通常通过调用Deleted
、DbContext.Remove之一或在DbContext.RemoveRange上调用等效的方法,被置于DbSet<TEntity>状态。 例如,将现有的帖子标记为 Deleted
:
context.Remove(
new Post { Id = 2 });
在此调用后检查 更改跟踪器调试视图 显示上下文正在跟踪 Deleted
状态中的实体。
Post {Id: 2} Deleted
Id: 2 PK
BlogId: <null> FK
Content: <null>
Title: <null>
Blog: <null>
调用 SaveChanges 时,将删除此实体。 例如,使用 SQLite 时:
-- Executed DbCommand (0ms) [Parameters=[@p0='2' (DbType = String)], CommandType='Text', CommandTimeout='30']
DELETE FROM "Posts"
WHERE "Id" = @p0;
SELECT changes();
SaveChanges 完成后,已删除的实体将从 DbContext 中分离,因为它不再存在于数据库中。 因此,调试窗口为空,因为未跟踪任何实体。
删除从属实体/子实体
从图形中删除从属/子实体比删除主体/父实体更为简单。 有关详细信息,请参阅下一部分以及 更改外键和导航。
使用Remove
创建的实体上进行new
调用是不寻常的。 此外,与 Add
、Attach
和 Update
不同,通常不会对尚未在 Remove
或 Unchanged
状态中跟踪的实体调用 Modified
。 相反,通常会跟踪一个单独的实体或一组相关实体的图,然后对需要删除的实体调用 Remove
。 此跟踪实体图通常由以下任一项创建:
- 对实体进行查询
- 如前述部分所述,在断开连接的实体图上使用
Attach
或Update
方法。
例如,上一部分中的代码更有可能从客户端获取数据,然后执行类似的操作。
context.Attach(post);
context.Remove(post);
这与上一个示例的表现完全相同,因为调用 Remove
未跟踪的实体会导致先连接它,然后标记为 Deleted
。
在更现实的示例中,首先附加实体图,然后将其中一些实体标记为已删除。 例如:
// Attach a blog and associated posts
context.Attach(blog);
// Mark one post as Deleted
context.Remove(blog.Posts[1]);
除调用了Unchanged
的实体之外,所有实体均标记为Remove
:
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}
调用 SaveChanges 时,将删除此实体。 例如,使用 SQLite 时:
-- Executed DbCommand (0ms) [Parameters=[@p0='2' (DbType = String)], CommandType='Text', CommandTimeout='30']
DELETE FROM "Posts"
WHERE "Id" = @p0;
SELECT changes();
SaveChanges 完成后,已删除的实体将从 DbContext 中分离,因为它不再存在于数据库中。 其他实体仍处于 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}
删除主要/父级实体
连接两个实体类型的每个关系都有一个主体或父端,以及一个从属或子端。 依赖/子实体是具有外键属性的实体。 在一对多关系中,主体/父级位于“一”端,依赖/子级位于“多”端。 有关详细信息,请参阅 关系 。
在前面的示例中,我们删除了一篇文章,这是博客文章一对多关系中的依赖/子实体。 这相对简单,因为删除依赖/子实体对其他实体没有任何影响。 另一方面,删除主实体或父实体时,必须同时影响所有依赖实体或子实体。 不采取行动将导致外键值引用一个不再存在的主键值。 这是一个无效的模型状态,导致大多数数据库中出现引用约束错误。
可以通过两种方式处理此无效的模型状态:
- 将 FK 值设置为 null。 这表示受养人/子女不再与任何主申请人/父母相关。 这是可选关系的默认值,其中外键必须为 null。 将 FK 设置为 null 对于必需的关系无效,其中外键通常不可为 null。
- 删除从属对象/子项。 这是必需关系的默认值,也对可选关系有效。
有关更改跟踪和关系的详细信息,请参阅 更改外键和导航 。
可选关系
外 Post.BlogId
键属性在我们一直在使用的模型中为 null。 这意味着关系是可选的,因此 EF Core 的默认行为是在删除博客时将外键属性设置为 BlogId
null。 例如:
// Attach a blog and associated posts
context.Attach(blog);
// Mark the blog as deleted
context.Remove(blog);
在调用后检查Remove
,显示博客现在已标记为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>
更有趣的是,所有相关帖子现在都标记为 Modified
。 这是因为每个实体中的外键属性已设置为 null。 调用 SaveChanges 会将数据库中每个帖子的外键值更新为 null,然后删除博客:
-- 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();
SaveChanges 完成后,已删除的实体将从 DbContext 中分离,因为它不再存在于数据库中。 其他实体现在标记为 Unchanged
,具有空值的外键,与数据库的状态相匹配。
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>
必需的关系
Post.BlogId
如果外键属性不可为 null,则博客和文章之间的关系将变为“必需”。 在这种情况下,在删除主体/父实体时,EF Core 将默认删除从属/子实体。 例如,删除包含相关文章的博客,如前面的示例所示:
// Attach a blog and associated posts
context.Attach(blog);
// Mark the blog as deleted
context.Remove(blog);
在对 的调用后检查Remove
,显示博客再次标记为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}
更有趣的是,在这种情况下,所有相关的帖子也被标记为 Deleted
。 调用 SaveChanges 会导致博客和所有相关文章从数据库中删除:
-- 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;
SaveChanges 完成后,所有已删除的实体都会从 DbContext 中分离,因为它们不再存在于数据库中。 因此,调试视图中的输出为空。
注释
本文档只是浅尝辄止介绍如何在 EF Core 中处理(或管理)关系。 有关建模关系以及更改外键和导航的详细信息,请参阅“关系”,了解有关在调用 SaveChanges 时更新/删除依赖实体/子实体的详细信息。
使用 TrackGraph 进行自定义跟踪
ChangeTracker.TrackGraph的工作方式与Add
、Attach
和Update
类似,但在跟踪之前,它会为每个实体实例生成一个回调。 这允许在确定如何跟踪图形中的单个实体时使用自定义逻辑。
例如,假设 EF Core 在跟踪具有生成的键值的实体时使用的规则:如果键值为零,则实体为新实体,应插入。 让我们扩展此规则来说明键值是否为负值,则应删除实体。 这样,我们就可以更改断开连接图形实体中的主键值,以标记已删除的实体:
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;
然后,可以使用 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();
}
对于图形中的每个实体,上述代码在 跟踪实体之前会检查主键值。 对于未设置(零)键值的情况,代码会执行 EF Core 通常会做的默认操作。 也就是说,如果未设置键,则实体将被标记为 Added
。 如果设置了键且值为非负值,则实体将被标记为 Modified
。 但是,如果找到负键值,则会还原其实际非负值,并将实体跟踪为 Deleted
。
运行此代码的输出为:
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
注释
为简单起见,此代码假定每个实体都有一个名为整数的主键属性 Id
。 这可以编码为抽象基类或接口。 或者,可以从元数据中获取 IEntityType 主键属性或属性,以便此代码适用于任何类型的实体。
TrackGraph 有两个重载。 在上述简单重载中,EF Core 确定何时停止遍历图形。 具体而言,当某个实体已被跟踪,或者回调没有开始对该实体进行跟踪时,它将停止从该给定实体访问新的相关实体。
高级重载ChangeTracker.TrackGraph<TState>(Object, TState, Func<EntityEntryGraphNode<TState>,Boolean>)包含一个回调函数,该函数返回一个布尔值。 如果回调返回 false,则图遍历停止,否则继续。 在使用此重载时,必须小心避免无限循环。
高级重载还允许向 TrackGraph 提供状态,然后将此状态传递给每个回调。