共用方式為


數值比較器

小提示

此檔中的程式代碼可在 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 現在會比較位元組序列,因此會偵測位元組陣列突變。