Melewati info tentang siapa yang menghapus rekaman ke pemicu Hapus


11

Dalam menyiapkan jejak audit saya tidak memiliki masalah melacak siapa yang memperbarui atau menyisipkan catatan dalam sebuah tabel, namun, pelacakan yang menghapus catatan tampaknya lebih bermasalah.

Saya dapat melacak Sisipan / Pembaruan dengan memasukkan dalam bidang Sisipkan / Perbarui "Diperbarui oleh". Ini memungkinkan pemicu INSERT / UPDATE untuk memiliki akses ke bidang "UpdatedBy" via inserted.UpdatedBy. Namun, dengan pemicu Hapus tidak ada data yang dimasukkan / diperbarui. Apakah ada cara untuk meneruskan informasi ke pemicu Hapus sehingga dapat mengetahui siapa yang menghapus catatan?

Berikut ini adalah pemicu Sisipkan / Perbarui

ALTER TRIGGER [dbo].[trg_MyTable_InsertUpdate] 
ON [dbo].[MyTable]
FOR INSERT, UPDATE
AS  

INSERT INTO AuditTable (IdOfRecordedAffected, UserWhoMadeChanges) 
VALUES (inserted.ID, inserted.LastUpdatedBy)
FROM inserted 

Menggunakan SQL Server 2012


1
Lihat jawaban ini . SUSER_SNAME()adalah kunci untuk mendapatkan siapa yang menghapus data.
Kin Shah

1
Terima kasih Kin, namun saya tidak berpikir SUSER_SNAME()akan berfungsi dalam situasi seperti aplikasi web di mana satu pengguna dapat digunakan untuk komunikasi database untuk seluruh aplikasi.
webworm

1
Anda tidak menyebutkan bahwa Anda memanggil aplikasi web.
Kin Shah

Maaf Kin, saya seharusnya lebih spesifik untuk jenis aplikasi.
webworm

Jawaban:


10

Apakah ada cara untuk meneruskan informasi ke pemicu Hapus sehingga dapat mengetahui siapa yang menghapus catatan?

Ya: dengan menggunakan fitur yang sangat keren (dan kurang dimanfaatkan) disebut CONTEXT_INFO. Ini pada dasarnya adalah memori sesi yang ada di semua ruang lingkup dan tidak terikat oleh transaksi. Ini dapat digunakan untuk meneruskan info (info apa pun - yah, apa pun yang sesuai dengan ruang terbatas) untuk memicu serta bolak-balik antara panggilan sub-proc / EXEC. Dan saya telah menggunakannya sebelumnya untuk situasi yang sama persis ini.

