Haruskah saya memeriksa apakah ada sesuatu di db dan gagal cepat atau menunggu pengecualian db

32

Memiliki dua kelas:

public class Parent 
{
    public int Id { get; set; }
    public int ChildId { get; set; }
}

public class Child { ... }

Ketika menetapkan ChildIduntuk Parentharus saya cek dulu jika ada di DB atau menunggu DB untuk melemparkan pengecualian?

Misalnya (menggunakan Entity Framework Core):

Perhatikan jenis-jenis cek ini SELURUH INTERNET, bahkan pada dokumen resmi Microsoft: https://docs.microsoft.com/en-us/aspnet/mvc/overview/getting-started/getting-started-with-ef-using- mvc / handling-concurrency-with-the-entitas-framework-in-an-asp-net-mvc-application #odifikasi-departemen-pengontrol tetapi ada penanganan pengecualian tambahan untukSaveChanges

juga, perhatikan bahwa maksud utama dari pemeriksaan ini adalah untuk mengembalikan pesan ramah dan status HTTP yang diketahui kepada pengguna API dan tidak sepenuhnya mengabaikan pengecualian basis data. Dan satu-satunya pengecualian tempat yang dibuang adalah di dalam SaveChangesatau SaveChangesAsyncmenelepon ... jadi tidak akan ada pengecualian saat Anda menelepon FindAsyncatau Any. Jadi jika anak ada tetapi sudah dihapus sebelumnya SaveChangesAsyncmaka pengecualian concurrency akan dibuang.

Saya melakukan ini karena fakta bahwa foreign key violationpengecualian akan jauh lebih sulit untuk memformat untuk menampilkan "Anak dengan id {parent.ChildId} tidak dapat ditemukan."

public async Task<ActionResult<Parent>> CreateParent(Parent parent)
{
    // is this code redundant?
   // NOTE: its probably better to use Any isntead of FindAsync because FindAsync selects *, and Any selects 1
    var child = await _db.Children.FindAsync(parent.ChildId);
    if (child == null)
       return NotFound($"Child with id {parent.ChildId} could not be found.");

    _db.Parents.Add(parent);    
    await _db.SaveChangesAsync();        

    return parent;
}

melawan:

public async Task<ActionResult<Parent>> CreateParent(Parent parent)
{
    _db.Parents.Add(parent);
    await _db.SaveChangesAsync();  // handle exception somewhere globally when child with the specified id doesn't exist...  

    return parent;
}

Contoh kedua di Postgres akan menampilkan 23503 foreign_key_violationkesalahan: https://www.postgresql.org/docs/9.4/static/errcodes-appendix.html

Kelemahan dari penanganan pengecualian dengan ORM seperti EF adalah bahwa hal itu hanya akan bekerja dengan backend basis data tertentu. Jika Anda pernah ingin beralih ke SQL server atau yang lain ini tidak akan berfungsi lagi karena kode kesalahan akan berubah.

Tidak memformat pengecualian dengan benar untuk pengguna akhir dapat memaparkan beberapa hal yang tidak ingin dilihat oleh siapa pun selain oleh pengembang.

Terkait:

https://stackoverflow.com/questions/6171588/preventing-race-condition-of-if-exists-update-else-insert-in-entity-framework

https://stackoverflow.com/questions/4189954/implementing-if-not-exists-insert-using-entity-framework-without-race-conditions

https://stackoverflow.com/questions/308905/should-there-be-a-ttransaction-for-read-queries

