Share via


EF Core 中的變更追蹤

每個 DbContext 執行個體都會追蹤實體的變更。 然後,這些被追蹤的實體會在呼叫 SaveChanges 時推動資料庫變更。

本文件會說明 Entity Framework Core (EF Core) 變更追蹤,及其與查詢和更新的關係。

提示

您可以從 GitHub 下載範例程式碼,以執行並偵錯此文件中的所有程式碼。

提示

為方便說明,本文件使用與參考了同步方法,例如 SaveChanges,不是其非同步方法,例如 SaveChangesAsync。 除非另有說明,呼叫和等候非同步方法皆可替換。

如何追蹤實體

開始追蹤實體執行個體的時機:

  • 從資料庫執行的查詢傳回時
  • 使用 AddAttachUpdate 或類似方法明確附加至 DbCoNtext 時
  • 連線到現有追蹤實體,被偵測為新實體時

不再繼續追蹤實體執行個體的情況:

  • DbCoNtext 已被處置
  • 已清除變更追蹤器
  • 實體已明確中斷連結

DbCoNtext 旨在代表短期的工作單位,如 DbCoNtext 初始化和設定中所述。 這表示處置 DbCoNtext 是停止追蹤實體的「正常方式」。 換言之,DbCoNtext 的存留期應是:

  1. 建立 DbCoNtext 執行個體
  2. 追蹤部分實體
  3. 對實體進行一些變更
  4. 呼叫 SaveChanges 以更新資料庫
  5. 處置 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 將會是無作業。