Bagikan melalui


Pembanding Nilai

Tip

Kode dalam dokumen ini dapat ditemukan di GitHub sebagai sampel yang dapat dijalankan.

Latar belakang

Pelacakan perubahan berarti bahwa EF Core secara otomatis menentukan perubahan apa yang dilakukan oleh aplikasi pada instans entitas yang dimuat, sehingga perubahan tersebut dapat disimpan kembali ke database saat SaveChanges dipanggil. EF Core biasanya melakukan ini dengan mengambil rekam jepret instans saat dimuat dari database, dan membandingkan rekam jepret tersebut dengan instans yang diserahkan ke aplikasi.

EF Core hadir dengan logika bawaan untuk rekam jepret dan membandingkan sebagian besar jenis standar yang digunakan dalam database, sehingga pengguna biasanya tidak perlu khawatir tentang topik ini. Namun, ketika properti dipetakan melalui pengonversi nilai, EF Core perlu melakukan perbandingan pada jenis pengguna arbitrer, yang mungkin kompleks. Secara default, EF Core menggunakan perbandingan kesetaraan default yang ditentukan oleh jenis (misalnya Equals metode); untuk rekam jepret, jenis nilai disalin untuk menghasilkan rekam jepret, sementara untuk jenis referensi tidak ada penyalinan yang terjadi, dan instans yang sama digunakan sebagai rekam jepret.

Dalam kasus di mana perilaku perbandingan bawaan tidak sesuai, pengguna dapat memberikan perbandingan nilai, yang berisi logika untuk rekam jepret, membandingkan, dan menghitung kode hash. Misalnya, berikut ini menyiapkan konversi nilai untuk List<int> properti menjadi nilai yang dikonversi ke string JSON dalam database, dan menentukan pembanding nilai yang sesuai juga:

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()));

Lihat kelas yang dapat diubah di bawah ini untuk detail lebih lanjut.

Perhatikan bahwa pembanding nilai juga digunakan saat menentukan apakah dua nilai kunci sama saat menyelesaikan hubungan; ini dijelaskan di bawah ini.

Perbandingan dangkal vs. mendalam

Untuk jenis nilai kecil yang tidak dapat diubah seperti int, logika default EF Core berfungsi dengan baik: nilai disalin apa adanya ketika direkam jepret, dan dibandingkan dengan perbandingan kesetaraan bawaan jenis. Saat menerapkan perbandingan nilai Anda sendiri, penting untuk mempertimbangkan apakah logika perbandingan mendalam atau dangkal (dan snapshotting) sesuai.

Pertimbangkan array byte, yang bisa sangat besar. Ini dapat dibandingkan:

  • Dengan referensi, sehingga perbedaan hanya terdeteksi jika array byte baru digunakan
  • Dengan perbandingan mendalam, sehingga mutasi byte dalam array terdeteksi

Secara default, EF Core menggunakan pendekatan pertama ini untuk array byte non-kunci. Artinya, hanya referensi yang dibandingkan dan perubahan hanya terdeteksi ketika array byte yang ada diganti dengan yang baru. Ini adalah keputusan pragmatis yang menghindari penyalinan seluruh array dan membandingkannya byte-to-byte saat mengeksekusi SaveChanges. Ini berarti bahwa skenario umum mengganti, katakanlah, satu gambar dengan gambar lain ditangani dengan cara yang berkinerja.

Di sisi lain, kesetaraan referensi tidak akan berfungsi ketika array byte digunakan untuk mewakili kunci biner, karena sangat tidak mungkin properti FK diatur ke instans yang sama dengan properti PK yang perlu dibandingkan. Oleh karena itu, EF Core menggunakan perbandingan mendalam untuk array byte yang bertindak sebagai kunci; ini tidak mungkin memiliki hit performa besar karena kunci biner biasanya pendek.

Perhatikan bahwa perbandingan dan logika rekam jepret yang dipilih harus sesuai satu sama lain: perbandingan mendalam memerlukan rekam jepret mendalam agar berfungsi dengan benar.

Kelas sederhana yang tidak dapat diubah

Pertimbangkan properti yang menggunakan pengonversi nilai untuk memetakan kelas sederhana dan tidak dapat diubah.

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));