Konrad
sumber
2
Berbagi penelitian Anda membantu semua orang . Beri tahu kami apa yang telah Anda coba dan mengapa itu tidak memenuhi kebutuhan Anda. Ini menunjukkan bahwa Anda telah meluangkan waktu untuk mencoba membantu diri sendiri, itu menyelamatkan kami dari mengulangi jawaban yang jelas, dan yang paling utama itu membantu Anda mendapatkan jawaban yang lebih spesifik dan relevan. Lihat juga Cara Meminta
nyamuk
5
Seperti yang disebutkan orang lain, ada kemungkinan catatan dapat dimasukkan atau dihapus bersamaan dengan pemeriksaan Anda untuk NotFound. Karena alasan itu, pengecekan pertama sepertinya solusi yang tidak dapat diterima. Jika Anda khawatir tentang penulisan penanganan pengecualian khusus Postgres yang tidak portabel untuk backend database lain, cobalah untuk membuat struktur pengendali pengecualian sedemikian rupa sehingga fungsionalitas inti dapat diperluas oleh kelas khusus basis data (SQL, Postgres, dll)
billrichard
3
Melihat melalui komentar, saya perlu mengatakan ini: berhenti berpikir dalam hampa . "Gagal cepat" bukanlah aturan yang terisolasi, di luar konteks yang dapat atau harus diikuti secara membabi buta. Ini aturan praktis. Selalu analisis apa yang sebenarnya ingin Anda capai dan pertimbangkan teknik apa pun yang membantu Anda mencapai tujuan itu atau tidak. "Gagal cepat" membantu Anda mencegah efek samping yang tidak diinginkan. Dan selain itu, "gagal cepat" benar-benar berarti, "gagal segera setelah Anda dapat mendeteksi ada masalah." Kedua teknik gagal segera setelah masalah terdeteksi, jadi Anda harus melihat pertimbangan lain.
jpmc26
1
@ Konrad apa hubungannya dengan pengecualian? Berhentilah memikirkan kondisi ras sebagai sesuatu yang hidup dalam kode Anda: itu adalah milik alam semesta. Apa pun, apa pun yang menyentuh sumber daya yang tidak sepenuhnya dikontrol (misalnya akses memori langsung, memori bersama, database, REST API, sistem file, dll.) Lebih dari sekali dan mengharapkannya tidak berubah memiliki kondisi lomba yang potensial. Heck, kita berurusan dengan ini dalam C yang bahkan tidak memiliki pengecualian. Hanya saja jangan pernah bercabang pada keadaan sumber daya yang tidak Anda kontrol jika setidaknya salah satu cabang mengacaukan keadaan sumber daya itu.
Jared Smith
1
@DanielPryden Dalam pertanyaan saya, saya tidak mengatakan bahwa saya tidak ingin menangani pengecualian basis data (saya tahu bahwa pengecualian tidak dapat dihindari). Saya pikir banyak orang salah paham, saya ingin memiliki pesan kesalahan yang ramah untuk API Web saya (untuk dibaca oleh pengguna akhir) Child with id {parent.ChildId} could not be found.. Dan memformat "Pelanggaran kunci asing" Saya pikir lebih buruk dalam kasus ini.
Konrad

Jawaban:

3

Agak pertanyaan yang membingungkan, tapi YA Anda harus memeriksa dulu dan tidak hanya menangani pengecualian DB.

Pertama-tama, dalam contoh Anda, Anda berada di lapisan data, menggunakan EF langsung pada database untuk menjalankan SQL. Kode Anda sama dengan menjalankan

select * from children where id = x
//if no results, perform logic
insert into parents (blah)

Alternatif yang Anda sarankan adalah:

insert into parents (blah)
//if exception, perform logic

Menggunakan pengecualian untuk menjalankan logika kondisional lambat dan secara universal disukai.

Anda memang memiliki kondisi balapan dan harus menggunakan transaksi. Tapi ini bisa sepenuhnya dilakukan dalam kode.

using (var transaction = new TransactionScope())
{
    var child = await _db.Children.FindAsync(parent.ChildId);
    if (child == null) 
    {
       return NotFound($"Child with id {parent.ChildId} could not be found.");
    }

    _db.Parents.Add(parent);    
    await _db.SaveChangesAsync();        
    transaction.Complete();

    return parent;
}

Kuncinya adalah bertanya pada diri sendiri:

"Apakah kamu mengharapkan situasi ini terjadi?"

Jika tidak, maka pastikan, sisipkan dan buang pengecualian. Tetapi hanya menangani pengecualian seperti kesalahan lain yang mungkin terjadi.

Jika Anda mengharapkannya terjadi, maka itu TIDAK luar biasa dan Anda harus memeriksa untuk melihat apakah anak itu ada terlebih dahulu, merespons dengan pesan ramah yang sesuai jika tidak.

Sunting - Ada banyak kontroversi mengenai ini sepertinya. Sebelum Anda downvote pertimbangkan:

A. Bagaimana jika ada dua kendala FK. Apakah Anda menganjurkan parsing pesan pengecualian untuk mengetahui objek yang hilang?

