每個 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 索引鍵屬性 會設定為使用 自動產生的索引鍵值。 這對於變更追蹤來說的主要優點是:未設定的索引鍵值表示實體是「新增」的。 藉由「新增」,我們表示尚未將它插入資料庫中。
下列各節會使用兩個模型。 第一個設定為 不使用 產生的索引鍵值:
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
外鍵屬性是可以是空值的。 這表示關聯性是選擇性的,因此 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,然後此狀態會傳遞至每個回呼。