Properti jenis ini tidak memerlukan perbandingan atau rekam jepret khusus karena:

  • Kesetaraan ditimpa sehingga instans yang berbeda akan dibandingkan dengan benar
  • Jenisnya tidak dapat diubah, sehingga tidak ada kemungkinan untuk bermutasi nilai rekam jepret

Jadi dalam hal ini perilaku default EF Core baik-baik saja apa adanya.

Struktur sederhana yang tidak dapat diubah

Pemetaan untuk struktur sederhana juga sederhana dan tidak memerlukan pembanding atau rekam jepret khusus.

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 memiliki dukungan bawaan untuk menghasilkan perbandingan properti struct yang dikompilasi dan dikompilasi. Ini berarti struktur tidak perlu mengambil alih kesetaraan untuk EF Core, tetapi Anda mungkin masih memilih untuk melakukan ini karena alasan lain. Selain itu, rekam jepret khusus tidak diperlukan karena struktur tidak dapat diubah dan selalu disalin anggotanya. (Ini juga berlaku untuk struktur yang dapat diubah, tetapi struktur yang dapat diubah harus dihindari secara umum.)

Kelas yang dapat diubah

Disarankan agar Anda menggunakan jenis yang tidak dapat diubah (kelas atau struktur) dengan pengonversi nilai jika memungkinkan. Ini biasanya lebih efisien dan memiliki semantik yang lebih bersih daripada menggunakan jenis yang dapat diubah. Namun, yang sedang dikatakan, adalah umum untuk menggunakan properti jenis yang tidak dapat diubah aplikasi. Misalnya, memetakan properti yang berisi daftar angka:

public List<int> MyListProperty { get; set; }

Kelas List<T>:

  • Memiliki kesetaraan referensi; dua daftar yang berisi nilai yang sama diperlakukan sebagai berbeda.
  • Dapat diubah; nilai dalam daftar dapat ditambahkan dan dihapus.

Konversi nilai umum pada properti daftar mungkin mengonversi daftar ke dan dari 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> Konstruktor menerima tiga ekspresi:

  • Ekspresi untuk memeriksa kesetaraan
  • Ekspresi untuk menghasilkan kode hash
  • Ekspresi untuk rekam jepret nilai

Dalam hal ini perbandingan dilakukan dengan memeriksa apakah urutan angka sama.

Demikian juga, kode hash dibangun dari urutan yang sama ini. (Perhatikan bahwa ini adalah kode hash di atas nilai yang dapat diubah dan karenanya dapat menyebabkan masalah. Jadilah tidak dapat diubah sebagai gantinya jika Anda bisa.)

Rekam jepret dibuat dengan mengkloning daftar dengan ToList. Sekali lagi, ini hanya diperlukan jika daftar akan dimutasi. Jadilah tidak dapat diubah sebagai gantinya jika Anda bisa.

Catatan

Pengonversi dan pembanding nilai dibangun menggunakan ekspresi daripada delegasi sederhana. Ini karena EF Core menyisipkan ekspresi ini ke dalam pohon ekspresi yang jauh lebih kompleks yang kemudian dikompilasi ke dalam delegasi pembentuk entitas. Secara konseptual, ini mirip dengan compiler inlining. Misalnya, konversi sederhana mungkin hanya dikompilasi dalam pemeran, daripada panggilan ke metode lain untuk melakukan konversi.

Pembanding kunci

Bagian latar belakang mencakup mengapa perbandingan kunci mungkin memerlukan semantik khusus. Pastikan untuk membuat comparer yang sesuai untuk kunci saat mengaturnya pada properti utama, utama, atau kunci asing.

Gunakan SetKeyValueComparer dalam kasus yang jarang terjadi di mana semantik yang berbeda diperlukan pada properti yang sama.

Catatan

SetStructuralValueComparer telah usang. Gunakan SetKeyValueComparer sebagai gantinya.

Mengesampingkan pembanding default

Terkadang perbandingan default yang digunakan oleh EF Core mungkin tidak sesuai. Misalnya, mutasi array byte tidak, secara default, terdeteksi di EF Core. Ini dapat ditimpa dengan mengatur pembanding yang berbeda pada properti:

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 sekarang akan membandingkan urutan byte dan oleh karena itu akan mendeteksi mutasi array byte.