Prosedur tersimpan basis data dengan "mode pratinjau"

15

Pola yang cukup umum dalam aplikasi basis data yang saya gunakan adalah kebutuhan untuk membuat prosedur tersimpan untuk laporan atau utilitas yang memiliki "mode pratinjau". Ketika prosedur seperti itu melakukan pembaruan, parameter ini menunjukkan bahwa hasil tindakan harus dikembalikan, tetapi prosedur tersebut seharusnya tidak benar-benar melakukan pembaruan ke database.

Salah satu cara untuk mencapai hal ini adalah dengan hanya menulis ifpernyataan untuk parameter, dan memiliki dua blok kode lengkap; salah satunya melakukan pembaruan dan mengembalikan data dan yang lainnya hanya mengembalikan data. Tetapi ini tidak diinginkan karena duplikasi kode dan tingkat kepercayaan yang relatif rendah bahwa data pratinjau sebenarnya merupakan refleksi akurat tentang apa yang akan terjadi dengan pembaruan.

Contoh berikut mencoba untuk memanfaatkan savepoints transaksi dan variabel (yang tidak terpengaruh oleh transaksi, berbeda dengan temp tables yang) untuk menggunakan hanya satu blok kode untuk mode pratinjau sebagai mode pembaruan langsung.

Catatan: Kembalikan transaksi bukan merupakan opsi karena panggilan prosedur ini sendiri dapat bersarang dalam transaksi. Ini diuji pada SQL Server 2012.

CREATE TABLE dbo.user_table (a int);
GO

CREATE PROCEDURE [dbo].[PREVIEW_EXAMPLE] (
  @preview char(1) = 'Y'
) AS

CREATE TABLE #dataset_to_return (a int);

BEGIN TRANSACTION; -- preview mode required infrastructure
  DECLARE @output_to_return TABLE (a int);
  SAVE TRANSACTION savepoint;

  -- do stuff here
  INSERT INTO dbo.user_table (a)
    OUTPUT inserted.a INTO @output_to_return (a)
    VALUES (42);

  -- catch preview mode
  IF @preview = 'Y'
    ROLLBACK TRANSACTION savepoint;

  -- save output to temp table if used for return data
  INSERT INTO #dataset_to_return (a)
  SELECT a FROM @output_to_return;
COMMIT TRANSACTION;

SELECT a AS proc_return_data FROM #dataset_to_return;
RETURN 0;
GO

-- Examples
EXEC dbo.PREVIEW_EXAMPLE @preview = 'Y';
SELECT a AS user_table_after_preview_mode FROM user_table;

EXEC dbo.PREVIEW_EXAMPLE @preview = 'N';
SELECT a AS user_table_after_live_mode FROM user_table;

-- Cleanup
DROP TABLE dbo.user_table;
DROP PROCEDURE dbo.PREVIEW_EXAMPLE;
GO

Saya mencari umpan balik tentang kode ini dan pola desain, dan / atau jika solusi lain untuk masalah yang sama ada dalam format yang berbeda.

NReilingh
sumber

Jawaban:

12