B. Jika Anda kehilangan, hanya satu pernyataan SQL yang dijalankan. Hanya klik yang mengeluarkan biaya tambahan dari kueri kedua.

C. Biasanya ID akan menjadi kunci pengganti, Sulit membayangkan situasi di mana Anda tahu satu dan Anda tidak cukup yakin itu pada DB. Memeriksa akan aneh. Tetapi bagaimana jika itu adalah kunci alami yang diketik pengguna? Itu bisa memiliki peluang tinggi untuk tidak hadir

Ewan
sumber
Komentar bukan untuk diskusi panjang; percakapan ini telah dipindahkan ke obrolan .
maple_shaft
1
Ini benar-benar salah dan menyesatkan! Jawaban seperti inilah yang menghasilkan profesional yang buruk yang selalu harus saya lawan. SELECT tidak pernah mengunci tabel, jadi antara SELECT dan INSERT, UPDATE atau DELTE, catatan dapat berubah. Jadi perangkat lunak buruk buruk berkenan dan kecelakaan menunggu untuk terjadi dalam produksi.
Daniel Lobo
1
Transactments @DanielLobo memperbaiki itu
Ewan
1
ujilah jika Anda tidak percaya kepada saya
Ewan
1
@Yusha Saya punya kode di sini
Ewan
111

Memeriksa keunikan dan kemudian pengaturan adalah antipattern; selalu dapat terjadi bahwa ID dimasukkan bersamaan antara waktu pemeriksaan dan waktu penulisan. Database dilengkapi untuk menangani masalah ini melalui mekanisme seperti kendala dan transaksi; kebanyakan bahasa pemrograman tidak. Oleh karena itu, jika Anda menghargai konsistensi data, serahkan ke ahlinya (basis data), yaitu melakukan penyisipan dan menangkap pengecualian jika itu terjadi.

Kilian Foth
sumber
34
memeriksa dan gagal tidak lebih cepat dari sekadar "mencoba" dan berharap yang terbaik. Mantan menyiratkan 2 operasi untuk diimplementasikan dan dilakukan oleh sistem Anda dan 2 oleh DB, sedangkan yang terbaru hanya menyiratkan salah satunya. Memeriksa didelegasikan ke server DB. Ini juga menyiratkan satu hop kurang ke jaringan dan satu tugas kurang untuk dihadiri oleh DB. Kita mungkin berpikir bahwa satu permintaan lagi ke DB terjangkau, tetapi kita sering lupa untuk berpikir besar. Pikirkan dalam konkurensi tinggi yang memicu kueri berulang kali. Itu bisa membohongi seluruh lalu lintas ke DB. Jika itu masalah terserah Anda untuk memutuskan.
Laiv
6
@Konrad Posisi saya adalah bahwa pilihan default-benar adalah satu permintaan yang akan gagal dengan sendirinya, dan itu adalah pendekatan pra-penerbangan permintaan yang terpisah yang memiliki beban pembuktian untuk membenarkan dirinya sendiri. Adapun "menjadi masalah": sehingga Anda sedang menggunakan transaksi atau sebaliknya memastikan bahwa Anda aman terhadap kesalahan ToCToU , kan? Tidak jelas bagi saya dari kode yang diposting bahwa Anda adalah, tetapi jika Anda tidak, maka sudah menjadi masalah dengan cara bom detak menjadi masalah jauh sebelum benar-benar meledak.
mtraceur
4
@Konrad EF Core tidak akan secara implisit memasukkan cek dan masukkan Anda ke dalam satu transaksi, Anda harus secara eksplisit memintanya. Tanpa transaksi, pengecekan pertama tidak ada gunanya karena status basis data dapat berubah antara cek dan masukkan. Bahkan dengan transaksi, Anda mungkin tidak mencegah perubahan basis data di bawah kaki Anda. Kami mengalami masalah beberapa tahun yang lalu menggunakan EF dengan Oracle di mana meskipun db mendukungnya, Entity tidak memicu penguncian catatan baca dalam transaksi, dan hanya sisipan yang diperlakukan sebagai transaksi.
Mr.Mindor
3
"Memeriksa keunikan dan kemudian pengaturan adalah antipattern" Aku tidak akan mengatakan ini. Ini sangat tergantung pada apakah Anda dapat menganggap tidak ada modifikasi lain yang terjadi dan apakah pemeriksaan menghasilkan beberapa hasil yang lebih berguna (bahkan hanya pesan kesalahan yang benar-benar berarti sesuatu bagi pembaca) ketika itu tidak ada. Dengan database yang menangani permintaan web secara bersamaan, tidak, Anda tidak dapat menjamin modifikasi lainnya tidak terjadi, tetapi ada kasus-kasus ketika itu asumsi yang masuk akal.
jpmc26
5
Memeriksa keunikan terlebih dahulu tidak menghilangkan kebutuhan untuk menangani kemungkinan kegagalan. Di sisi lain, jika suatu tindakan akan memerlukan melakukan beberapa operasi, memeriksa apakah semua kemungkinan untuk berhasil sebelum memulai salah satu dari mereka sering lebih baik daripada melakukan tindakan yang mungkin mungkin perlu digulung kembali. Melakukan pemeriksaan awal mungkin tidak menghindari semua situasi di mana kemunduran akan diperlukan, tetapi dapat membantu mengurangi frekuensi kasus tersebut.
supercat
38

