其他變更追蹤功能
本檔涵蓋涉及變更追蹤的雜項功能和案例。
提示
本檔假設瞭解實體狀態和 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 提供 、 Update
、 Attach
和 Remove
的 Add
替代版本,以接受單一呼叫中的多個實例。 這些方法分別為 AddRange 、 UpdateRange 、 AttachRange 和 RemoveRange 。
為了方便起見,會提供這些方法。 使用 「range」 方法的功能與對等的非範圍方法的多個呼叫相同。 這兩種方法之間沒有顯著的效能差異。
注意
這與 EF6 不同,其中 AddRange
和 Add
都會自動呼叫 DetectChanges
,但多次呼叫 Add
會導致 DetectChanges 多次呼叫,而不是呼叫一次。 這讓 AddRange
EF6 更有效率。 在 EF Core 中,這兩種方法都不會自動呼叫 DetectChanges
。
DbCoNtext 與 DbSet 方法
許多方法,包括 Add
、 Attach
Update
和 ,在 和 Remove
DbContext 上 DbSet<TEntity> 都有 實作。 這些方法對一般實體類型的行為 完全相同 。 這是因為實體的 CLR 類型對應到 EF Core 模型中只有一個實體類型。 因此,CLR 類型會完整定義實體放入模型中的位置,因此可以隱含判斷要使用的 DbSet。
此規則的例外狀況是使用共用類型實體類型,主要用於多對多聯結實體。 使用共用類型實體類型時,必須先針對正在使用的 EF Core 模型類型建立 DbSet。 接著,可以在 DbSet 上使用 、 Update
、 Attach
和 Remove
等 Add
方法,而不用任何模棱兩可的 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 = context.Posts.Single(e => e.Id == 3);
var tag = context.Tags.Single(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);
context.SaveChanges();
請注意, DbContext.Set<TEntity>(String) 用來建立 PostTag
實體類型的 DbSet。 接著,您可以使用這個 DbSet 來呼叫 Add
與新的聯結實體實例。
重要
依慣例用於聯結實體類型的 CLR 類型在未來版本中可能會變更,以改善效能。 除非已在上述程式碼中明確設定為 , Dictionary<string, int>
否則請勿相依于任何特定的聯結實體類型。
屬性與欄位存取
實體屬性的存取預設會使用 屬性的備份欄位。 這是有效率的,可避免從呼叫屬性 getter 和 setter 觸發副作用。 例如,這是延遲載入如何避免觸發無限迴圈。 如需在模型中設定支援欄位的詳細資訊,請參閱 備份欄位 。
有時候,EF Core 在修改屬性值時可能會產生副作用。 例如,當資料系結至實體時,設定屬性可能會產生通知給 U.I。直接設定欄位時不會發生。 您可以藉由變更 PropertyAccessMode 下列專案的 來達成此目的:
- 使用 模型中的所有實體類型 ModelBuilder.UsePropertyAccessMode
- 使用 之特定實體類型的所有屬性和導覽 EntityTypeBuilder<TEntity>.UsePropertyAccessMode
- 使用 的特定屬性 PropertyBuilder.UsePropertyAccessMode
- 使用 的特定導覽 NavigationBuilder.UsePropertyAccessMode
屬性存取模式 Field
,並 PreferField
會導致 EF Core 透過其備份欄位存取屬性值。 同樣地, Property
而且 PreferProperty
會導致 EF Core 透過其 getter 和 setter 存取屬性值。
如果使用 或 Property
,且 Field
EF Core 無法透過欄位或屬性 getter/setter 分別存取值,則 EF Core 會擲回例外狀況。 這可確保 EF Core 一律會在您認為時使用欄位/屬性存取。
另一方面,如果無法使用慣用的存取權,和 PreferField
PreferProperty
模式將會分別回復為使用 屬性或備份欄位。 PreferField
是預設值。 這表示 EF Core 會在可以時使用欄位,但如果屬性必須透過其 getter 或 setter 進行存取,則不會失敗。
FieldDuringConstruction
並將 PreferFieldDuringConstruction
EF Core 設定為只在建立實體實例 時使用備份欄位 。 這可讓查詢在沒有 getter 和 setter 副作用的情況下執行,而 EF Core 稍後的屬性變更會導致這些副作用。
下表摘要說明不同的屬性存取模式:
PropertyAccessMode | 偏好 | 建立實體的喜好設定 | 後援 | 建立實體的後援 |
---|---|---|---|---|
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);
context.SaveChanges();
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) });
context.SaveChanges();
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'
注意
使用資料庫預設值需要資料庫資料行已設定預設值條件約束。 使用 或 HasDefaultValue 時 HasDefaultValueSql ,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 的預設值。 不過,將值設定為 0(整數的 CLR 預設值)與 EF Core 不設定任何值不區分,這表示無法插入此屬性的 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);
context.SaveChanges();
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);
context.SaveChanges();
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);
context.SaveChanges();
Debug.Assert(fooA.Count == 10);
Debug.Assert(fooB.Count == 0);
Debug.Assert(fooC.Count == -1);
布林屬性的可為 Null 支援欄位
搭配存放區產生的預設值使用 bool 屬性時,此模式特別有用。 由於 的 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);
context.SaveChanges();
使用 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 移轉所建立的資料庫架構中,若未使用這些值進行插入,就很有用。 您可以藉由將 屬性設定為 PropertyBuilder.ValueGeneratedNever ,以達成此目的,例如:
modelBuilder
.Entity<Bar>()
.Property(e => e.Count)
.HasDefaultValue(-1)
.ValueGeneratedNever();