Porovnávače hodnot
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ř. metodouEquals
); 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. hloubkové porovnání
U malých neměnných hodnotových typů, jako int
je výchozí logika EF Core, funguje dobře: hodnota se zkopíruje tak, jak je při vytváření snímků, 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:
- Na základě odkazu je takový rozdíl zjištěn pouze v případě, že se použije nové bajtové pole.
- 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 proto používá hloubkové porovnání pro bajtová pole fungující jako klíče; to pravděpodobně nemá velký výkon, 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 ztlumení 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
. Opět je to potřeba jenom v případě, že seznamy budou ztlumené. 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 to, že EF Core tyto výrazy vloží do mnohem složitějšího stromu výrazů, který se pak zkompiluje do delegáta shaperu entity. Koncepčně se to podobá vkládání kompilátoru. Například jednoduchý převod může být pouze kompilován v 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 objektu nebo cizího klíče nezapomeňte vytvořit porovnávací nástroj, který je vhodný pro 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.
Přepsání 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 teď bude porovnávat bajtové sekvence, a proto bude zjišťovat bajtové maticové mutace.