値比較子

ヒント

このドキュメント内のコードは、実行可能なサンプルとして GitHub にあります。

経歴

変更の追跡とは、読み込まれたエンティティ インスタンスに対してアプリケーションによって行われた変更が EF Core で自動的に判断されることを意味します。これにより、SaveChanges が呼び出されたときに、これらの変更を元のデータベースに保存できます。 EF Core では、通常、インスタンスがデータベースから読み込まれたときに "スナップショット" を作成し、そのスナップショットをアプリケーションに渡されたインスタンスと "比較" することによってこれを実行します。

EF Core には、データベースで使用されるほとんどの標準の型でスナップショットを作成して比較するための組み込みロジックが付属しているため、通常、ユーザーはこのトピックについて心配する必要はありません。 ただし、プロパティが値コンバーターによってマップされている場合、EF Core では任意のユーザーの型に対して比較を実行する必要がありますが、これは複雑になる可能性があります。 既定で、EF Core では型によって定義された既定の等値比較 (Equals メソッドなど) が使用されます。スナップショットの場合は、スナップショットを作成するために値型がコピーされます。一方、参照型の場合、コピーは行われず、同じインスタンスがスナップショットとして使用されます。

組み込みの比較の動作が適切でない場合、ユーザーは "値比較子" を指定できます。これには、スナップショットの作成、比較、およびハッシュ コードの計算を行うためのロジックが含まれています。 たとえば、次により、データベース内の JSON 文字列に変換された値になるように List<int> プロパティの値変換が設定され、適切な値比較子も定義されます。

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 の既定のロジックが適切に機能します。値は、スナップショットを作成するときにそのままコピーされ、型の組み込み等値比較を使用して比較されます。 独自の値比較子を実装するときは、深い比較または浅い比較 (およびスナップショット作成) ロジックのどちらが適切かを検討することが重要です。

バイト配列を検討します。これは任意に大きい場合があります。 これらは、次のように比較できます。

  • 参照によって。この場合は、新しいバイト配列が使用された場合にのみ、違いが検出されます
  • 深い比較によって。この場合は、配列内のバイトの変化が検出されます

既定で、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 との間で変換できます。

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 を使用してリストを複製することによって作成されます。 この場合も、これが必要なのは、リストを変更可能にする場合のみです。 可能であれば、不変にしてください。

Note

値コンバーターと比較子は、単純なデリゲートではなく、式を使用して構築されます。 これは、EF Core によってこれらの式はより複雑な式ツリーに挿入され、その後、エンティティ Shaper デリゲートにコンパイルされるからです。 概念的には、これはコンパイラのインライン展開に似ています。 たとえば、単純な変換は、変換を行うための別のメソッドへの呼び出しではなく、単にキャストでのコンパイルの可能性があります。

キー比較子

バックグラウンド セクションで、キーの比較で特殊なセマンティクスが必要になる場合がある理由を説明しています。 プライマリ、プリンシパル、または外部キーのプロパティに設定するときにキーに適した比較子を必ず作成してください。

同じプロパティで異なるセマンティクスが必要となるまれなケースでは、SetKeyValueComparer を使用します。

Note

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 ではバイト シーケンスが比較されるため、バイト配列の変化が検出されるようになります。