Porównywanie wartości

Napiwek

Kod w tym dokumencie można znaleźć w witrynie GitHub jako przykład z możliwością uruchomienia.

Tło

Śledzenie zmian oznacza, że program EF Core automatycznie określa, jakie zmiany zostały wykonane przez aplikację w załadowanym wystąpieniu jednostki, dzięki czemu te zmiany można zapisać z powrotem do bazy danych po SaveChanges wywołaniu. Program EF Core zwykle wykonuje to przez utworzenie migawki wystąpienia podczas ładowania z bazy danych i porównanie tej migawki z wystąpieniem przekazanym aplikacji.

Program EF Core zawiera wbudowaną logikę tworzenia migawek i porównywania większości standardowych typów używanych w bazach danych, więc użytkownicy zwykle nie muszą się martwić o ten temat. Jednak gdy właściwość jest mapowana za pośrednictwem konwertera wartości, program EF Core musi przeprowadzić porównanie dla dowolnych typów użytkowników, które mogą być złożone. Domyślnie program EF Core używa domyślnego porównania równości zdefiniowanego przez typy (np. Equals metoda); do migawek typy wartości są kopiowane w celu utworzenia migawki, natomiast w przypadku typów referencyjnych nie ma kopiowania, a to samo wystąpienie jest używane jako migawka.

W przypadkach, gdy wbudowane zachowanie porównania nie jest odpowiednie, użytkownicy mogą udostępnić moduł porównujący wartości, który zawiera logikę tworzenia migawek, porównywania i obliczania kodu skrótu. Na przykład następujące polecenie konfiguruje konwersję wartości dla List<int> właściwości, która ma być konwertowana na ciąg JSON w bazie danych, i definiuje również odpowiedni element porównujący wartości:

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

Aby uzyskać więcej informacji, zobacz poniższe klasy modyfikowalne.

Należy pamiętać, że podczas określania, czy dwie wartości klucza są takie same podczas rozwiązywania relacji, są również używane w przypadku porównywania wartości; zostało to wyjaśnione poniżej.

Płytkie a głębokie porównanie

W przypadku małych, niezmiennych typów wartości, takich jak int, domyślna logika ef Core działa dobrze: wartość jest kopiowana jako podczas migawek i porównywana z wbudowanym porównaniem równości typu. Podczas implementowania własnego porównania wartości ważne jest, aby rozważyć, czy logika głębokiego lub płytkiego porównania (i migawek) jest odpowiednia.

Rozważ tablice bajtów, które mogą być dowolnie duże. Można je porównać:

  • Przy użyciu odwołania taka różnica jest wykrywana tylko wtedy, gdy jest używana nowa tablica bajtów
  • Z głębokiego porównania wykryto mutację bajtów w tablicy

Domyślnie program EF Core używa pierwszego z tych metod dla tablic bajtów innych niż klucz. Oznacza to, że tylko odwołania są porównywane, a zmiana jest wykrywana tylko wtedy, gdy istniejąca tablica bajtów zostanie zamieniona na nową. Jest to pragmatyczna decyzja, która pozwala uniknąć kopiowania całych tablic i porównywania ich bajtów do bajtów podczas wykonywania polecenia SaveChanges. Oznacza to, że typowy scenariusz zamiany jednego obrazu na inny jest obsługiwany w wydajny sposób.

Z drugiej strony równość odwołań nie będzie działać, gdy tablice bajtów są używane do reprezentowania kluczy binarnych, ponieważ jest bardzo mało prawdopodobne, że właściwość FK jest ustawiona na to samo wystąpienie co właściwość PK, do której należy ją porównać. W związku z tym program EF Core używa głębokich porównań dla tablic bajtów działających jako klucze; jest to mało prawdopodobne, aby osiągnąć dużą wydajność, ponieważ klucze binarne są zwykle krótkie.

Należy pamiętać, że wybrana logika porównania i migawek musi odpowiadać sobie nawzajem: głębokie porównanie wymaga dokładnego tworzenia migawek w celu poprawnego działania.

Proste niezmienne klasy

Rozważ właściwość, która używa konwertera wartości do mapowania prostej, niezmiennej klasy.

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

