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

Совет

код в этом документе можно найти на GitHub в качестве готового к запуску примера.

История

Отслеживание изменений означает, что EF Core автоматически определяет, какие изменения были выполнены приложением на загруженном экземпляре сущности, чтобы эти изменения можно было сохранить обратно в базу данных при вызове метода. EF Core обычно выполняет это путем создания моментального снимка экземпляра при его загрузке из базы данных и сравнения этого моментального снимка с экземпляром приложения.

EF Core поставляется со встроенной логикой для значит и сравнивает большинство стандартных типов, используемых в базах данных, поэтому пользователям обычно не нужно беспокоиться об этом разделе. Однако если свойство сопоставлено через преобразователь значений, EF Core необходимо выполнить сравнение с произвольными типами пользователей, что может быть сложным. По умолчанию EF Core использует сравнение на равенство по умолчанию, определенное типами (например, Equals метод); для значит 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 вставляет эти выражения в гораздо более сложное дерево выражения, которое затем компилируется в делегат сущности-фигуры. По сути, это похоже на встраивание компилятора. Например, простое преобразование может быть просто скомпилированным в приведении, а не вызовом другого метода для преобразования.

Ключевые компараторы

В разделе Background описывается, почему для сравнения ключей может потребоваться специальная семантика. Обязательно создайте средство сравнения, которое подходит для ключей при его задании в свойстве первичного, основного или внешнего ключа.

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