Сравнение значений

Совет

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