共用方式為


其他變更追蹤功能

本文件涵蓋涉及變更追蹤的雜項功能和案例。

小提示

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

小提示

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

AddAddAsync

Entity Framework Core (EF Core) 每當使用該方法可能會導致資料庫互動時,都提供異步方法。 使用不支援高效能異步存取的資料庫時,也會提供同步方法以避免額外負荷。

DbContext.AddDbSet<TEntity>.Add 通常不會存取資料庫,因為這些方法原本就只是開始追蹤實體。 不過,某些形式的值產生 可能會 存取資料庫,以產生鍵值。 執行這項動作並隨附於 EF Core 的唯一值產生器是 HiLoValueGenerator<TValue>。 使用這個發電機是很少見的;預設狀態下,它不會被配置。 這表示絕大多數應用程式都應該使用 Add,而不是 AddAsync

其他類似的方法,例如 UpdateAttachRemove 沒有異步多載,因為它們永遠不會產生新的索引鍵值,因此永遠不需要存取資料庫。

AddRangeUpdateRangeAttachRangeRemoveRange

DbSet<TEntity>DbContext 提供、AddUpdateAttachRemove替代版本,以接受單一呼叫中的多個實例。 這些方法分別為 AddRangeUpdateRangeAttachRangeRemoveRange

為了方便起見,會提供這些方法。 使用 「range」 方法的功能與對等的非範圍方法的多個呼叫相同。 這兩種方法之間沒有顯著的效能差異。

備註

這與 EF6 不同,其中 AddRangeAdd 都會自動呼叫 DetectChanges,但多次呼叫 Add 會導致 DetectChanges 多次呼叫,而不是呼叫一次。 這讓 EF6 的 AddRange 更有效率。 在EF Core 中,這兩種方法都不會自動呼叫 DetectChanges

DbContext 與 DbSet 方法

許多方法,包括 AddUpdateAttachRemove,在 DbSet<TEntity>DbContext 上都有實作。 這些方法對一般實體類型的行為 完全相同 。 這是因為實體的 CLR 類型唯獨對應於 EF Core 模型中的唯一實體類型。 因此,CLR 類型會完整定義實體放入模型中的位置,因此可以隱含判斷要使用的 DbSet。

此規則的例外狀況是使用共用類型實體類型,主要用於多對多聯結實體。 使用共用類型實體類型時,必須先針對正在使用的EF Core 模型類型建立 DbSet。 接著,您可以在 DbSet 上使用 AddUpdateAttachRemove 方法,而不會有任何 EF Core 模型類型的模棱兩可。

共享型實體類型預設會用於多對多關聯性中的聯結實體。 您也可以明確設定共享實體類型,以用於多對多關係中。 例如,下列程式代碼會 Dictionary<string, int> 設定為聯結實體類型:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder
        .SharedTypeEntity<Dictionary<string, int>>(
            "PostTag",
            b =>
            {
                b.IndexerProperty<int>("TagId");
                b.IndexerProperty<int>("PostId");
            });

    modelBuilder.Entity<Post>()
        .HasMany(p => p.Tags)
        .WithMany(p => p.Posts)
        .UsingEntity<Dictionary<string, int>>(
            "PostTag",
            j => j.HasOne<Tag>().WithMany(),
            j => j.HasOne<Post>().WithMany());
}

變更外鍵和導覽 示範如何藉由追蹤新的聯結實體實例來關聯兩個實體。 下列程式代碼會針對 Dictionary<string, int> 用於聯結實體的共用類型實體類型執行此動作:

using var context = new BlogsContext();

var post = await context.Posts.SingleAsync(e => e.Id == 3);
var tag = await context.Tags.SingleAsync(e => e.Id == 1);

var joinEntitySet = context.Set<Dictionary<string, int>>("PostTag");
var joinEntity = new Dictionary<string, int> { ["PostId"] = post.Id, ["TagId"] = tag.Id };
joinEntitySet.Add(joinEntity);

Console.WriteLine(context.ChangeTracker.DebugView.LongView);

await context.SaveChangesAsync();

請注意, DbContext.Set<TEntity>(String) 用來建立 PostTag 實體類型的 DbSet。 接著,您可以使用這個 DbSet 來對新的聯結實體實例執行 Add

這很重要

依慣例用於聯結實體類型的 CLR 類型在未來版本中可能會變更,以改善效能。 除非已在上述程式代碼中明確設定為 Dictionary<string, int>,否則請勿依賴任何特定的聯結實體類型。

屬性與欄位存取

