Поделиться через


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

Подсказка

Код в этом документе можно найти на сайте 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, хорошо работает: значение копируется as-is при моментальном снимке и сравнивается со встроенным сравнением равенства типа. При реализации собственного средства сравнения значений важно учитывать, подходит ли логика глубокого или мелкого сравнения (и создания моментальных снимков).

Рассмотрим массивы байтов, которые могут быть произвольно большими. Их можно сравнить:

  • По ссылке, так что разница обнаруживается только в том случае, если используется новый массив байтов.
  • При глубоком сравнении, например, обнаружена мутация байтов в массиве

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