次の方法で共有


値の比較器

ヒント

このドキュメントのコードは、実行可能 なサンプルとして 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()));

詳細については、以下の 変更可能なクラス を参照してください。

値比較子は、リレーションシップを解決するときに 2 つのキー値が同じかどうかを判断するときにも使用されることに注意してください。これについては以下で説明します。

浅い比較と深い比較

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> クラス:

  • 参照の等価性があります。同じ値を含む 2 つのリストが異なるとして扱われます。
  • 変更可能です。リスト内の値を追加および削除できます。

リスト プロパティの一般的な値変換では、リストを JSON に変換したり、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> コンストラクターは、次の 3 つの式を受け入れます。

  • 等しいかどうかを確認する式
  • ハッシュ コードを生成するための式
  • 値のスナップショットを作成する式

この場合、数値のシーケンスが同じかどうかを確認することで比較が行われます。

同様に、ハッシュ コードはこの同じシーケンスから構築されます。 (これは変更可能な値に対するハッシュ コードであるため、問題が発生する可能性があることに注意してください。可能な場合は、代わりに不変であるしてください。)

スナップショットは、 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 はバイト シーケンスを比較するため、バイト配列の変更を検出します。