Компараторы значений

Совет

Код в этом документе можно найти на сайте 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 и из нее:

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 теперь сравнивает последовательности байтов и, следовательно, обнаруживает изменения массива байтов.