Perilaku Membingungkan dari EF Core OnDelete Restrict

Saya baru-baru ini membantu pengembang lain memahami berbagai perilaku “OnDelete” dari Entity Framework Core. Artinya, ketika entitas induk dalam hubungan induk/anak dihapus, apa yang harus terjadi pada anak?

Saya pikir ini sebenarnya semua cukup lurus ke depan. Cara saya memahami sesuatu adalah:

DeleteBehavior.Cascade – Hapus anak ketika orang tua dihapus (misalnya Cascading menghapus)
DeleteBehavior.SetNull – Mengatur FK pada anak menjadi nol (Jadi izinkan anak yatim)
DeleteBehavior.Restrict – Jangan izinkan induk dihapus sama sekali

Saya cukup yakin jika saya bertanya kepada 100 pengembang .NET apa artinya ini, ada kemungkinan besar mereka semua akan menjawab dengan cara yang sama. Namun pada kenyataannya, DeleteBehavior.Restrict sebenarnya bergantung pada apa yang telah Anda lakukan di DBContext tersebut hingga penghapusan… Mari saya jelaskan.

Pengaturan

Mari kita bayangkan bahwa saya memiliki dua model di database saya, mereka terlihat seperti ini:

class BlogPost
{
	public int Id { get; set; }
	public string PostName { get; set; }
	public ICollection<BlogImage> BlogImages { get; set; }
}

class BlogImage
{
	public int Id { get; set; }
	public int? BlogPostId { get; set; }
	public BlogPost? BlogPost { get; set; }
	public string ImageUrl { get; set; }
}

Kemudian bayangkan hubungan di EF Core diatur seperti ini:

modelBuilder.Entity<BlogImage>()
    .HasOne(x => x.BlogPost)
    .WithMany(x => x.BlogImages)
    .OnDelete(DeleteBehavior.Restrict);

Setiap pengembang yang melihat ini pada pandangan pertama akan mengatakan, jika saya menghapus posting blog yang memiliki gambar yang mengarah ke sana, itu akan menghentikan saya dari menghapus posting blog itu sendiri. Tapi apakah itu benar?

Mengujinya

Mari kita bayangkan saya memiliki satu set kode sederhana yang terlihat seperti do :

var context = new MyContext();
context.Database.Migrate();

var blogPost = new BlogPost
{
	PostName = "Post 1", 
	BlogImages = new List<BlogImage>
	{
		new BlogImage
		{
			ImageUrl = "/foo.png"
		}
	}
};

context.Add(blogPost);
context.SaveChanges();

Console.WriteLine("Blog Post Added");

var getBlogPost = context.Find<BlogPost>(blogPost.Id);
context.Remove(getBlogPost);
context.SaveChanges(); //Does this error here? We are deleting the blog post that has images

Console.WriteLine("Blog Post Removed");

Apakah saya menerima pengecualian? Jawabannya adalah .. Tidak. Ketika kode ini dijalankan, dan saya memeriksa database saya berakhir dengan BlogImage yang terlihat seperti ini:

Jadi alih-alih membatasi penghapusan, EF Core telah melanjutkan dan mengatur BlogPostId menjadi nol, dan pada dasarnya memberi saya catatan yatim piatu. Tapi kenapa?!

Menyelam terlebih dahulu ke dalam dokumentasi, kita dapat melihat bahwa DeleteBehavior.Restrict memiliki deskripsi berikut:

Untuk entitas yang dilacak oleh DbContext, nilai properti kunci asing di entitas dependen disetel ke nol saat prinsip terkait dihapus. Ini membantu menjaga grafik entitas dalam keadaan konsisten saat mereka dilacak, sehingga grafik yang sepenuhnya konsisten kemudian dapat ditulis ke database. Jika properti tidak dapat disetel ke null karena bukan tipe yang dapat dibatalkan, maka pengecualian akan ditampilkan saat SaveChanges() dipanggil.

Penekanan milikku.

Ini sangat tidak masuk akal IMO. Tapi saya ingin mengujinya lebih jauh. Jadi saya menggunakan skrip pengujian berikut, yang persis sama seperti sebelumnya, kecuali di tengah jalan saya membuat ulang Konteks DB. Mengingat dokumentasinya, entitas yang saya tarik kembali untuk dihapus tidak akan membuat gambar blog itu sendiri dilacak.

