明確追蹤實體
每個 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);
這些實體都會在 Unchanged
SaveChanges 完成之後的狀態進行追蹤,因為這些實體現在存在於資料庫中:
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.AttachRange 或 上的 DbSet<TEntity> 對等方法,進入這個狀態 DbContext.Attach 。 例如,若要開始追蹤現有的部落格:
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
。 例如,若要將現有的部落格和相關聯的現有文章附加為 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
追蹤該實體。 實體通常會藉由呼叫 上的其中 DbContext.Remove 一個 、 DbContext.RemoveRange 或對等方法 DbSet<TEntity> ,置於 Deleted
狀態中。 例如,若要將現有的貼文標示為 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 中斷連結,因為它已不存在於資料庫中。 因此,偵錯檢視是空的,因為不會追蹤任何實體。
刪除相依/子實體
從圖形中刪除相依/子實體比刪除主體/父實體更為直接。 如需詳細資訊,請參閱下一節和 變更外鍵和導覽 。
在以 new
建立的實體上呼叫 Remove
是不尋常的。 此外,與 和 Update
不同 Attach
Add
,對尚未在 或 Modified
狀態中 Unchanged
追蹤的實體呼叫 Remove
並不常見。 相反地,通常會追蹤相關實體的單一實體或圖表,然後在應刪除的實體上呼叫 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
Null 外鍵值,其符合資料庫的狀態:
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
, Update
Attach
不同之處在于它會在追蹤每個實體實例之前產生回呼。 這可讓自訂邏輯在決定如何追蹤圖形中的個別實體時使用。
例如,假設 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 void 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}");
});
context.SaveChanges();
}
針對圖形中的每個實體,上述程式碼會先檢查主鍵值 ,再追蹤實體 。 針對未設定 (零) 索引鍵值,程式碼會執行 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>) 具有傳回 bool 的回呼。 如果回呼傳回 false,則圖表周遊會停止,否則會繼續。 在使用這個多載時,必須小心避免無限迴圈。
進階多載也允許將狀態提供給 TrackGraph,然後此狀態會傳遞至每個回呼。