Uji dengan yang berikut ini untuk melihat cara kerjanya. Perhatikan bahwa saya masuk CHAR(128)sebelum CONVERT(VARBINARY(128), ... Ini untuk memaksa pad-kosong agar lebih mudah untuk mengkonversi kembali VARCHARketika keluar CONTEXT_INFO()karena VARBINARY(128)padded dengan 0x00s.

SELECT CONTEXT_INFO();
-- Initially = NULL

DECLARE @EncodedUser VARBINARY(128);
SET @EncodedUser = CONVERT(VARBINARY(128),
                            CONVERT(CHAR(128), 'I deleted ALL your records! HA HA!')
                          );
SET CONTEXT_INFO @EncodedUser;

SELECT CONTEXT_INFO() AS [RawContextInfo],
       RTRIM(CONVERT(VARCHAR(128), CONTEXT_INFO())) AS [DecodedUser];

Hasil:

0x492064656C6574656420414C4C20796F7572207265636F7264732120484120484121202020202020...
I deleted ALL your records! HA HA!

MENEMPATKAN SEMUA BERSAMA:

  1. Aplikasi harus memanggil prosedur tersimpan "Hapus" yang dilewati di UserName (atau apa pun) yang menghapus catatan. Saya berasumsi ini sudah model yang digunakan karena sepertinya Anda sudah melacak operasi Sisipkan dan Perbarui.

  2. Prosedur "Hapus" yang disimpan tidak:

    DECLARE @EncodedUser VARBINARY(128);
    SET @EncodedUser = CONVERT(VARBINARY(128),
                                CONVERT(CHAR(128), @UserName)
                              );
    SET CONTEXT_INFO @EncodedUser;
    
    -- DELETE STUFF HERE
  3. Pemicu audit tidak:

    -- Set the INT value in LEFT (currently 50) to the max size of [UserWhoMadeChanges]
    INSERT INTO AuditTable (IdOfRecordedAffected, UserWhoMadeChanges) 
       SELECT del.ID, COALESCE(
                         LEFT(RTRIM(CONVERT(VARCHAR(128), CONTEXT_INFO())), 50),
                         '<unknown>')
       FROM DELETED del;
  4. Harap perhatikan bahwa, seperti yang @SeanGallardy tunjukkan dalam komentar, karena prosedur lain dan / atau kueri ad hoc menghapus catatan dari tabel ini, ada kemungkinan bahwa:

    • CONTEXT_INFObelum diatur dan masih NULL:

      Untuk alasan ini saya telah memperbarui di atas INSERT INTO AuditTableuntuk menggunakan nilai COALESCEdefault. Atau, jika Anda tidak ingin default dan memerlukan nama, maka Anda dapat melakukan sesuatu yang mirip dengan:

      DECLARE @UserName VARCHAR(50); -- set to the size of AuditTable.[UserWhoMadeChanges]
      SET @UserName = LEFT(RTRIM(CONVERT(VARCHAR(128), CONTEXT_INFO())), 50);
      
      IF (@UserName IS NULL)
      BEGIN
         ROLLBACK TRAN; -- cancel the DELETE operation
         RAISERROR('Please set UserName via "SET CONTEXT_INFO.." and try again.', 16 ,1);
      END;
      
      -- use @UserName in the INSERT...SELECT
    • CONTEXT_INFOtelah disetel ke nilai yang bukan UserName yang valid, dan karenanya mungkin melebihi ukuran AuditTable.[UserWhoMadeChanges]bidang:

      Untuk alasan ini saya menambahkan LEFTfungsi untuk memastikan bahwa apa pun yang diambil CONTEXT_INFOtidak akan merusak INSERT. Seperti disebutkan dalam kode, Anda hanya perlu mengatur 50ukuran UserWhoMadeChangesbidang yang sebenarnya.


PEMBARUAN UNTUK SQL SERVER 2016 DAN BARU

SQL Server 2016 menambahkan versi yang disempurnakan dari memori per-sesi ini: Session Context. Konteks Sesi baru pada dasarnya adalah tabel hash pasangan Key-Value dengan tipe "Key" sysname(yaitu NVARCHAR(128)) dan "Value" SQL_VARIANT. Berarti:

  1. Sekarang ada pemisahan nilai sehingga lebih kecil kemungkinannya bertentangan dengan kegunaan lain
  2. Anda dapat menyimpan berbagai jenis, tidak perlu lagi khawatir tentang perilaku aneh ketika mendapatkan kembali nilai melalui CONTEXT_INFO()(untuk detail, silakan lihat posting saya: Mengapa Tidak CONTEXT_INFO () Mengembalikan Nilai Tepat yang Ditetapkan oleh SET CONTEXT_INFO? )
  3. Anda mendapatkan lebih banyak ruang: maks 8000 byte per "Nilai", hingga total 256kb di semua kunci (dibandingkan dengan 128 byte maks CONTEXT_INFO)

Untuk detailnya, silakan lihat halaman dokumentasi berikut:


Masalah dengan pendekatan ini adalah sangat SANGAT mudah menguap. Sesi apa pun dapat mengatur ini, karena itu dapat menimpa item yang sebelumnya diatur. Ingin merusak aplikasi Anda? memiliki satu dev menimpa apa yang Anda harapkan. Saya akan sangat menyarankan TIDAK untuk menggunakan ini dan memiliki pendekatan standar yang mungkin memerlukan perubahan arsitektur. Kalau tidak, Anda bermain api.
Sean Gallardy

@SeanGallardy Bisakah Anda memberikan contoh aktual tentang kejadian ini? Sesi == @@SPID. Ini adalah memori PER-Sesi / Koneksi. Satu sesi tidak dapat menimpa informasi konteks sesi lain. Dan ketika sesi log off nilai hilang. Tidak ada yang namanya "item yang ditetapkan sebelumnya".
Solomon Rutzky

1
Saya tidak mengatakan "sesi lain". Saya mengatakan objek apa pun dalam cakupan sesi bisa melakukan ini. Jadi, seorang dev menulis sproc untuk menyimpan informasi "kontekstual" miliknya sendiri dan sekarang milik Anda ditimpa. Ada aplikasi yang harus saya tangani yang menggunakan pola yang sama, saya perhatikan itu terjadi ... itu adalah perangkat lunak SDM. Biarkan saya memberi tahu Anda betapa bahagianya orang-orang TIDAK dibayar tepat waktu karena "bug" oleh salah satu pengembang menulis SP baru yang keliru memperbarui informasi konteks untuk sesi dari apa yang "seharusnya". Hanya memberi contoh saya sudah benar-benar menyaksikan mengapa tidak menggunakan metode ini.
Sean Gallardy

@SeanGallardy Ok, terima kasih telah menjelaskan poin itu. Tapi itu masih hanya sebagian yang valid. Agar situasi itu terjadi, proc "lain" itu harus dipanggil bagian dalam dari yang ini. Atau, jika Anda berbicara tentang beberapa proc lain yang mungkin dihapus dari tabel ini dan menendang pelatuknya, itu adalah sesuatu yang dapat diuji. Ini adalah kondisi balapan, yang merupakan sesuatu yang harus dipertanggungjawabkan (sama seperti mereka ada di semua aplikasi multithreaded), dan bukan alasan untuk tidak menggunakan teknik ini. Jadi saya akan membuat pembaruan kecil untuk melakukan hal itu. Terima kasih telah mengangkat kemungkinan ini.
Solomon Rutzky

2
Saya mengatakan keamanan sebagai pikiran setelah adalah masalah utama dan ini bukan alat untuk menyelesaikannya. Struktur memo atau kegunaan lain yang tidak merusak aplikasi, tentu saya tidak punya masalah. Ini benar-benar alasan untuk TIDAK menggunakannya. YMMV tapi saya tidak akan pernah menggunakan sesuatu yang begitu mudah berubah dan tidak terstruktur untuk sesuatu yang penting seperti keamanan. Menggunakan semua jenis penyimpanan yang dapat ditulis pengguna bersama untuk keamanan adalah ide yang buruk secara keseluruhan. Desain yang tepat akan menghilangkan kebutuhan untuk hal-hal seperti ini, sebagian besar.
Sean Gallardy

5

Anda tidak bisa seperti itu, kecuali jika Anda ingin merekam ID pengguna server SQL daripada tingkat aplikasi.

Anda dapat melakukan soft delete dengan memiliki kolom bernama DeletedBy dan pengaturan yang diperlukan, maka pemicu pembaruan Anda dapat melakukan penghapusan yang sebenarnya (atau mengarsipkan catatan, saya biasanya menghindari penghapusan yang sulit jika mungkin dan legal) serta memperbarui jejak audit Anda . Untuk memaksa penghapusan dilakukan dengan cara itu, tentukan on deletepemicu yang memunculkan kesalahan. Jika Anda tidak ingin menambahkan kolom ke tabel fisik Anda, Anda bisa menentukan tampilan yang menambahkan kolom dan menentukan instead ofpemicu untuk menangani memperbarui tabel dasar, tetapi itu mungkin berlebihan.


Saya mengerti maksud Anda. Saya memang ingin mencari pengguna tingkat aplikasi.
webworm

David, sebenarnya Anda bisa meneruskan info ke pemicu. Silakan lihat jawaban saya untuk detailnya :).
Solomon Rutzky

