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

Совет

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