Ada beberapa kelemahan dalam pendekatan ini:

  1. Istilah "pratinjau" bisa sangat menyesatkan dalam banyak kasus, tergantung pada sifat data yang dioperasikan (dan itu berubah dari operasi ke operasi). Apa yang memastikan bahwa data saat ini dioperasikan akan berada dalam keadaan yang sama antara waktu "pratinjau" data dikumpulkan dan ketika pengguna kembali 15 menit kemudian - setelah mengambil kopi, melangkah keluar untuk merokok, berjalan sekitar blok, kembali, dan memeriksa sesuatu di eBay - dan menyadari bahwa mereka tidak mengklik tombol "OK" untuk benar-benar melakukan operasi dan akhirnya mengklik tombol itu?

    Apakah Anda memiliki batas waktu untuk melanjutkan operasi setelah pratinjau dibuat? Atau mungkin cara untuk menentukan bahwa data berada dalam kondisi yang sama pada waktu modifikasi seperti pada SELECTwaktu awal ?

  2. Ini adalah poin kecil karena kode contoh bisa dilakukan dengan tergesa-gesa dan tidak mewakili kasus penggunaan yang sebenarnya, tetapi mengapa akan ada "Pratinjau" untuk INSERToperasi? Itu bisa masuk akal ketika memasukkan beberapa baris melalui sesuatu seperti INSERT...SELECTdan mungkin ada sejumlah variabel baris yang dimasukkan, tetapi ini tidak masuk akal untuk operasi tunggal.

  3. ini tidak diinginkan karena ... tingkat kepercayaan yang relatif rendah bahwa data pratinjau sebenarnya merupakan refleksi akurat tentang apa yang akan terjadi dengan pembaruan.

    Di mana tepatnya "tingkat kepercayaan rendah" ini berasal? Meskipun dimungkinkan untuk memperbarui jumlah baris yang berbeda dari yang muncul SELECTketika beberapa tabel digabungkan dan ada duplikasi baris dalam set hasil, itu seharusnya tidak menjadi masalah di sini. Setiap baris yang harus dipengaruhi oleh UPDATEdapat dipilih sendiri. Jika ada ketidaksesuaian maka Anda melakukan kueri dengan tidak benar.

    Dan situasi-situasi di mana ada duplikasi karena tabel BERGABUNG yang cocok dengan beberapa baris dalam tabel yang akan diperbarui bukanlah situasi di mana "Pratinjau" akan dihasilkan. Dan jika ada situasi di mana hal ini terjadi, maka perlu dijelaskan kepada pengguna bahwa mereka memperbarui bagian dari laporan yang diulang dalam laporan sehingga tampaknya tidak ada kesalahan jika seseorang hanya melihat jumlah baris yang terpengaruh.

  4. Demi kelengkapan (meskipun jawaban lain menyebutkan ini), Anda tidak menggunakan TRY...CATCHkonstruk sehingga dapat dengan mudah mengalami masalah saat menyarangkan panggilan ini (bahkan jika tidak menggunakan Simpan Poin, dan bahkan jika tidak menggunakan Transaksi). Silakan lihat jawaban saya untuk Pertanyaan berikut, di sini di DBA.SE, untuk templat yang menangani transaksi lintas panggilan Prosedur Tersarang:

    Apakah kita diharuskan untuk menangani Transaksi dalam Kode C # dan juga dalam prosedur tersimpan

  5. BAHKAN JIKA masalah-masalah yang disebutkan di atas dipertanggungjawabkan, masih ada kelemahan kritis: untuk periode waktu yang singkat operasi itu dilakukan (yaitu sebelum ROLLBACK), setiap kueri yang dibaca kotor (kueri yang menggunakan WITH (NOLOCK)atau SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED) dapat mengambil data yang tidak ada sesaat kemudian. Sementara siapa pun yang menggunakan kueri baca-kotor harus sudah mengetahui hal ini dan telah menerima kemungkinan itu, operasi seperti ini sangat meningkatkan peluang untuk memperkenalkan anomali data yang sangat sulit untuk di-debug (artinya: berapa banyak waktu yang ingin Anda habiskan untuk mencoba menemukan masalah yang tidak memiliki penyebab langsung yang jelas?).

  6. Pola seperti ini juga menurunkan kinerja sistem dengan meningkatkan pemblokiran dengan mengeluarkan lebih banyak kunci, dan menghasilkan lebih banyak aktivitas Log Transaksi. (Saya mengerti bahwa @MartinSmith juga menyebutkan 2 masalah ini dalam komentar pada Pertanyaan.)

    Selain itu, jika ada Pemicu pada tabel yang sedang dimodifikasi, itu bisa menjadi sedikit proses tambahan (CPU dan Fisik / Logical membaca) yang tidak perlu. Pemicu juga akan semakin meningkatkan kemungkinan anomali data yang dihasilkan dari pembacaan yang kotor.

  7. Terkait dengan poin yang disebutkan secara langsung di atas - peningkatan kunci - penggunaan Transaksi meningkatkan kemungkinan menemui kebuntuan, terutama jika Pemicu terlibat.

  8. Masalah yang tidak terlalu parah yang hanya berhubungan dengan skenario INSERToperasi yang kurang memungkinkan : data "Pratinjau" mungkin tidak sama dengan apa yang dimasukkan terkait dengan nilai kolom yang ditentukan oleh DEFAULTKendala ( Sequences/ NEWID()/ NEWSEQUENTIALID()) dan IDENTITY.

  9. Tidak perlu untuk overhead tambahan menulis isi dari Tabel Variabel ke dalam Tabel Sementara. Itu ROLLBACKtidak akan memengaruhi data dalam Table Variable (itulah sebabnya Anda mengatakan Anda menggunakan Table Variables di tempat pertama), jadi akan lebih masuk akal untuk hanya SELECT FROM @output_to_return;pada akhirnya, dan kemudian bahkan tidak repot-repot membuat Temporary. Meja.

  10. Kalau-kalau nuansa Save Points ini tidak diketahui (sulit untuk mengetahui dari kode contoh karena hanya menunjukkan satu Prosedur Tersimpan): Anda perlu menggunakan nama Simpan Poin unik sehingga ROLLBACK {save_point_name}operasi berperilaku seperti yang Anda harapkan. Jika Anda menggunakan kembali nama-nama tersebut, ROLLBACK akan memutar-mutar Save Point terbaru dari nama itu, yang mungkin tidak berada pada level bersarang yang sama dengan tempat ROLLBACKdipanggilnya. Silakan lihat contoh blok kode pertama dalam jawaban berikut untuk melihat perilaku ini dalam tindakan: Transaksi dalam prosedur tersimpan