Saya pikir apa yang Anda sebut "gagal cepat" dan apa yang saya sebut itu tidak sama.

Memberitahu database untuk membuat perubahan dan menangani kegagalan, itu cepat. Cara Anda rumit, lambat, dan tidak terlalu andal.

Teknik Anda itu tidak cepat gagal, itu "prelighting". Terkadang ada alasan bagus, tetapi tidak ketika Anda menggunakan database.

gnasher729
sumber
1
Ada kasus ketika Anda membutuhkan kueri ke-2 ketika satu kelas bergantung pada yang lain, jadi Anda tidak punya pilihan dalam kasus seperti itu.
Konrad
4
Tapi bukan disini. Dan permintaan basis data bisa sangat pintar, jadi saya biasanya meragukan "tidak ada pilihan".
gnasher729
1
Saya pikir itu juga tergantung pada aplikasi, jika Anda membuatnya hanya untuk beberapa pengguna maka itu tidak akan membuat perbedaan dan kode lebih mudah dibaca dengan 2 pertanyaan.
Konrad
21
Anda mengasumsikan bahwa DB Anda menyimpan data yang tidak konsisten. Dengan kata lain, sepertinya Anda tidak percaya pada DB Anda dan konsistensi datanya. Jika itu masalahnya, Anda memiliki masalah yang sangat besar dan solusi Anda adalah solusi. Suatu solusi paliatif ditakdirkan untuk ditolak lebih awal daripada nanti. Mungkin ada kasus di mana Anda dipaksa untuk mengkonsumsi DB di luar kendali dan manajemen Anda. Dari aplikasi lain. Dalam kasus tersebut, saya akan mempertimbangkan validasi tersebut. Bagaimanapun, @gnasher benar, milikmu tidak gagal dengan cepat atau bukan yang kami pahami sebagai gagal dengan cepat.
Laiv
15

Ini dimulai sebagai komentar tetapi tumbuh terlalu besar.

Tidak, seperti yang dinyatakan oleh jawaban lain, pola ini tidak boleh digunakan. *

Ketika berhadapan dengan sistem yang menggunakan komponen asinkron, akan selalu ada kondisi ras di mana database (atau sistem file, atau sistem async lainnya) dapat berubah antara cek dan perubahan. Pemeriksaan jenis ini sama sekali bukan cara yang dapat diandalkan untuk mencegah jenis kesalahan yang tidak ingin Anda tangani.
Lebih buruk daripada tidak mencukupi, sekilas itu memberi kesan bahwa itu harus mencegah kesalahan rekaman duplikat memberikan rasa aman yang salah.

Anda tetap membutuhkan penanganan kesalahan.

Dalam komentar Anda telah bertanya bagaimana jika Anda membutuhkan data dari berbagai sumber.
Masih tidak.

Itu mendasar tidak hilang jika apa yang ingin Anda periksa menjadi lebih kompleks.

Anda masih membutuhkan penanganan kesalahan.

Bahkan jika pemeriksaan ini adalah cara yang dapat diandalkan untuk mencegah kesalahan tertentu yang Anda coba lindungi, kesalahan lain masih dapat terjadi. Apa yang terjadi jika Anda kehilangan koneksi ke database, atau kehabisan ruang, atau?

