EF Core 中的變更追蹤
每個 DbContext 執行個體都會追蹤實體的變更。 然後,這些被追蹤的實體會在呼叫 SaveChanges 時推動資料庫變更。
本文件會說明 Entity Framework Core (EF Core) 變更追蹤,及其與查詢和更新的關係。
提示
您可以從 GitHub 下載範例程式碼,以執行並偵錯此文件中的所有程式碼。
提示
為方便說明,本文件使用與參考了同步方法,例如 SaveChanges,不是其非同步方法,例如 SaveChangesAsync。 除非另有說明,呼叫和等候非同步方法皆可替換。
如何追蹤實體
開始追蹤實體執行個體的時機:
- 從資料庫執行的查詢傳回時
- 使用
Add
、Attach
、Update
或類似方法明確附加至 DbCoNtext 時 - 連線到現有追蹤實體,被偵測為新實體時
不再繼續追蹤實體執行個體的情況:
- DbCoNtext 已被處置
- 已清除變更追蹤器
- 實體已明確中斷連結
DbCoNtext 旨在代表短期的工作單位,如 DbCoNtext 初始化和設定中所述。 這表示處置 DbCoNtext 是停止追蹤實體的「正常方式」。 換言之,DbCoNtext 的存留期應是:
- 建立 DbCoNtext 執行個體
- 追蹤部分實體
- 對實體進行一些變更
- 呼叫 SaveChanges 以更新資料庫
- 處置 DbCoNtext 執行個體
提示
使用此方法時,不需要清除變更追蹤器或明確中斷連結實體執行個體。 不過,如果確實需要中斷連結實體,則呼叫 ChangeTracker.Clear 會比逐一中斷連結實體更有效率。
實體狀態
每個實體都與指定的 EntityState 相關聯:
- DbContext 不會追蹤
Detached
實體。 Added
實體是新的,且尚未插入資料庫。 這表示呼叫 SaveChanges 後會插入這些實體。Unchanged
實體自從在資料庫中查詢過後,「尚未」變更。 所有從查詢傳回的實體一開始都是這個狀態。Modified
實體自從在資料庫中查詢過後,即已變更。 這表示其會在呼叫 SaveChanges 後更新。Deleted
實體存在於資料庫中,但在呼叫 SaveChanges 後標示為待刪除。
EF Core 會追蹤屬性層級的變更。 例如,如果只修改單一屬性值,則資料庫更新只會變更該值。 但當實體本身處於「已修改」狀態時,屬性只能標示為已修改。 (或者,從另一個角度看來,「已修改」狀態表示至少有一個屬性值標示為已修改。)
下表摘要說明不同的狀態:
實體狀態 | 由 DbCoNtext 追蹤 | 存在於資料庫中 | 屬性已修改 | SaveChanges 的動作 |
---|---|---|---|---|
Detached |
No | - | - | - |
Added |
.是 | No | - | 插入 |
Unchanged |
Yes | 是 | 無 | - |
Modified |
.是 | .是 | Yes | 更新 |
Deleted |
是 | Yes | - | 刪除 |
注意
此文字使用關聯式資料庫詞彙以清楚說明。 NoSQL 資料庫一般支援類似的作業,但可能使用不同的名稱。 請參閱資料庫提供者文件以取得詳細資訊。
從查詢追蹤
呼叫 SaveChanges 以使用相同的 DbContext 執行個體來查詢與更新實體時,EF Core 變更追蹤的執行效果最佳。 這是因為 EF Core 會自動追蹤被查詢實體的狀態,然後在呼叫 SaveChanges 後,偵測對這些實體所做的所有變更。
這個方法有數點優於明確追蹤實體執行個體:
- 它為簡單式。 實體狀態很少需要明確操作,因為 EF Core 會負責處理狀態變更。
- 更新僅限於實際變更的值。
- 陰影屬性的值予以保留,並視需要使用。 當外部索引鍵儲存在陰影狀態時特別相關。
- 屬性的原始值會自動保留,並用於有效率的更新。
簡單查詢和更新
例如,請考慮簡單的部落格/貼文模型:
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; }
}
我們可以使用此模型查詢部落格和貼文,然後對資料庫進行一些更新:
using var context = new BlogsContext();
var blog = context.Blogs.Include(e => e.Posts).First(e => e.Name == ".NET Blog");
blog.Name = ".NET Blog (Updated!)";
foreach (var post in blog.Posts.Where(e => !e.Title.Contains("5.0")))
{
post.Title = post.Title.Replace("5", "5.0");
}
context.SaveChanges();
使用 SQLite 作為範例資料庫時,呼叫 SaveChanges 會導致下列資料庫更新:
-- Executed DbCommand (0ms) [Parameters=[@p1='1' (DbType = String), @p0='.NET Blog (Updated!)' (Size = 20)], CommandType='Text', CommandTimeout='30']
UPDATE "Blogs" SET "Name" = @p0
WHERE "Id" = @p1;
SELECT changes();
-- Executed DbCommand (0ms) [Parameters=[@p1='2' (DbType = String), @p0='Announcing F# 5.0' (Size = 17)], CommandType='Text', CommandTimeout='30']
UPDATE "Posts" SET "Title" = @p0
WHERE "Id" = @p1;
SELECT changes();
變更追蹤器偵錯檢視讓您一目了然正在追蹤哪些實體及其狀態,是非常棒的視覺化呈現方式。 例如,在呼叫 SaveChanges 之前,先將下列程式碼插入上述範例中:
context.ChangeTracker.DetectChanges();
Console.WriteLine(context.ChangeTracker.DebugView.LongView);
它會產生下列輸出:
Blog {Id: 1} Modified
Id: 1 PK
Name: '.NET Blog (Updated!)' Modified Originally '.NET Blog'
Posts: [{Id: 1}, {Id: 2}, {Id: 3}]
Post {Id: 1} Unchanged
Id: 1 PK
BlogId: 1 FK
Content: 'Announcing the release of EF Core 5.0, a full featured cross...'
Title: 'Announcing the Release of EF Core 5.0'
Blog: {Id: 1}
Post {Id: 2} Modified
Id: 2 PK
BlogId: 1 FK
Content: 'F# 5 is the latest version of F#, the functional programming...'
Title: 'Announcing F# 5.0' Modified Originally 'Announcing F# 5'
Blog: {Id: 1}
請特別注意:
Blog.Name
屬性會標示為已修改 (Name: '.NET Blog (Updated!)' Modified Originally '.NET Blog'
),這會導致部落格處於Modified
狀態。- 貼文 2 的
Post.Title
屬性標示為已修改 (Title: 'Announcing F# 5.0' Modified Originally 'Announcing F# 5'
),這會導致此貼文處於Modified
狀態。 - 貼文 2 的其他屬性值尚未變更,因此不會標示為已修改。 這就是資料庫更新不包含這些值的原因。
- 其他貼文未以任何方式修改。 這就是貼文仍處於
Unchanged
狀態,且未包含在資料庫更新中的原因。
查詢後插入、更新及刪除
類似上例中的更新可結合相同工作單位中的插入和刪除。 例如:
using var context = new BlogsContext();
var blog = context.Blogs.Include(e => e.Posts).First(e => e.Name == ".NET Blog");
// Modify property values
blog.Name = ".NET Blog (Updated!)";
// Insert a new Post
blog.Posts.Add(
new Post
{
Title = "What’s next for System.Text.Json?", Content = ".NET 5.0 was released recently and has come with many..."
});
// Mark an existing Post as Deleted
var postToDelete = blog.Posts.Single(e => e.Title == "Announcing F# 5");
context.Remove(postToDelete);
context.ChangeTracker.DetectChanges();
Console.WriteLine(context.ChangeTracker.DebugView.LongView);
context.SaveChanges();
在此範例中:
- 查詢並追蹤資料庫中的部落格和相關貼文
Blog.Name
屬性已變更- 新貼文會新增至部落格的現有貼文集合
- 呼叫 DbContext.Remove 將現有的貼文標示為待刪除
呼叫 SaveChanges 之前再次查看變更追蹤器偵錯檢視,可顯示 EF Core 追蹤這些變更的方式:
Blog {Id: 1} Modified
Id: 1 PK
Name: '.NET Blog (Updated!)' Modified Originally '.NET Blog'
Posts: [{Id: 1}, {Id: 2}, {Id: 3}, {Id: -2147482638}]
Post {Id: -2147482638} Added
Id: -2147482638 PK Temporary
BlogId: 1 FK
Content: '.NET 5.0 was released recently and has come with many...'
Title: 'What's next for System.Text.Json?'
Blog: {Id: 1}
Post {Id: 1} Unchanged
Id: 1 PK
BlogId: 1 FK
Content: 'Announcing the release of EF Core 5.0, a full featured cross...'
Title: 'Announcing the Release of EF Core 5.0'
Blog: {Id: 1}
Post {Id: 2} Deleted
Id: 2 PK
BlogId: 1 FK
Content: 'F# 5 is the latest version of F#, the functional programming...'
Title: 'Announcing F# 5'
Blog: {Id: 1}
請注意:
- 部落格標示為
Modified
。 這會產生資料庫更新。 - 貼文 2 標示為
Deleted
。 這會產生資料庫刪除。 - 具有暫存識別碼的新貼文與部落格 1 相關聯,且標示為
Added
。 這會產生資料庫插入。
這會在呼叫 SaveChanges 時,導致下列資料庫命令 (使用 SQLite):
-- Executed DbCommand (0ms) [Parameters=[@p1='1' (DbType = String), @p0='.NET Blog (Updated!)' (Size = 20)], CommandType='Text', CommandTimeout='30']
UPDATE "Blogs" SET "Name" = @p0
WHERE "Id" = @p1;
SELECT changes();
-- Executed DbCommand (0ms) [Parameters=[@p0='2' (DbType = String)], CommandType='Text', CommandTimeout='30']
DELETE FROM "Posts"
WHERE "Id" = @p0;
SELECT changes();
-- Executed DbCommand (0ms) [Parameters=[@p0='1' (DbType = String), @p1='.NET 5.0 was released recently and has come with many...' (Size = 56), @p2='What's next for System.Text.Json?' (Size = 33)], CommandType='Text', CommandTimeout='30']
INSERT INTO "Posts" ("BlogId", "Content", "Title")
VALUES (@p0, @p1, @p2);
SELECT "Id"
FROM "Posts"
WHERE changes() = 1 AND "rowid" = last_insert_rowid();
如需插入與刪除實體的詳細資訊,請參閱明確追蹤實體。 如需如何讓 EF Core 自動偵測這類變更的詳細資訊,請參閱變更偵測和通知。
提示
呼叫 ChangeTracker.HasChanges() 以判斷是否已完成任何變更,導致 SaveChanges 更新資料庫。 如果 HasChanges 傳回 false,則 SaveChanges 將會是無作業。