值比較子

提示

此檔中的程式碼可在 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 的預設邏輯運作良好:當快照集時,會依現時複製值,並與類型的內建相等比較進行比較。 實作您自己的值比較子時,請務必考慮深層或淺層比較(以及快照集)邏輯是否適當。

請考慮位元組陣列,它可以任意大。 您可以比較這些專案:

  • 根據參考,只有在使用新的位元組陣列時,才會偵測到差異
  • 藉由深入比較,就會偵測到陣列中位元組的突變

根據預設,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 會將這些運算式插入更複雜的運算式樹狀結構,然後編譯成實體 Shaper 委派。 就概念上講,這類似于編譯器內嵌。 例如,簡單的轉換可能只是在轉換中編譯,而不是呼叫另一個方法來執行轉換。

索引鍵比較子

背景區段涵蓋索引鍵比較可能需要特殊語意的原因。 請務必在主要、主體或外鍵屬性上設定索引鍵時,建立適合索引鍵的比較子。

在相同屬性上需要不同語意的罕見情況下使用 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 現在會比較位元組序列,因此會偵測位元組陣列突變。