實體屬性的存取預設會使用 屬性的備份欄位。 這是有效的,並且避免從呼叫屬性 getter 和 setter 觸發副作用。 例如,以下是延遲載入如何避免觸發無限迴圈的方法。 如需在模型中設定支援字段的詳細資訊,請參閱 支援字段

有時候,EF Core 在修改屬性值時可能會產生副作用。 例如,當數據系結至實體時,設定屬性可能會產生通知給使用者介面,而這在直接設定欄位值時不會發生。 您可以藉由變更 PropertyAccessMode 來達成此目的。

屬性存取模式 FieldPreferField 使 EF Core 透過其備用欄位存取屬性值。 同樣地, Property 而且 PreferProperty 會導致 EF Core 透過其 getter 和 setter 存取屬性值。

如果使用 FieldProperty 且 EF Core 無法通過欄位或屬性 getter/setter 分別存取值,則 EF Core 會擲回例外狀況。 這可確保 EF Core 在您預期時始終使用字段/屬性存取。

另一方面,如果無法使用慣用的存取權,PreferField 模式和 PreferProperty 模式將會分別改為使用屬性或備份欄位。 PreferField 是預設值。 這表示 EF Core 會在可以時使用字段,但如果屬性必須透過其 getter 或 setter 進行存取,則不會失敗。

FieldDuringConstructionPreferFieldDuringConstruction 將 EF Core 配置為 僅在創建實體實例時使用備用欄位。 這可讓查詢在沒有 getter 和 setter 副作用的情況下執行,而 EF Core 稍後的屬性變更會導致這些副作用。

下表摘要說明不同的屬性存取模式:

屬性存取模式 偏好 偏好設定創建實體 後備 建立實體的後援
Field 領域 領域 拋出 拋出
Property 房產 房產 拋出 拋出
PreferField 領域 領域 房產 房產
PreferProperty 房產 房產 領域 領域
FieldDuringConstruction 房產 領域 領域 拋出
PreferFieldDuringConstruction 房產 領域 領域 房產

暫存值

EF Core 會在追蹤將會有由資料庫在呼叫 SaveChanges 時產生的實際索引鍵值的新實體時,建立暫時性的索引鍵值。 如需如何使用這些暫存值的概觀,請參閱 EF Core 中的變更追蹤

存取暫存值

暫存值會儲存在變更追蹤器中,而不會直接設定為實體實例。 不過,這些暫存值 會在 使用各種機制 存取追蹤實體時公開。 例如,下列程式代碼會使用 EntityEntry.CurrentValues存取暫存值:

using var context = new BlogsContext();

var blog = new Blog { Name = ".NET Blog" };

context.Add(blog);

Console.WriteLine($"Blog.Id set on entity is {blog.Id}");
Console.WriteLine($"Blog.Id tracked by EF is {context.Entry(blog).Property(e => e.Id).CurrentValue}");

此程式代碼的輸出如下:

Blog.Id set on entity is 0
Blog.Id tracked by EF is -2147482643

PropertyEntry.IsTemporary 可用來檢查暫存值。

操作暫存值

有時明確處理暫存值很有用。 例如,可能會在 Web 用戶端上建立新實體的集合,然後串行化回伺服器。 外鍵值是設定這些實體之間關聯性的其中一種方式。 下列程式碼使用此方法,透過外鍵將新實體的圖形建立關聯,同時在呼叫 SaveChanges 時仍允許生成真實鍵值。

var blogs = new List<Blog> { new Blog { Id = -1, Name = ".NET Blog" }, new Blog { Id = -2, Name = "Visual Studio Blog" } };

var posts = new List<Post>
{
    new Post
    {
        Id = -1,
        BlogId = -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,
        BlogId = -2,
        Title = "Disassembly improvements for optimized managed debugging",
        Content = "If you are focused on squeezing out the last bits of performance for your .NET service or..."
    }
};

using var context = new BlogsContext();

foreach (var blog in blogs)
{
    context.Add(blog).Property(e => e.Id).IsTemporary = true;
}

foreach (var post in posts)
{
    context.Add(post).Property(e => e.Id).IsTemporary = true;
}

Console.WriteLine(context.ChangeTracker.DebugView.LongView);

await context.SaveChangesAsync();

Console.WriteLine(context.ChangeTracker.DebugView.LongView);

請注意:

  • 負數用作暫時的索引鍵值;這不是必要的,但這是防止索引鍵衝突的一種常見慣例。
  • Post.BlogId FK 屬性會指派與相關聯部落格的 PK 相同的負值。
  • PK 值在追蹤每個實體之後會被標示為暫時性的,方式是設定 IsTemporary。 這是必要的,因為應用程式提供的任何索引鍵值都假設為實際索引鍵值。