Dan tentu saja diberikan kode ini:

var context = new MyContext();
context.Database.Migrate();

var blogPost = new BlogPost
{
	PostName = "Post 1", 
	BlogImages = new List<BlogImage>
	{
		new BlogImage
		{
			ImageUrl = "/foo.png"
		}
	}
};

context.Add(blogPost);
context.SaveChanges();

Console.WriteLine("Blog Post Added");

context = new MyContext(); // <-- Create a NEW DB context

var getBlogPost = context.Find<BlogPost>(blogPost.Id);
context.Remove(getBlogPost);
context.SaveChanges();

Console.WriteLine("Blog Post Removed");

Saya *melakukan* dapatkan pengecualian yang saya harapkan selama ini:

SqlException: Pernyataan DELETE bertentangan dengan batasan REFERENCE “FK_BlogImages_BlogPosts_BlogPostId”.

Masih menulis ini, saya berjuang untuk memahami logika di sini. Jika kebetulan Anda telah memuat entitas anak (Secara tidak sengaja atau tidak), pembatasan penghapusan Anda tiba-tiba berperilaku sangat berbeda. Itu tidak masuk akal bagi saya.

Saya yakin beberapa dari Anda siap untuk melompat melalui layar Anda dan memberi tahu saya bahwa ambiguitas semacam ini adalah karena saya menggunakan FK yang dapat dibatalkan pada jenis BlogImage saya. Yang benar, dan itu berarti saya berharap bahwa entitas BlogImage *bisa* menjadi yatim piatu. Jika saya mengatur ini menjadi kunci yang tidak dapat dibatalkan, maka saya akan selalu mendapatkan pengecualian karena tidak dapat mengatur FK ke nol. Namun, poin yang saya coba sampaikan adalah jika saya memiliki kunci yang dapat dibatalkan, tetapi saya mengatur perilaku hapus untuk membatasi, saya masih harus melihat semacam perilaku yang konsisten.

Bagaimana Dengan DeleteBehavior.SetNull?

Hal lain yang menarik untuk dicatat adalah bahwa dokumentasi untuk DeleteBehavior.SetNull sebenarnya identik dengan Restrict :

Untuk entitas yang dilacak oleh DbContext, nilai properti kunci asing di entitas dependen disetel ke nol saat prinsip terkait dihapus. Ini membantu menjaga grafik entitas dalam keadaan konsisten saat mereka dilacak, sehingga grafik yang sepenuhnya konsisten kemudian dapat ditulis ke database. Jika properti tidak dapat disetel ke null karena bukan tipe yang dapat dibatalkan, maka pengecualian akan ditampilkan saat SaveChanges() dipanggil.

Namun, dalam pengujian saya, menggunakan SetNull tidak bergantung pada entitas mana yang dilacak oleh DbContext, dan bekerja sama setiap saat (Meskipun, saya menganggap bahwa mungkin ini adalah fungsi SQL Server menggunakan nilai default daripada EF Core melakukan pekerjaan kaki).

Saya sebenarnya menghabiskan waktu lama menggunakan Google-Fu untuk mencoba dan menemukan orang yang berbicara tentang perbedaan antara SetNull dan Restrict tetapi, banyak yang hanya mengikuti apa yang saya jelaskan di intro. SetNull menyetel nol ketika itu datang, dan membatasi selalu menghentikan Anda dari menghapus.

Kesimpulan

Mungkin saya minoritas di sini, atau mungkin ada alasan yang sangat bagus untuk perilaku pembatasan yang bertindak seperti itu, tetapi saya benar-benar berpikir bahwa untuk sebagian besar pengembang, ketika mereka menggunakan DeleteBehavior.Restrict, mereka mengharapkan orang tua untuk diblokir agar tidak dihapus dalam keadaan apa pun. Saya tidak berpikir ada orang yang mengharapkan pemuatan entitas yang tidak disengaja ke dalam DbContext untuk tiba-tiba mengubah perilaku. Apakah saya sendirian dalam hal itu?

Mempelajari Sesuatu yang Baru?

Dengan membelikan saya kopi (Sebagai satu kali off atau bulanan), Anda memastikan saya memiliki tingkat kafein yang optimal untuk terus membaca catatan tempel dan membaca dokumentasi yang buruk untuk memberi Anda apa pun kecuali bagian penting dari .NET.