Sdílet prostřednictvím


Porovnávače hodnot

Návod

Kód v tomto dokumentu najdete na GitHubu jako spustitelnou ukázku.

Pozadí

Sledování změn znamená, že EF Core automaticky určuje, jaké změny prováděla aplikace na načtené instanci entity, aby se tyto změny mohly uložit zpět do databáze, když SaveChanges je volána. EF Core to obvykle provádí pořízením snímku instance při načtení z databáze a porovnáním tohoto snímku s instancí předanou aplikaci.

EF Core obsahuje integrovanou logiku pro vytváření snímků a porovnávání většiny standardních typů používaných v databázích, takže se uživatelé obvykle nemusí starat o toto téma. Pokud je však vlastnost mapována prostřednictvím převaděče hodnot, EF Core musí provést porovnání s libovolnými typy uživatelů, což může být složité. EF Core ve výchozím nastavení používá výchozí porovnání rovnosti definované typy (např. metodou Equals ); pro vytváření snímků se zkopírují typy hodnot pro vytvoření snímku, zatímco u referenčních typů nedojde k kopírování a stejná instance se používá jako snímek.

V případech, kdy integrované chování porovnání není vhodné, mohou uživatelé poskytnout porovnávač hodnot, který obsahuje logiku pro vytváření snímků, porovnávání a výpočet hash kódu. Například následující nastaví převod hodnoty pro List<int> vlastnost, která má být převedena na řetězec JSON v databázi, a definuje také odpovídající porovnávač hodnot:

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()));

Další podrobnosti najdete v níže uvedených proměnlivých třídách .

Všimněte si, že porovnávače hodnot se používají také při určování, zda jsou dvě hodnoty klíče stejné při překladu relací; toto je vysvětleno níže.

Mělké vs. hluboké porovnávání

U malých neměnných hodnotových typů, jako intje výchozí logika EF Core, funguje dobře: hodnota se zkopíruje as-is při vytvoření snímku a porovná se s předdefinovaným porovnáním rovnosti typu. Při implementaci vlastního porovnávače hodnot je důležité zvážit, jestli je vhodná logika hloubkového nebo mělkého porovnání (a vytváření snímků).

Zvažte pole bajtů, která mohou být libovolně velká. Můžete je porovnat:

  • Rozdíl je zjištěn pouze tehdy, když se použije nové bajtové pole jako reference.
  • Hloubkové porovnání tak, aby se zjistila mutace bajtů v matici

EF Core ve výchozím nastavení používá první z těchto přístupů pro pole bez klíčových bajtů. To znamená, že se porovnávají pouze odkazy a změna se zjistí pouze v případě, že je existující bajtové pole nahrazeno novým polem. Jedná se o pragmatičtější rozhodnutí, které zabraňuje kopírování celých polí a porovnávání bajtů při provádění SaveChanges. To znamená, že běžný scénář nahrazení, řekněme, jeden obrázek druhým je zpracován výkonným způsobem.

Na druhou stranu by rovnost odkazů nefungovala, když se k reprezentaci binárních klíčů používají pole bajtů, protože je velmi nepravděpodobné, že je vlastnost FK nastavená na stejnou instanci jako vlastnost PK, se kterou je potřeba porovnat. EF Core používá hloubkové porovnání pro bajtová pole fungující jako klíče; toto pravděpodobně nebude mít výrazný výkonový dopad, protože binární klíče jsou obvykle krátké.

Všimněte si, že zvolená logika porovnání a vytváření snímků musí vzájemně odpovídat: hluboké porovnání vyžaduje správné fungování hloubkového snímku.

Jednoduché neměnné třídy

Představte si vlastnost, která používá převaděč hodnot k mapování jednoduché neměnné třídy.

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));

Vlastnosti tohoto typu nevyžadují zvláštní porovnání ani snímky, protože:

  • Rovnost se přepíše, aby se různé instance správně porovnávaly.
  • Typ je neměnný, takže neexistuje žádná šance na změnu hodnoty snímku.

V tomto případě je tedy výchozí chování EF Core v pořádku, jak je.

Jednoduché neměnné struktury

Mapování jednoduchých struktur je také jednoduché a nevyžaduje žádné zvláštní porovnávače ani snímkování.

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 má integrovanou podporu generování kompilovaných a členových porovnání vlastností struktury. To znamená, že struktury nemusí mít přepsání rovnosti pro EF Core, ale přesto se můžete rozhodnout, že to uděláte z jiných důvodů. Speciální vytváření snímků také není potřeba, protože struktury jsou neměnné a vždy se kopírují po členech. (To platí také pro proměnlivé struktury, ale proměnlivé struktury by se měly obecně vyhnout.)

Proměnlivé třídy

Pokud je to možné, doporučujeme používat neměnné typy (třídy nebo struktury) s převaděči hodnot. To je obvykle efektivnější a má čistější sémantiku než použití proměnlivého typu. To však znamená, že je běžné používat vlastnosti typů, které aplikace nemůže změnit. Například mapování vlastnosti obsahující seznam čísel:

public List<int> MyListProperty { get; set; }

Třída List<T>:

  • Má rovnost odkazů; dva seznamy obsahující stejné hodnoty jsou považovány za odlišné.
  • Je proměnlivý; hodnoty v seznamu lze přidat a odebrat.

Typický převod hodnoty u vlastnosti seznamu může převést seznam na JSON a z 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()));

Konstruktor ValueComparer<T> přijímá tři výrazy:

  • Výraz pro kontrolu rovnosti
  • Výraz pro generování kódu hash
  • Výraz pro vytvoření snímku hodnoty

V takovém případě je porovnání provedeno kontrolou, jestli jsou sekvence čísel stejné.

Stejně tak je kód hash sestavený z této stejné sekvence. (Všimněte si, že se jedná o kód hash nad proměnlivými hodnotami, a proto může způsobit problémy. Pokud je to možné, buďte neměnní.)

Snímek se vytvoří klonováním seznamu pomocí ToList. Je to potřeba opět jenom v případě, že seznamy budou změněny. Pokud je to možné, buďte neměnní.

Poznámka:

Převaděče hodnot a porovnávače se vytvářejí pomocí výrazů, nikoli jednoduchých delegátů. Důvodem je, že EF Core tyto výrazy vloží do mnohem složitějšího stromu výrazů, který se pak zkompiluje do delegáta pro tvarování entity. Koncepčně se to podobá internímu nahrazování funkcí v kompilátoru. Například jednoduchý převod může být pouze vkompilované přetypování, nikoli volání jiné metody k provedení převodu.

Klíčové porovnávače

Část pozadí popisuje, proč může porovnání klíčů vyžadovat speciální sémantiku. Při nastavování vlastnosti primárního, hlavního nebo cizího klíče nezapomeňte vytvořit porovnávací nástroj, který bude vhodný pro tyto klíče.

Používejte SetKeyValueComparer ve výjimečných případech, kdy se pro stejnou vlastnost vyžaduje jiná sémantika.

Poznámka:

SetStructuralValueComparer byla zastaralá. Místo toho použijte SetKeyValueComparer.

Nahrazení výchozího porovnávače

Někdy nemusí být výchozí porovnání používané EF Core vhodné. Například mutace bajtů není ve výchozím nastavení zjištěna v EF Core. To lze přepsat nastavením jiného porovnávače u vlastnosti:

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 nyní bude porovnávat bajtové sekvence a proto bude detekovat mutace bajtového pole.