小提示
此檔中的程式代碼可在 GitHub 上找到,做為 可執行的範例。
背景
變更追蹤 表示 EF Core 會自動判斷應用程式在載入的實體實例上執行的變更,以便在呼叫 時 SaveChanges 將這些變更儲存回資料庫。 EF Core 通常會在從資料庫載入實例時擷取實例的 快照 集,並將該快照集與交給應用程式的實例 進行比較 來執行此作業。
EF Core 附有內建邏輯,能夠建立快照並比較資料庫中所使用的大部分標準類型,因此通常使用者不需要過於擔心此議題。 不過,當屬性透過 值轉換器對應時,EF Core 必須對可能很複雜的任意使用者類型執行比較。 根據預設,EF Core 會使用類型所定義的預設相等比較(例如 Equals
方法);在快照時,會複製 實值型別 以產生快照,而 參考型別 則保持原樣,使用相同的實例。
如果內建比較行為不合適,使用者可能會提供 值比較子,其中包含快照集、比較和計算哈希碼的邏輯。 例如,下例會設定 List<int>
屬性的值轉換,以便將其轉換為資料庫中的 JSON 字串,並定義一個適當的值比較器:
modelBuilder
.Entity<EntityType>()
.Property(e => e.MyListProperty)
.HasConversion(
v => JsonSerializer.Serialize(v, (JsonSerializerOptions)null),
v => JsonSerializer.Deserialize<List<int>>(v, (JsonSerializerOptions)null),
new ValueComparer<List<int>>(
(c1, c2) => c1.SequenceEqual(c2),
c => c.Aggregate(0, (a, v) => HashCode.Combine(a, v.GetHashCode())),
c => c.ToList()));
如需進一步的詳細 數據,請參閱下面的可變動類別 。
請注意,在解析關係時判斷兩個鍵值是否相同,也會使用數值比較器,如下所述。
淺層與深層比較
對於小型、不可變的實值類型,例如 int
,EF Core 的預設邏輯運作良好:此值會在快照集時複製 as-is,並與類型的內建相等比較進行比較。 實作您自己的值比較子時,請務必考慮使用深層或淺層比較及快照邏輯是否適合。
請考慮位元組陣列,它可以任意大。 這些可以比較:
- 以參考為基準,只有在使用新的位元組陣列時才會偵測到差異。
- 藉由深入比較,就會偵測到陣列中位元組的突變
根據預設,EF Core 會針對非索引鍵位元組陣列使用上述其中一種方法。 也就是說,只有比較引用,而且只有在現有的位元組陣列被新的位元組陣列取代時,才會偵測到變更。 這是一個務實的決策,可避免在執行 SaveChanges 時複製整個陣列並逐個位元組進行比較。 這表示在高效能的情況下,常見的情景是將一幅圖像替換成另一幅圖像。
另一方面,當位元組陣列用來表示二進位索引鍵時,參考相等將無法運作,因為 FK 屬性不太可能設定為與它需要比較的 PK 屬性 相同的實例 。 因此,EF Core 會針對作為索引鍵的位元組陣列使用深層比較,由於二進位索引鍵通常很短,因此不太可能有大幅的效能影響。
請注意,選擇的比較和快照集邏輯必須彼此對應:深層比較需要深度快照集才能正常運作。
簡單不可變類別
請考慮一個屬性,該屬性將使用值轉換器來對應一個簡單且不可變的類別。
public sealed class ImmutableClass
{
public ImmutableClass(int value)
{
Value = value;
}
public int Value { get; }
private bool Equals(ImmutableClass other)
=> Value == other.Value;
public override bool Equals(object obj)
=> ReferenceEquals(this, obj) || obj is ImmutableClass other && Equals(other);
public override int GetHashCode()
=> Value.GetHashCode();
}
modelBuilder
.Entity<MyEntityType>()
.Property(e => e.MyProperty)
.HasConversion(
v => v.Value,
v => new ImmutableClass(v));
此類型的屬性不需要特殊比較或快照集,因為:
- 已覆寫相等,讓不同的實例正確比較
- 此類型是不可變的,因此不可能變更快照集值
因此,在此案例中,EF Core 的預設行為會正常運作。
簡單不可變的結構
簡單結構體的對應也很簡單,不需要特殊的比較子或快照集。
public readonly struct ImmutableStruct
{
public ImmutableStruct(int value)
{
Value = value;
}
public int Value { get; }
}
modelBuilder
.Entity<EntityType>()
.Property(e => e.MyProperty)
.HasConversion(
v => v.Value,
v => new ImmutableStruct(v));
EF Core 內建支援產生已編譯的結構體屬性成員逐一比較。 這意味著結構體不需要為 EF Core 覆寫相等性,但您可能仍然會因為 其他原因選擇這樣做。 此外,不需要特殊快照,因為結構是不可變的,而且始終以成員方式複製。 (這也適用於可變結構,但 一般應避免可變結構。
可變動類別
建議您盡可能使用不可變的類型(類別或結構)搭配值轉換器。 這通常更有效率,而且具有比使用可變動類型更簡潔的語意。 不過,也就是說,通常會使用應用程式無法變更的類型屬性。 例如,對應包含數字清單的屬性:
public List<int> MyListProperty { get; set; }
List<T> 類別:
- 具有參考相等;包含相同值的兩個清單會被視為不同的。
- 可變動;您可以在清單中新增和移除值。
在清單屬性上的一般值轉換可能包括將清單轉為 JSON 格式,或從 JSON 格式轉回清單:
modelBuilder
.Entity<EntityType>()
.Property(e => e.MyListProperty)
.HasConversion(
v => JsonSerializer.Serialize(v, (JsonSerializerOptions)null),
v => JsonSerializer.Deserialize<List<int>>(v, (JsonSerializerOptions)null),
new ValueComparer<List<int>>(
(c1, c2) => c1.SequenceEqual(c2),
c => c.Aggregate(0, (a, v) => HashCode.Combine(a, v.GetHashCode())),
c => c.ToList()));
建構函式 ValueComparer<T> 接受三個運算式:
- 用來檢查是否相等的運算式
- 產生哈希碼的表達式
- 用於捕捉值的表達式
在此情況下,比較是藉由檢查數位序列是否相同來完成。
同樣地,哈希程式代碼是從這個相同的序列建置的。 (請注意,這是可變動值的哈希碼,因此 可能會導致問題。如果可以的話,請改為不可變。
通過複製清單 ToList
來創建快照集。 同樣地,只有當清單要變動時,才需要此專案。 如果可以的話,請保持不可變。
備註
值轉換器和比較子是使用表達式來建構,而不是簡單的委派。 這是因為 EF Core 會將這些表達式插入到更複雜的表達式樹狀結構中,然後編譯成一個可調整實體的委託物件。 就概念上講,這類似於編譯程式內嵌。 例如,簡單的轉換可能只是編譯時內置的轉換,而不是呼叫另一個方法來執行轉換。
鍵比較器
背景區段涵蓋索引鍵比較可能需要特殊語意的原因。 請確保在主要、主體或外鍵屬性上設定鍵值時,建立適合鍵值的比較器。
在相同屬性上需要不同語意的罕見情況下使用 SetKeyValueComparer 。
備註
SetStructuralValueComparer 已經過時。 請改用 SetKeyValueComparer。
覆寫預設比較子
有時候 EF Core 所使用的預設比較可能不合適。 例如,EF Core 根據預設不會偵測到位元組陣列的突變。 您可以藉由在屬性上設定不同的比較子來重設。
modelBuilder
.Entity<EntityType>()
.Property(e => e.MyBytes)
.Metadata
.SetValueComparer(
new ValueComparer<byte[]>(
(c1, c2) => c1.SequenceEqual(c2),
c => c.Aggregate(0, (a, v) => HashCode.Combine(a, v.GetHashCode())),
c => c.ToArray()));
EF Core 現在會比較位元組序列,因此會偵測位元組陣列突變。