本文件涵蓋涉及變更追蹤的雜項功能和案例。
小提示
本文件假設您已瞭解實體狀態以及 EF Core 變更追蹤的基本概念。 如需這些主題的詳細資訊,請參閱 EF Core 中的變更追蹤 。
小提示
您可以從 GitHub 下載範例程式代碼,以執行並偵錯此檔案中的所有程式代碼。
Add 與 AddAsync
Entity Framework Core (EF Core) 每當使用該方法可能會導致資料庫互動時,都提供異步方法。 使用不支援高效能異步存取的資料庫時,也會提供同步方法以避免額外負荷。
DbContext.Add 和 DbSet<TEntity>.Add 通常不會存取資料庫,因為這些方法原本就只是開始追蹤實體。 不過,某些形式的值產生 可能會 存取資料庫,以產生鍵值。 執行這項動作並隨附於 EF Core 的唯一值產生器是 HiLoValueGenerator<TValue>。 使用這個發電機是很少見的;預設狀態下,它不會被配置。 這表示絕大多數應用程式都應該使用 Add,而不是 AddAsync。
其他類似的方法,例如 Update、 Attach和 Remove 沒有異步多載,因為它們永遠不會產生新的索引鍵值,因此永遠不需要存取資料庫。
AddRange、UpdateRange、AttachRange和 RemoveRange
DbSet<TEntity>和 DbContext 提供、Add、 Update和 Attach 的Remove替代版本,以接受單一呼叫中的多個實例。 這些方法分別為 AddRange、 UpdateRange、 AttachRange和 RemoveRange 。
為了方便起見,會提供這些方法。 使用 「range」 方法的功能與對等的非範圍方法的多個呼叫相同。 這兩種方法之間沒有顯著的效能差異。
備註
這與 EF6 不同,其中 AddRange 和 Add 都會自動呼叫 DetectChanges,但多次呼叫 Add 會導致 DetectChanges 多次呼叫,而不是呼叫一次。 這讓 EF6 的 AddRange 更有效率。 在EF Core 中,這兩種方法都不會自動呼叫 DetectChanges。
DbContext 與 DbSet 方法
許多方法,包括 Add、Update、Attach 和 Remove,在 DbSet<TEntity> 和 DbContext 上都有實作。 這些方法對一般實體類型的行為 完全相同 。 這是因為實體的 CLR 類型唯獨對應於 EF Core 模型中的唯一實體類型。 因此,CLR 類型會完整定義實體放入模型中的位置,因此可以隱含判斷要使用的 DbSet。
此規則的例外狀況是使用共用類型實體類型,主要用於多對多聯結實體。 使用共用類型實體類型時,必須先針對正在使用的EF Core 模型類型建立 DbSet。 接著,您可以在 DbSet 上使用 Add、Update、Attach 和 Remove 方法,而不會有任何 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 來達成此目的。
- 模型中的所有實體類型都使用 ModelBuilder.UsePropertyAccessMode
- 使用 EntityTypeBuilder<TEntity>.UsePropertyAccessMode 的特定實體類型的所有屬性和導覽
- 使用PropertyBuilder.UsePropertyAccessMode的特定屬性
- 特定導覽使用NavigationBuilder.UsePropertyAccessMode
屬性存取模式 Field 和 PreferField 使 EF Core 透過其備用欄位存取屬性值。 同樣地, Property 而且 PreferProperty 會導致 EF Core 透過其 getter 和 setter 存取屬性值。
如果使用 Field 或 Property 且 EF Core 無法通過欄位或屬性 getter/setter 分別存取值,則 EF Core 會擲回例外狀況。 這可確保 EF Core 在您預期時始終使用字段/屬性存取。
另一方面,如果無法使用慣用的存取權,PreferField 模式和 PreferProperty 模式將會分別改為使用屬性或備份欄位。
PreferField 是預設值。 這表示 EF Core 會在可以時使用字段,但如果屬性必須透過其 getter 或 setter 進行存取,則不會失敗。
FieldDuringConstruction 和 PreferFieldDuringConstruction 將 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.BlogIdFK 屬性會指派與相關聯部落格的 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'
備註
使用資料庫預設值需要資料庫欄位已設定預設值限制。 使用 HasDefaultValueSql 或 HasDefaultValue 時,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();