Anda mungkin masih membutuhkan penanganan kesalahan terkait database lainnya . Penanganan kesalahan khusus ini mungkin harus menjadi bagian kecil darinya.

Jika Anda membutuhkan data untuk menentukan apa yang harus diubah, Anda jelas perlu mengumpulkannya dari suatu tempat. (tergantung pada alat apa yang Anda gunakan, mungkin ada cara yang lebih baik daripada permintaan terpisah untuk mengumpulkannya) Jika, dalam memeriksa data yang Anda kumpulkan, Anda menentukan bahwa Anda tidak perlu melakukan perubahan setelah semua, bagus, jangan membuat perubahan. Penentuan ini benar-benar terpisah dari masalah penanganan kesalahan.

Anda masih membutuhkan penanganan kesalahan.

Saya tahu saya berulang-ulang tetapi saya merasa penting untuk menjelaskannya. Saya sudah membersihkan kekacauan ini sebelumnya.

Ini akan gagal pada akhirnya. Ketika gagal, akan sulit dan memakan waktu untuk sampai ke dasar. Menyelesaikan masalah yang timbul dari kondisi balapan sulit. Mereka tidak terjadi secara konsisten, sehingga akan sulit atau bahkan tidak mungkin untuk mereproduksi secara terpisah. Anda tidak melakukan penanganan kesalahan yang tepat untuk memulai sehingga Anda tidak akan punya banyak hal untuk dilanjutkan: Mungkin laporan pengguna akhir tentang beberapa teks rahasia (yang Anda coba untuk mencegah agar tidak melihatnya sejak awal.) Mungkin jejak tumpukan yang menunjuk kembali ke fungsi itu yang ketika Anda melihatnya terang-terangan menyangkal kesalahan bahkan mungkin terjadi.

* Mungkin ada alasan bisnis yang sah untuk melakukan pemeriksaan yang ada ini, seperti untuk mencegah aplikasi menggandakan pekerjaan yang mahal, tetapi itu bukan pengganti yang cocok untuk penanganan kesalahan yang tepat.

Mr.Mindor
sumber
2

Saya pikir hal kedua yang perlu diperhatikan di sini - salah satu alasan Anda menginginkan ini adalah agar Anda dapat memformat pesan kesalahan agar dapat dilihat pengguna.

Saya sungguh-sungguh merekomendasikan Anda:

a) menunjukkan kepada pengguna akhir pesan kesalahan umum yang sama untuk setiap kesalahan yang terjadi.

b) mencatat pengecualian aktual di suatu tempat yang hanya dapat diakses oleh pengembang (jika di server) atau di suatu tempat yang dapat dikirim kepada Anda dengan alat pelaporan kesalahan (jika digunakan klien)

c) jangan mencoba dan memformat detail pengecualian kesalahan yang Anda catat kecuali Anda dapat menambahkan lebih banyak informasi berguna. Anda tidak ingin secara tidak sengaja 'memformat' satu informasi penting yang dapat Anda gunakan untuk melacak masalah.


Singkatnya - pengecualian penuh dengan informasi teknis yang sangat berguna. Semua ini tidak boleh untuk pengguna akhir dan Anda kehilangan informasi ini atas risiko Anda.

Padi
sumber
2
+ msgstr "perlihatkan kepada pengguna akhir pesan kesalahan umum yang sama untuk setiap kesalahan yang terjadi." itulah alasan utama, memformat pengecualian untuk pengguna akhir terlihat seperti hal yang mengerikan untuk dilakukan ..
Konrad
1
Dalam sistem basis data apa pun yang masuk akal, Anda harus dapat mengetahui secara terprogram mengapa sesuatu telah gagal. Seharusnya tidak perlu mengurai pesan pengecualian. Dan yang lebih umum: siapa yang mengatakan bahwa pesan kesalahan perlu ditampilkan kepada pengguna sama sekali? Anda dapat gagal memasukkan pertama dan coba lagi dalam satu lingkaran sampai Anda berhasil (atau sampai batas tertentu coba atau waktu). Dan faktanya, backoff-and-retry adalah sesuatu yang Anda ingin terapkan pada akhirnya.
Daniel Pryden