在呼叫 SaveChanges 之前查看 變更追蹤器偵錯檢視,顯示 PK 值標示為暫時性,且文章會與正確的部落格相關聯,包括修正導覽。

Blog {Id: -2} Added
  Id: -2 PK Temporary
  Name: 'Visual Studio Blog'
  Posts: [{Id: -2}]
Blog {Id: -1} Added
  Id: -1 PK Temporary
  Name: '.NET Blog'
  Posts: [{Id: -1}]
Post {Id: -2} Added
  Id: -2 PK Temporary
  BlogId: -2 FK
  Content: 'If you are focused on squeezing out the last bits of perform...'
  Title: 'Disassembly improvements for optimized managed debugging'
  Blog: {Id: -2}
  Tags: []
Post {Id: -1} Added
  Id: -1 PK Temporary
  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}

呼叫 SaveChanges之後,這些暫存值已由資料庫所產生的實際值所取代:

Blog {Id: 1} Unchanged
  Id: 1 PK
  Name: '.NET Blog'
  Posts: [{Id: 1}]
Blog {Id: 2} Unchanged
  Id: 2 PK
  Name: 'Visual Studio Blog'
  Posts: [{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}
  Tags: []
Post {Id: 2} Unchanged
  Id: 2 PK
  BlogId: 2 FK
  Content: 'If you are focused on squeezing out the last bits of perform...'
  Title: 'Disassembly improvements for optimized managed debugging'
  Blog: {Id: 2}
  Tags: []

使用預設值

EF Core 允許在呼叫 SaveChanges 時,從資料庫取得其預設值。 與產生的索引鍵值一樣,如果尚未明確設定任何值,EF Core 只會使用資料庫中的預設值。 例如,請考慮下列實體類型:

public class Token
{
    public int Id { get; set; }
    public string Name { get; set; }
    public DateTime ValidFrom { get; set; }
}

屬性 ValidFrom 已設定為從資料庫取得預設值:

modelBuilder
    .Entity<Token>()
    .Property(e => e.ValidFrom)
    .HasDefaultValueSql("CURRENT_TIMESTAMP");

插入此類型的實體時,除非已設定明確的值,否則 EF Core 會讓資料庫產生值。 例如:

using var context = new BlogsContext();

context.AddRange(
    new Token { Name = "A" },
    new Token { Name = "B", ValidFrom = new DateTime(1111, 11, 11, 11, 11, 11) });

await context.SaveChangesAsync();

Console.WriteLine(context.ChangeTracker.DebugView.LongView);

查看 變更追蹤器偵錯檢視 會顯示資料庫所產生的第一個令牌 ValidFrom ,而第二個令牌則使用明確設定的值:

Token {Id: 1} Unchanged
  Id: 1 PK
  Name: 'A'
  ValidFrom: '12/30/2020 6:36:06 PM'
Token {Id: 2} Unchanged
  Id: 2 PK
  Name: 'B'
  ValidFrom: '11/11/1111 11:11:11 AM'

備註

使用資料庫預設值需要資料庫欄位已設定預設值限制。 使用 HasDefaultValueSqlHasDefaultValue 時,EF Core 移轉會自動完成此動作。 當不使用 EF Core 遷移時,請務必透過其他方法在資料欄上建立預設約束。

使用可為 Null 的屬性

EF Core 能夠藉由比較屬性值與該類型的 CLR 預設值,判斷是否已設定屬性。 這在大部分情況下都運作良好,但表示CLR預設值無法明確插入資料庫中。 例如,請考慮具有整數屬性的實體:

public class Foo1
{
    public int Id { get; set; }
    public int Count { get; set; }
}

其中該屬性設定為具有 -1 的資料庫預設值:

modelBuilder
    .Entity<Foo1>()
    .Property(e => e.Count)
    .HasDefaultValue(-1);

其目的是,每當未設定明確值時,就會使用 -1 的預設值。 不過,EF Core無法區分將值設定為0(整數的CLR預設值)與未設定任何值,因此無法為此屬性插入0。 例如:

using var context = new BlogsContext();

var fooA = new Foo1 { Count = 10 };
var fooB = new Foo1 { Count = 0 };
var fooC = new Foo1();

context.AddRange(fooA, fooB, fooC);
await context.SaveChangesAsync();

Debug.Assert(fooA.Count == 10);
Debug.Assert(fooB.Count == -1); // Not what we want!
Debug.Assert(fooC.Count == -1);

請注意,明確設定為0的實例 Count 仍會從資料庫取得預設值,這不是我們預期的情況。 處理此作業的簡單方式是讓 Count 屬性可為 Null:

public class Foo2
{
    public int Id { get; set; }
    public int? Count { get; set; }
}

這會使 CLR 預設為 null,而不是 0,這表示現在會在明確設定時插入 0:

using var context = new BlogsContext();

var fooA = new Foo2 { Count = 10 };
var fooB = new Foo2 { Count = 0 };
var fooC = new Foo2();

context.AddRange(fooA, fooB, fooC);
await context.SaveChangesAsync();

Debug.Assert(fooA.Count == 10);
Debug.Assert(fooB.Count == 0);
Debug.Assert(fooC.Count == -1);

使用可為 Null 的備份欄位

使屬性可為 Null 的問題在於,在定義域模型中,該屬性可能在概念上不可為 Null。 因此,將屬性強制設為可為 Null 可能會影響模型的完整性。

屬性可以保留為不可為 Null,只有支援欄位可以設為可為 Null。 例如:

public class Foo3
{
    public int Id { get; set; }

    private int? _count;

    public int Count
    {
        get => _count ?? -1;
        set => _count = value;
    }
}

如果屬性明確設定為 0,則允許插入 CLR 預設值 (0),而不需要在定義域模型中將屬性公開為可為 Null。 例如:

using var context = new BlogsContext();

var fooA = new Foo3 { Count = 10 };
var fooB = new Foo3 { Count = 0 };
var fooC = new Foo3();

context.AddRange(fooA, fooB, fooC);
await context.SaveChangesAsync();

Debug.Assert(fooA.Count == 10);
Debug.Assert(fooB.Count == 0);
Debug.Assert(fooC.Count == -1);

布爾屬性的可為 Null 支持欄位

當使用具有由儲存庫生成的預設值的布林屬性時,此模式特別有用。 因為 CLR 預設值 bool 為「false」,表示無法使用正常方式明確插入「false」。 我們以 User 實體類型為例:

public class User
{
    public int Id { get; set; }
    public string Name { get; set; }

    private bool? _isAuthorized;

    public bool IsAuthorized
    {
        get => _isAuthorized ?? true;
        set => _isAuthorized = value;
    }
}

屬性 IsAuthorized 會設定為 「true」 的資料庫預設值:

modelBuilder
    .Entity<User>()
    .Property(e => e.IsAuthorized)
    .HasDefaultValue(true);

屬性 IsAuthorized 可以在插入之前明確設定為 「true」 或 「false」,或者可以保留未設定,在此情況下會使用資料庫預設值:

using var context = new BlogsContext();

var userA = new User { Name = "Mac" };
var userB = new User { Name = "Alice", IsAuthorized = true };
var userC = new User { Name = "Baxter", IsAuthorized = false }; // Always deny Baxter access!

context.AddRange(userA, userB, userC);

await context.SaveChangesAsync();

使用 SQLite 時,SaveChanges 的輸出會顯示 Mac 的資料庫預設值,而明確值則設定為 Alice 和 Baxter:

-- Executed DbCommand (0ms) [Parameters=[@p0='Mac' (Size = 3)], CommandType='Text', CommandTimeout='30']
INSERT INTO "User" ("Name")
VALUES (@p0);
SELECT "Id", "IsAuthorized"
FROM "User"
WHERE changes() = 1 AND "rowid" = last_insert_rowid();

-- Executed DbCommand (0ms) [Parameters=[@p0='True' (DbType = String), @p1='Alice' (Size = 5)], CommandType='Text', CommandTimeout='30']
INSERT INTO "User" ("IsAuthorized", "Name")
VALUES (@p0, @p1);
SELECT "Id"
FROM "User"
WHERE changes() = 1 AND "rowid" = last_insert_rowid();

-- Executed DbCommand (0ms) [Parameters=[@p0='False' (DbType = String), @p1='Baxter' (Size = 6)], CommandType='Text', CommandTimeout='30']
INSERT INTO "User" ("IsAuthorized", "Name")
VALUES (@p0, @p1);
SELECT "Id"
FROM "User"
WHERE changes() = 1 AND "rowid" = last_insert_rowid();

僅限架構預設值

有時候,EF Core 移轉會在資料庫架構中建立預設值,但這些值不會在插入操作中被 EF Core 使用。 您可以藉由將 屬性設定為 PropertyBuilder.ValueGeneratedNever ,以達成此目的,例如:

modelBuilder
    .Entity<Bar>()
    .Property(e => e.Count)
    .HasDefaultValue(-1)
    .ValueGeneratedNever();