每個 DbContext 實例都會追蹤對實體所做的變更。 當 SaveChanges 被呼叫時,這些被追蹤的實體會驅動資料庫的變更。 本文件涵蓋 EF Core 的變更追蹤,並假設讀者已瞭解 Entity Framework Core(EF Core)的實體狀態以及變更追蹤的基本概念。
追蹤屬性和關聯性變更需要 DbContext 能夠偵測這些變更。 本文件涵蓋此偵測的發生方式,以及如何使用屬性通知或變更追蹤 Proxy 來強制立即偵測變更。
小提示
您可以從 GitHub 下載範例程式代碼,以執行並偵錯此檔案中的所有程式代碼。
快照變更追蹤
預設情況下,EF Core 會在 DbContext 實例第一次追蹤每個實體的屬性值時,為其建立快照集。 然後,在此快照集中儲存的值會與實體的目前值進行比較,以判斷哪些屬性值已變更。
呼叫 SaveChanges 以確保在將更新傳送至資料庫之前偵測到所有變更的值時,就會發生這項變更偵測。 不過,偵測變更也會在其他時間發生,以確保應用程式正在使用 up-to日期追蹤資訊。 強制偵測變更可以隨時透過呼叫 ChangeTracker.DetectChanges() 來達成。
當需要進行變更偵測時
當屬性或導覽已變更 而不使用 EF Core 進行這項變更時,需要偵測變更。 例如,請考慮載入部落格和文章,然後對這些實體進行變更:
using var context = new BlogsContext();
var blog = await context.Blogs.Include(e => e.Posts).FirstAsync(e => e.Name == ".NET Blog");
// Change a property value
blog.Name = ".NET Blog (Updated!)";
// Add a new entity to a navigation
blog.Posts.Add(
new Post
{
Title = "What’s next for System.Text.Json?", Content = ".NET 5.0 was released recently and has come with many..."
});
Console.WriteLine(context.ChangeTracker.DebugView.LongView);
context.ChangeTracker.DetectChanges();
Console.WriteLine(context.ChangeTracker.DebugView.LongView);
在呼叫 之前查看ChangeTracker.DetectChanges(),顯示所做的變更尚未偵測到,因此不會反映在實體狀態和修改的屬性數據中:
Blog {Id: 1} Unchanged
Id: 1 PK
Name: '.NET Blog (Updated!)' Originally '.NET Blog'
Posts: [{Id: 1}, {Id: 2}, <not found>]
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}
具體來說,部落格項目的狀態仍然是 Unchanged,而且新文章不會顯示為追蹤的實體。 (精明會注意到屬性會報告其新值,即使 EF Core 尚未偵測到這些變更也一樣。這是因為偵錯檢視正直接從實體實例讀取目前的值。
與呼叫 DetectChanges 之後的偵錯檢視形成對比:
Blog {Id: 1} Modified
Id: 1 PK
Name: '.NET Blog (Updated!)' Modified Originally '.NET Blog'
Posts: [{Id: 1}, {Id: 2}, {Id: -2147482643}]
Post {Id: -2147482643} Added
Id: -2147482643 PK Temporary
BlogId: 1 FK
Content: '.NET 5.0 was released recently and has come with many...'
Title: 'What's next for System.Text.Json?'
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...'
Title: 'Announcing F# 5'
Blog: {Id: 1}
現在,部落格已正確標示為 Modified ,且已偵測到新文章,並追蹤為 Added。
在本節開始時,我們指出在不使用 EF Core 進行變更時,需要偵測變更。 這就是上述程式代碼中發生的情況。 也就是說,屬性和導覽的變更 會直接在實體實例上進行,而不是使用任何 EF Core 方法。
這與下列程式代碼形成對比,這些程式代碼會以相同方式修改實體,但這次使用 EF Core 方法:
using var context = new BlogsContext();
var blog = await context.Blogs.Include(e => e.Posts).FirstAsync(e => e.Name == ".NET Blog");
// Change a property value
context.Entry(blog).Property(e => e.Name).CurrentValue = ".NET Blog (Updated!)";
// Add a new entity to the DbContext
context.Add(
new Post
{
Blog = blog,
Title = "What’s next for System.Text.Json?",
Content = ".NET 5.0 was released recently and has come with many..."
});
Console.WriteLine(context.ChangeTracker.DebugView.LongView);
在此情況下,變更追蹤器偵錯檢視會顯示所有實體狀態和屬性修改都是已知的,即使偵測到變更並未發生。 這是因為 PropertyEntry.CurrentValue 是 EF Core 方法,這表示 EF Core 會立即知道此方法所做的變更。 同樣地,呼叫 DbContext.Add 可讓EF Core立即知道新的實體,並適當地追蹤它。
小提示
請勿嘗試避免一律使用 EF Core 方法來進行實體變更來偵測變更。 這樣做通常比較繁瑣,而且執行效能比以正常方式對實體進行變更還少。 本文件的意圖是通知何時需要偵測變更,以及何時不偵測變更。 其意圖不是為了鼓勵人們避免進行變更偵測。
自動偵測變更的方法
DetectChanges() 自動由可能會影響結果的方法所呼叫。 方法如下:
- DbContext.SaveChanges 和 DbContext.SaveChangesAsync,以確保在更新資料庫之前偵測到所有變更。
- ChangeTracker.Entries() 和 ChangeTracker.Entries<TEntity>(),以確保實體狀態和修改的屬性 up-to-date。
- ChangeTracker.HasChanges(),以確保結果正確無誤。
- ChangeTracker.CascadeChanges(),確保主要/父實體在串聯之前具有正確的狀態。
- DbSet<TEntity>.Local,以確保追蹤的圖表符合日期up-to。
也有一些地方只會在單一實體實例上偵測變更,而不是在追蹤實體的整個圖表上發生變更。 這些地方包括:
- 使用 DbContext.Entry 時,若要確保實體的狀態和已修改的屬性為 up-to-date。
- 使用 EntityEntry 方法,例如
Property、Collection、Reference或Member,來確保屬性修改、目前值等符合up-to日期。 - 刪除相依/子實體的原因是因為已切斷了必需的關聯。 這會偵測實體何時不應該刪除,因為它已重新指定父元素。
您可以透過呼叫 EntityEntry.DetectChanges(),明確觸發對單一實體的本地變更偵測。
備註
本機偵測變更可能會遺漏完整偵測發現的某些變更。 當因未偵測到對其他實體所做的變更而產生的串連動作對有問題的實體造成影響時,就會發生這種情況。 在這種情況下,應用程式可能需要藉由明確呼叫 ChangeTracker.DetectChanges()來強制完整掃描所有實體。
停用自動變更偵測
偵測變更的效能對大多數應用程式而言並不是瓶頸。 不過,針對追蹤數千個實體的某些應用程式,偵測變更可能會成為效能問題。 (確切的數字將取決於許多因素,例如實體中的屬性數目。因此,您可以使用ChangeTracker.AutoDetectChangesEnabled來停用自動偵測變更。) 例如,請考慮在具有承載的多對多關聯性中處理聯結實體:
public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
{
foreach (var entityEntry in ChangeTracker.Entries<PostTag>()) // Detects changes automatically
{
if (entityEntry.State == EntityState.Added)
{
entityEntry.Entity.TaggedBy = "ajcvickers";
entityEntry.Entity.TaggedOn = DateTime.Now;
}
}
try
{
ChangeTracker.AutoDetectChangesEnabled = false;
return await base.SaveChangesAsync(cancellationToken); // Avoid automatically detecting changes again here
}
finally
{
ChangeTracker.AutoDetectChangesEnabled = true;
}
}
如上一節所述, ChangeTracker.Entries<TEntity>() 並 DbContext.SaveChanges 自動偵測變更。 不過,呼叫 Entries 之後,程式代碼就不會進行任何實體或屬性狀態變更。 (在 [新增實體] 上設定一般屬性值不會造成任何狀態變更。因此,當呼叫Base SaveChanges方法時,程式代碼會停用不必要的自動變更偵測。 程序代碼也會使用 try/finally 區塊來確保即使 SaveChanges 失敗,還是會還原預設設定。
小提示
請勿假設您的程式代碼必須停用自動變更偵測,才能正常執行。 只有在分析應用程式追蹤許多實體時,才需要這樣做,這表示變更偵測的效能是個問題。
偵測變更和值轉換
若要配合實體類型使用快照集變更追蹤,EF Core 必須能夠:
- 追蹤實體時,建立每個屬性值的快照集
- 將此值與屬性的目前值進行比較
- 為該值產生哈希碼
由 EF Core 自動處理那些可以直接映射到資料庫的類型。 不過,當 值轉換器用來對應屬性時,該轉換器必須指定如何執行這些動作。 這是使用值比較子達成的,而且會在 值比較子 文件中詳細說明。
通知實體
建議針對大多數應用程式使用快照變更追蹤。 不過,追蹤許多實體和/或對這些實體進行大量變更的應用程式,可能會受益於實作可以在屬性和導覽值改變時自動通知 EF Core 的實體。 這些稱為「通知實體」。
執行通知元件
通知實體會使用 INotifyPropertyChanging 和 INotifyPropertyChanged 介面,這些介面是 .NET 基類庫(BCL)的一部分。 這些介面會定義在變更屬性值之前和之後必須引發的事件。 例如:
public class Blog : INotifyPropertyChanging, INotifyPropertyChanged
{
public event PropertyChangingEventHandler PropertyChanging;
public event PropertyChangedEventHandler PropertyChanged;
private int _id;
public int Id
{
get => _id;
set
{
PropertyChanging?.Invoke(this, new PropertyChangingEventArgs(nameof(Id)));
_id = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Id)));
}
}
private string _name;
public string Name
{
get => _name;
set
{
PropertyChanging?.Invoke(this, new PropertyChangingEventArgs(nameof(Name)));
_name = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Name)));
}
}
public IList<Post> Posts { get; } = new ObservableCollection<Post>();
}
此外,任何集合導航都必須實作INotifyCollectionChanged; 在上述範例中,這是透過使用一個ObservableCollection<T> 文章來滿足的。 EF Core 提供一個 ObservableHashSet<T> 實作,這個實作以犧牲穩定順序為代價,進行更有效率的查找。
大部分的通知代碼通常會移至未映射的基類。 例如:
public class Blog : NotifyingEntity
{
private int _id;
public int Id
{
get => _id;
set => SetWithNotify(value, out _id);
}
private string _name;
public string Name
{
get => _name;
set => SetWithNotify(value, out _name);
}
public IList<Post> Posts { get; } = new ObservableCollection<Post>();
}
public abstract class NotifyingEntity : INotifyPropertyChanging, INotifyPropertyChanged
{
protected void SetWithNotify<T>(T value, out T field, [CallerMemberName] string propertyName = "")
{
NotifyChanging(propertyName);
field = value;
NotifyChanged(propertyName);
}
public event PropertyChangingEventHandler PropertyChanging;
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyChanged(string propertyName)
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
private void NotifyChanging(string propertyName)
=> PropertyChanging?.Invoke(this, new PropertyChangingEventArgs(propertyName));
}
設定通知實體
EF Core 無法驗證 INotifyPropertyChanging 或 INotifyPropertyChanged 是否已完全實作,以用於搭配 EF Core。 特別是,這些介面的一些用法只會在特定屬性上使用通知,而不是 EF Core 所需的所有屬性(包括導覽)。 因此,EF Core 不會自動連結至這些事件。
相反地,EF Core 必須設定為使用這些通知實體。 這通常是透過呼叫 ModelBuilder.HasChangeTrackingStrategy來針對所有實體類型完成。 例如:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.HasChangeTrackingStrategy(ChangeTrackingStrategy.ChangingAndChangedNotifications);
}
(也可以使用 EntityTypeBuilder.HasChangeTrackingStrategy 對不同的實體類型設定不同的策略,但這通常會適得其反,因為那些不是通知實體的類型仍然需要 DetectChanges。)
完整通知變更追蹤需要同時實作INotifyPropertyChanging 和 INotifyPropertyChanged。 這可讓原始值儲存在屬性值變更之前,避免EF Core 在追蹤實體時建立快照集的需求。 僅實作 INotifyPropertyChanged 的實體類型也可以與 EF Core 搭配使用。 在此情況下,EF 仍會在追蹤實體以追蹤原始值時建立快照集,但接著會使用通知立即偵測變更,而不需要呼叫 DetectChanges。
下表摘要說明不同的 ChangeTrackingStrategy 值。
| 變更追踪策略 | 所需的介面 | 需要偵測變更 | 快照的原始值 |
|---|---|---|---|
| 快照 | 沒有 | 是的 | 是的 |
| 變更通知 | INotifyPropertyChanged | 否 | 是的 |
| 更改和已更改通知 | INotifyPropertyChanged 和 INotifyPropertyChanging | 否 | 否 |
| 具有原始值的更改和已更改通知 | INotifyPropertyChanged 和 INotifyPropertyChanging | 否 | 是的 |
使用通知實體
通知實體的行為就像任何其他實體一樣,不同之處在於對實體實例進行變更不需要呼叫 ChangeTracker.DetectChanges() 來偵測這些變更。 例如:
using var context = new BlogsContext();
var blog = await context.Blogs.Include(e => e.Posts).FirstAsync(e => e.Name == ".NET Blog");
// Change a property value
blog.Name = ".NET Blog (Updated!)";
// Add a new entity to a navigation
blog.Posts.Add(
new Post
{
Title = "What’s next for System.Text.Json?", Content = ".NET 5.0 was released recently and has come with many..."
});
Console.WriteLine(context.ChangeTracker.DebugView.LongView);
使用一般實體時, 變更追蹤器偵錯檢視 顯示,在呼叫 DetectChanges 之前,不會偵測到這些變更。 查看使用通知實體時的偵錯檢視,顯示已立即偵測到這些變更:
Blog {Id: 1} Modified
Id: 1 PK
Name: '.NET Blog (Updated!)' Modified
Posts: [{Id: 1}, {Id: 2}, {Id: -2147482643}]
Post {Id: -2147482643} Added
Id: -2147482643 PK Temporary
BlogId: 1 FK
Content: '.NET 5.0 was released recently and has come with many...'
Title: 'What's next for System.Text.Json?'
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...'
Title: 'Announcing F# 5'
Blog: {Id: 1}
變更追蹤代理
EF Core 可以動態產生實作 INotifyPropertyChanging 和 INotifyPropertyChanged 的代理類型。 這需要安裝 Microsoft.EntityFrameworkCore.Proxies NuGet 套件,並啟用變更追蹤 Proxy。例如:
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
=> optionsBuilder.UseChangeTrackingProxies();
建立動態 Proxy 涉及建立一個新的動態 .NET 類型(使用 Castle.Core 代理實作),其繼承自該實體類型,並覆寫所有的屬性設值器。 因此,Proxy 的實體類型必須是可以被繼承的型別,並且必須具有可覆寫的屬性。 此外,明確建立的集合導覽需要實作 INotifyCollectionChanged 例如:
public class Blog
{
public virtual int Id { get; set; }
public virtual string Name { get; set; }
public virtual IList<Post> Posts { get; } = new ObservableCollection<Post>();
}
public class Post
{
public virtual int Id { get; set; }
public virtual string Title { get; set; }
public virtual string Content { get; set; }
public virtual int BlogId { get; set; }
public virtual Blog Blog { get; set; }
}
變更追蹤代理工具的一個重要缺點是,在 EF Core 中,必須始終追蹤代理工具的實例,而不會追蹤基礎實體類型的實例。 這是因為基礎實體類型的實例不會產生通知,這表示會遺漏對這些實體所做的變更。
EF Core 會在查詢資料庫時自動建立 Proxy 實例,因此此缺點通常僅限於追蹤新的實體實例。 這些實例必須使用CreateProxy擴展方法來建立,而不是使用的一般方式。 這表示先前範例中的程式代碼現在必須使用CreateProxy:
using var context = new BlogsContext();
var blog = await context.Blogs.Include(e => e.Posts).FirstAsync(e => e.Name == ".NET Blog");
// Change a property value
blog.Name = ".NET Blog (Updated!)";
// Add a new entity to a navigation
blog.Posts.Add(
context.CreateProxy<Post>(
p =>
{
p.Title = "What’s next for System.Text.Json?";
p.Content = ".NET 5.0 was released recently and has come with many...";
}));
Console.WriteLine(context.ChangeTracker.DebugView.LongView);
變更追蹤事件
EF Core 會在第一次追蹤實體時引發 ChangeTracker.Tracked 事件。 未來的實體狀態變更會導致 ChangeTracker.StateChanged 事件。 如需詳細資訊,請參閱 EF Core 中的 .NET 事件 。
備註
當實體第一次被追蹤時,StateChanged事件不會引發,即使狀態已從Detached變更為其他狀態之一。 請務必同時監聽 StateChanged 和 Tracked 事件,以獲得所有相關通知。