Apa yang terjadi adalah:

  • Melakukan "Pratinjau" tidak masuk akal untuk operasi yang dihadapi pengguna. Saya sering melakukan ini untuk operasi pemeliharaan sehingga saya bisa melihat apa yang akan dihapus / Sampah Dikumpulkan jika saya melanjutkan operasi. Saya menambahkan parameter opsional yang dipanggil @TestModedan melakukan IFpernyataan yang melakukan suatu SELECTketika yang @TestMode = 1lain melakukan DELETE. Terkadang saya menambahkan @TestModeparameter ke Stored Procedures yang dipanggil oleh aplikasi sehingga saya (dan lainnya) dapat melakukan pengujian sederhana tanpa mempengaruhi keadaan data, tetapi parameter ini tidak pernah digunakan oleh aplikasi.

  • Kalau-kalau ini tidak jelas dari bagian atas "masalah":

    Jika Anda benar-benar membutuhkan / menginginkan mode "Pratinjau" / "Uji" untuk melihat apa yang harus terpengaruh jika pernyataan DML dijalankan, maka JANGAN gunakan Transaksi (yaitu BEGIN TRAN...ROLLBACKpola) untuk melakukannya. Ini adalah pola yang, paling-paling, hanya benar-benar berfungsi pada sistem pengguna tunggal, dan bahkan bukan ide yang baik dalam situasi itu.

  • Mengulang sebagian besar kueri antara dua cabang IFpernyataan memang menghadirkan masalah potensial perlu memperbarui keduanya setiap kali ada perubahan yang harus dilakukan. Namun, perbedaan antara dua kueri biasanya cukup mudah untuk ditangkap dalam tinjauan kode dan mudah diperbaiki. Di sisi lain, masalah seperti perbedaan status dan pembacaan kotor jauh lebih sulit ditemukan dan diperbaiki. Dan masalah penurunan kinerja sistem tidak mungkin diperbaiki. Kita perlu mengenali dan menerima bahwa SQL bukan bahasa Object-Oriented, dan enkapsulasi / mengurangi kode duplikasi bukan desain-tujuan SQL seperti halnya dengan banyak bahasa lain.

    Jika kueri cukup panjang / kompleks, Anda bisa merangkumnya dalam Fungsi Inline Table-Valued. Kemudian Anda dapat melakukan sederhana SELECT * FROM dbo.MyTVF(params);untuk mode "Pratinjau", dan BERGABUNG ke nilai kunci untuk mode "lakukan itu". Sebagai contoh:

    UPDATE tab
    SET    tab.Col2 = tvf.ColB
           ...
    FROM   dbo.Table tab
    INNER JOIN dbo.MyTVF(params) tvf
            ON tvf.ColA = tab.Col1;
  • Jika ini adalah skenario laporan seperti yang Anda sebutkan, maka menjalankan laporan awal adalah "Pratinjau". Jika seseorang ingin mengubah sesuatu yang mereka lihat di laporan (status mungkin), maka itu tidak memerlukan pratinjau tambahan karena harapannya adalah untuk mengubah data yang sedang ditampilkan.

    Jika operasi mungkin mengubah jumlah tawaran dengan% atau aturan bisnis tertentu, maka hal itu dapat ditangani di lapisan presentasi (JavaScript?).

  • Jika Anda benar-benar perlu melakukan "Pratinjau" untuk operasi yang dihadapi pengguna akhir , maka Anda perlu menangkap status data terlebih dahulu (mungkin hash dari semua bidang dalam hasil yang ditetapkan untuk UPDATEoperasi atau nilai-nilai kunci untuk DELETEoperasi), dan kemudian, sebelum melakukan operasi, bandingkan info status yang ditangkap dengan info saat ini - dalam Transaksi melakukan HOLDkunci di atas meja sehingga tidak ada yang berubah setelah melakukan perbandingan ini - dan jika ada perbedaan APA PUN, kesalahan dan lakukan ROLLBACKalih - alih melanjutkan dengan UPDATEatau DELETE.

    Untuk mendeteksi perbedaan UPDATEoperasi, alternatif untuk menghitung hash pada bidang yang relevan adalah menambahkan kolom tipe ROWVERSION . Nilai ROWVERSIONtipe data secara otomatis berubah setiap kali ada perubahan pada baris itu. Jika Anda memiliki kolom seperti itu, Anda akan SELECTmelakukannya bersama dengan data "Pratinjau" lainnya, dan kemudian meneruskannya ke langkah "yakin, silakan lanjutkan dan lakukan pembaruan" bersama dengan nilai kunci dan nilai kunci Untuk mengganti. Anda kemudian akan membandingkan ROWVERSIONnilai - nilai yang diteruskan dari "Pratinjau" dengan nilai saat ini (per setiap kunci), dan hanya melanjutkan dengan UPDATEjika ALLdari nilai-nilai yang cocok. Manfaatnya di sini adalah Anda tidak perlu menghitung hash yang memiliki potensi, bahkan jika tidak mungkin, untuk false-negatif, dan mengambil sejumlah waktu masing-masing dan setiap kali Anda melakukannya SELECT. Di sisi lain, ROWVERSIONnilainya bertambah secara otomatis hanya ketika diubah, jadi tidak ada yang perlu Anda khawatirkan. Namun, ROWVERSIONtipenya adalah 8 byte, yang dapat bertambah saat berurusan dengan banyak tabel dan / atau banyak baris.

    Ada pro dan kontra untuk masing-masing dari dua metode ini untuk berurusan dengan mendeteksi keadaan tidak konsisten terkaitUPDATE operasi, sehingga Anda perlu menentukan metode mana yang lebih "pro" daripada "kontra" untuk sistem Anda. Namun dalam kedua kasus tersebut, Anda dapat menghindari penundaan antara membuat Pratinjau dan melakukan operasi dari menyebabkan perilaku di luar harapan pengguna akhir.

  • Jika Anda melakukan mode "Pratinjau" yang menghadap pengguna akhir, maka selain menangkap keadaan catatan pada waktu tertentu, meneruskan, dan memeriksa pada waktu modifikasi, termasuk DATETIMEuntuk SelectTimedan mengisi melalui GETDATE()atau sesuatu yang serupa. Lewati itu ke lapisan aplikasi sehingga dapat dilewatkan kembali ke prosedur tersimpan (sebagian besar kemungkinan sebagai parameter input tunggal) sehingga dapat diperiksa dalam Prosedur Tersimpan. Kemudian Anda dapat menentukan bahwa JIKA operasi bukan mode "Pratinjau", maka @SelectTimenilainya harus tidak lebih dari X menit sebelum nilai saat ini GETDATE(). Mungkin 2 menit? 5 menit? Kemungkinan besar tidak lebih dari 10 menit. Lempar kesalahan jika DATEDIFFdalam MINUTES melebihi ambang itu.