Saran bagus di sini, saya sangat suka rute ini. Membunuh dua burung dengan menangkap Who dengan langkah yang sama dengan memicu delete yang sebenarnya. Karena kolom ini akan menjadi NULL untuk setiap catatan dalam tabel ini, sepertinya itu akan menjadi penggunaan yang baik dari SPARSEkolom SQL Server ?
Airn5475

2

Apakah ada cara untuk meneruskan informasi ke pemicu Hapus sehingga dapat mengetahui siapa yang menghapus catatan?

Ya, ternyata ada dua cara ;-). Jika ada keraguan tentang penggunaan CONTEXT_INFOseperti yang saya sarankan dalam jawaban saya yang lain di sini , saya hanya memikirkan cara lain yang memiliki pemisahan fungsional yang lebih bersih dari kode / proses lain: gunakan tabel sementara lokal.

Nama tabel temp harus menyertakan nama tabel yang dihapus karena itu akan membuatnya tetap terpisah dari kode lain yang mungkin dijalankan di sesi yang sama. Sesuatu di sepanjang garis:
#<TableName>DeleteAudit

Salah satu manfaat dari temp tabel lokal CONTEXT_INFOadalah bahwa jika seseorang di proc lain - yang entah bagaimana memanggil dari proc "Delete" ini - kebetulan salah menggunakan nama tabel temp yang sama, subproses akan a) membuat lokal baru tabel temp dari nama yang diminta yang akan terpisah dari tabel temp awal ini (meskipun memiliki nama yang sama), dan b) setiap pernyataan DML terhadap tabel temp lokal baru dalam sub-proses tidak akan mempengaruhi data apa pun di tabel temp lokal dibuat di sini dalam proses induk, karenanya tidak ada penimpaan data. Tentu saja, jika subproses mengeluarkan pernyataan DML terhadap nama tabel temp ini tanpa terlebih dahulu menerbitkan CREATE TABLE dengan nama yang sama, maka pernyataan DML tersebut akan memengaruhi data dalam tabel ini. NAMUN, pada titik ini kita benar - benar mendapatkanedge-casey di sini, bahkan lebih daripada dengan kemungkinan tumpang tindih penggunaan CONTEXT_INFO(ya, saya tahu itu telah terjadi, itulah sebabnya saya mengatakan "edge-case" daripada "itu tidak akan pernah terjadi").

  1. Aplikasi harus memanggil prosedur tersimpan "Hapus" yang dilewati di UserName (atau apa pun) yang menghapus catatan. Saya berasumsi ini sudah model yang digunakan karena sepertinya Anda sudah melacak operasi Sisipkan dan Perbarui.

  2. Prosedur "Hapus" yang disimpan tidak:

    CREATE TABLE #MyTableDeleteAudit (UserName VARCHAR(50));
    INSERT INTO #MyTableDeleteAudit (UserName) VALUES (@UserName);
    
    -- DELETE STUFF HERE
  3. Pemicu audit tidak:

    -- Set the datatype and length to be the same as the [UserWhoMadeChanges] field
    DECLARE @UserName VARCHAR(50);
    IF (OBJECT_ID(N'tempdb..#TriggerTestDeleteAudit') IS NOT NULL)
    BEGIN
       SELECT @UserName = UserName
       FROM #TriggerTestDeleteAudit;
    END;
    
    -- catch the following conditions: missing table, no rows in table, or empty row
    IF (@UserName IS NULL OR @UserName NOT LIKE '%[a-z]%')
    BEGIN
      /* -- uncomment if undefined UserName == badness
       ROLLBACK TRAN; -- cancel the DELETE operation
       RAISERROR('Please set UserName via #TriggerTestDeleteAudit and try again.', 16 ,1);
       RETURN; -- exit
      */
      /* -- uncomment if undefined UserName gets default value
       SET @UserName = '<unknown>';
      */
    END;
    
    INSERT INTO AuditTable (IdOfRecordedAffected, UserWhoMadeChanges) 
       SELECT del.ID, @UserName
       FROM DELETED del;

    Saya telah menguji kode ini di pemicu dan berfungsi seperti yang diharapkan.

Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.