Właściwości tego typu nie wymagają specjalnych porównań ani migawek, ponieważ:

  • Równość jest zastępowana, dzięki czemu różne wystąpienia będą prawidłowo porównywane
  • Typ jest niezmienny, więc nie ma szans na wyciszenie wartości migawki

W tym przypadku domyślne zachowanie platformy EF Core jest w porządku, tak jak jest.

Proste niezmienne struktury

Mapowanie prostych struktur jest również proste i nie wymaga specjalnych porównań ani migawek.

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

Program EF Core ma wbudowaną obsługę generowania skompilowanych porównań elementów członkowskich właściwości struktury. Oznacza to, że struktury nie muszą mieć przesłonięć równości dla platformy EF Core, ale nadal możesz zdecydować się na to z innych powodów. Ponadto specjalne migawki nie są potrzebne, ponieważ struktury są niezmienne i są zawsze kopiowane w sposób członkowski. (Dotyczy to również modyfikowalnych struktur, ale należy unikać modyfikowalnych struktur).

Klasy modyfikowalne

Zaleca się używanie niezmiennych typów (klas lub struktur) z konwerterami wartości, jeśli jest to możliwe. Jest to zwykle bardziej wydajne i ma czystszą semantykę niż użycie typu modyfikowalnego. Jednak mówi się, że często używać właściwości typów, których aplikacja nie może zmienić. Na przykład mapowanie właściwości zawierającej listę liczb:

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

Klasa List<T> :

  • Ma równość referencyjną; dwie listy zawierające te same wartości są traktowane jako różne.
  • Jest modyfikowalny; wartości na liście można dodawać i usuwać.

Typowa konwersja wartości we właściwości listy może przekonwertować listę na i z formatu 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> akceptuje trzy wyrażenia:

  • Wyrażenie do sprawdzania równości
  • Wyrażenie służące do generowania kodu skrótu
  • Wyrażenie migawki wartości

W takim przypadku porównanie jest wykonywane przez sprawdzenie, czy sekwencje liczb są takie same.

Podobnie kod skrótu jest kompilowany na podstawie tej samej sekwencji. (Należy pamiętać, że jest to kod skrótu dla wartości modyfikowalnych, co może powodować problemy. Zamiast tego można je modyfikować.

Migawka jest tworzona przez sklonowanie listy za pomocą polecenia ToList. Ponownie jest to konieczne tylko wtedy, gdy listy zostaną zmutowane. Zamiast tego należy być niezmiennym, jeśli to możliwe.

Uwaga

Konwertery wartości i porównania są konstruowane przy użyciu wyrażeń, a nie prostych delegatów. Dzieje się tak, ponieważ program EF Core wstawia te wyrażenia do znacznie bardziej złożonego drzewa wyrażeń, które następnie jest kompilowane do delegata kształtowania jednostki. Koncepcyjnie jest to podobne do inlinowania kompilatora. Na przykład prosta konwersja może być tylko skompilowana w rzutowaniu, a nie wywołanie innej metody w celu przeprowadzenia konwersji.

Kluczowe porównania

W sekcji w tle opisano, dlaczego kluczowe porównania mogą wymagać specjalnej semantyki. Pamiętaj, aby utworzyć moduł porównujący, który jest odpowiedni dla kluczy podczas ustawiania go dla właściwości klucza podstawowego, podmiotu zabezpieczeń lub klucza obcego.

Należy użyć SetKeyValueComparer w rzadkich przypadkach, w których różne semantyka jest wymagana w tej samej właściwości.

Uwaga

SetStructuralValueComparer został przestarzały. Użycie w zamian parametru SetKeyValueComparer.

Zastępowanie domyślnego porównania

Czasami domyślne porównanie używane przez program EF Core może nie być odpowiednie. Na przykład mutacja tablic bajtowych nie jest domyślnie wykrywana w programie EF Core. Można to przesłonić, ustawiając inny element porównujący dla właściwości:

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

Program EF Core będzie teraz porównywać sekwencje bajtów i w związku z tym wykrywa mutacje tablic bajtów.