Solomon Rutzky
sumber
4

Pendekatan paling sederhana sering yang terbaik dan saya tidak benar-benar memiliki banyak masalah dengan duplikasi kode dalam SQL, terutama tidak dalam modul yang sama. Lagipula kedua pertanyaan itu melakukan hal yang berbeda. Jadi mengapa tidak mengambil 'Route 1' atau Keep It Simple dan hanya memiliki dua bagian di proc yang disimpan, satu untuk mensimulasikan pekerjaan yang perlu Anda lakukan dan satu untuk melakukannya, misalnya sesuatu seperti ini:

CREATE TABLE dbo.user_table ( rowId INT IDENTITY PRIMARY KEY, a INT NOT NULL, someGuid UNIQUEIDENTIFIER DEFAULT NEWID() );
GO
CREATE PROCEDURE [dbo].[PREVIEW_EXAMPLE2]

    @preview CHAR(1) = 'Y'

AS

    SET NOCOUNT ON

    --!!TODO add error handling

    IF @preview = 'Y'

        -- Simulate INSERT; could be more complex
        SELECT 
            ISNULL( ( SELECT MAX(rowId) FROM dbo.user_table ), 0 ) + 1 AS rowId,
            42 AS a,
            NEWID() AS someGuid

    ELSE

        -- Actually do the INSERT, return inserted values
        INSERT INTO dbo.user_table ( a )
        OUTPUT inserted.rowId, inserted.a, inserted.someGuid
        VALUES ( 42 )

    RETURN

