共用方式為


明確追蹤實體

每個 DbContext 實例都會追蹤對實體所做的變更。 當 SaveChanges 被呼叫時,這些被追蹤的實體會驅動資料庫的變更。

當相同的 DbContext 實例用於查詢實體並藉由呼叫 SaveChanges來更新實體時,Entity Framework Core (EF Core) 變更追蹤效果最佳。 這是因為 EF Core 會自動追蹤查詢實體的狀態,然後在呼叫 SaveChanges 時偵測對這些實體所做的任何變更。 此方法已在 EF Core 中的變更追蹤涵蓋。

小提示

本文件假設您已瞭解實體狀態以及 EF Core 變更追蹤的基本概念。 如需這些主題的詳細資訊,請參閱 EF Core 中的變更追蹤

小提示

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

小提示

為了簡單起見,本文件會使用和參考同步方法,例如SaveChanges,而不是其異步版本,例如SaveChangesAsync。 除非另有說明,否則可以替代呼叫和等候異步方法。

簡介

實體可以明確地「附加」至 DbContext ,讓內容追蹤這些實體。 這在下列情況下主要很有用:

  1. 建立將插入至資料庫的新實體。
  2. 重新附加先前由 不同 DbContext 實例查詢的已中斷連線實體。

大部分的應用程式都需要其中一個,而且主要是由 DbContext.Add 方法處理。

只有在未 追蹤實體時,變更實體或其關聯性的應用程式才需要第二個。 例如,Web 應用程式可能會將實體傳送至用戶進行變更的 Web 用戶端,並將實體傳回。 這些實體稱為「已中斷連線」,因為它們最初是從 DbContext 查詢出來的,但在被傳送至客戶端時,已與該上下文中斷連線。

Web 應用程式現在必須重新關聯這些實體,以便再次追蹤並指示已經進行的變更,從而使 SaveChanges 能夠對資料庫進行適當的更新。 這主要是由 DbContext.AttachDbContext.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.AddRangeDbContext.AddAsyncDbContext.AddRangeAsyncDbSet<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.AttachDbContext.AttachRangeDbSet<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: []

就像 AddAttach 實際上會將連線實體的整個圖形設定為 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 將新實體標示為 AddedUnchanged而不是 :

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.UpdateDbContext.UpdateRangeDbSet<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: []

就像AddAttach一樣,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 狀態中被追蹤。 實體通常會藉由呼叫DeletedDbContext.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 是不尋常的。 此外,與 AddAttachUpdate 不同,針對尚未在 RemoveUnchanged 狀態中追蹤的實體呼叫 Modified 並不常見。 相反地,通常會追蹤一個單一的實體或一組相關的實體,然後在應刪除的實體上呼叫 Remove。 追蹤實體的這個圖表通常是由下列其中一項所建立:

  1. 執行實體對象的查詢
  2. 在中斷連線實體的圖表上,使用AttachUpdate方法,如前幾節所述。

例如,上一節中的程式代碼更有可能從用戶端取得貼文,然後執行如下動作:

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}

刪除主要/父級實體

每個連接兩個實體類型的關聯性都有一個主端或父端,以及一個依賴端或子端。 相依/子實體是具有外鍵屬性的實體。 在一對多關係中,主體/父項位於「一」端,而相依/子項位於「多」端。 如需詳細資訊,請參閱 關聯 性。

在上述範例中,我們會刪除文章,這是部落格文章一對多關聯性中的相依/子實體。 這相當簡單,因為移除相依/子實體不會影響其他實體。 另一方面,刪除主體或父實體時,也必須影響到所有相依或子實體。 若未這麼做,則會遺留一個外鍵值,參考的是一個已不存在的主鍵值。 這是無效的模型狀態,而且會導致大部分資料庫中的引用條件約束錯誤。

這個無效的模型狀態可以透過兩種方式來處理:

  1. 將 FK 值設定為 null。 這表示子女不再與任何父母有關。 這是用於可選關聯的預設值,其中外鍵必須是可為 Null 的。 將外鍵(FK)設為 null 在必需的關聯中是不允許的,因為外鍵通常是不可為 Null 的。
  2. 刪除相依項目/子項目。 這是必要關聯性的預設值,也適用於選擇性關聯性。

如需變更追蹤和關聯性的詳細資訊,請參閱 變更外鍵和導覽

選擇性關聯性

我們使用的模型中,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 的運作方式就像 AddAttachUpdate,不同之處在於它會在追蹤每個實體實例之前產生回呼。 這可讓自定義邏輯在決定如何追蹤圖形中的個別實體時使用。

例如,假設 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,然後此狀態會傳遞至每個回呼。