GO

Ini memiliki keuntungan mendokumentasikan diri (yaitu IF ... ELSE mudah diikuti), kompleksitas rendah (dibandingkan dengan titik penyimpanan dengan pendekatan variabel tabel IMO), oleh karena itu lebih kecil kemungkinannya untuk memiliki bug (tempat yang bagus dari @Cody).

Mengenai poin Anda pada kepercayaan rendah, saya tidak yakin saya mengerti. Secara logis dua pertanyaan dengan kriteria yang sama harus melakukan hal yang sama. Ada kemungkinan ketidakcocokan kardinalitas antara an UPDATEdan aSELECT , tetapi akan menjadi fitur gabungan dan kriteria Anda. Bisakah Anda jelaskan lebih lanjut?

Sebagai tambahan, Anda harus mengatur NULLNOT NULL variabel / properti dan tabel dan tabel Anda, pertimbangkan untuk menetapkan kunci utama.

Pendekatan awal Anda tampaknya sedikit terlalu rumit mungkin bisa lebih rentan terhadap kebuntuan, karena INSERT/ UPDATE/ DELETEoperasi memerlukan tingkat penguncian yang lebih tinggi daripada biasa SELECTs.

Saya menduga procs dunia nyata Anda lebih rumit, jadi jika Anda merasa pendekatan di atas tidak akan berhasil untuk mereka, kirim kembali dengan beberapa contoh lagi.

wBob
sumber
3

Kekhawatiran saya adalah sebagai berikut.

  • Penanganan transaksi tidak mengikuti pola standar yang disarangkan di blok Mulai Coba / Mulai Tangkap. Jika ini adalah templat maka dalam prosedur tersimpan dengan beberapa langkah lagi Anda bisa keluar dari transaksi ini dalam mode pratinjau dengan data yang masih dimodifikasi.

  • Mengikuti format meningkatkan kerja pengembang. Jika mereka mengubah kolom internal mereka kemudian juga perlu memodifikasi definisi variabel tabel, kemudian memodifikasi definisi tabel temp, kemudian memodifikasi kolom sisipkan di akhir. Itu tidak akan menjadi populer.

  • Beberapa prosedur tersimpan tidak mengembalikan format data yang sama setiap kali; pikirkan sp_WhoIsActive sebagai contoh umum.

Saya belum menyediakan cara yang lebih baik untuk melakukannya tetapi saya tidak berpikir apa yang Anda miliki adalah pola yang baik.

Cody Konior
sumber