การส่งข้อมูลเกี่ยวกับผู้ที่ลบระเบียนไปยังทริกเกอร์ลบ


11

ในการตั้งค่าหลักฐานการตรวจสอบฉันไม่มีปัญหาในการติดตามว่าใครกำลังอัปเดตหรือแทรกระเบียนในตารางอย่างไรก็ตามการติดตามผู้ที่ลบระเบียนดูเหมือนว่าจะมีปัญหามากกว่า

ฉันสามารถติดตามส่วนแทรก / อัพเดตได้โดยใส่ในส่วนแทรก / อัปเดตฟิลด์ "UpdatedBy" นี้จะช่วยให้เรียก INSERT / UPDATE ให้มีการเข้าถึงช่อง "UpdatedBy" inserted.UpdatedByผ่านทาง อย่างไรก็ตามด้วยการลบทริกเกอร์ไม่มีข้อมูลถูกแทรก / ปรับปรุง มีวิธีการส่งผ่านข้อมูลไปยังทริกเกอร์การลบเพื่อที่จะได้รู้ว่าใครเป็นผู้ลบบันทึกหรือไม่?

นี่คือทริกเกอร์การแทรก / อัปเดต

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 

ใช้ SQL Server 2012


1
ดูนี้คำตอบ SUSER_SNAME()เป็นกุญแจสำคัญในการรับผู้ที่ลบบันทึก
Kin Shah

1
ขอบคุณ Kin แต่ฉันไม่คิดว่าSUSER_SNAME()จะทำงานในสถานการณ์เช่นเว็บแอปพลิเคชันซึ่งอาจมีผู้ใช้รายเดียวสำหรับการสื่อสารฐานข้อมูลสำหรับแอปพลิเคชันทั้งหมด
webworm

1
คุณไม่ได้พูดถึงว่าคุณกำลังเรียกใช้เว็บแอป
Kin Shah

ขออภัย Kin ฉันควรระบุประเภทแอปพลิเคชันให้ชัดเจนยิ่งขึ้น
webworm

คำตอบ:


10

มีวิธีการส่งผ่านข้อมูลไปยังทริกเกอร์การลบเพื่อที่จะได้รู้ว่าใครเป็นผู้ลบบันทึกหรือไม่?

ใช่: โดยใช้มากเย็น (และภายใต้คุณลักษณะใช้) CONTEXT_INFOเรียกว่า เป็นหน่วยความจำเซสชันหลักที่มีอยู่ในขอบเขตทั้งหมดและไม่ผูกพันตามธุรกรรม มันสามารถใช้ในการส่งผ่านข้อมูล (ข้อมูลใด ๆ - ดีใด ๆ ที่เหมาะกับพื้นที่ จำกัด ) เพื่อเรียกและกลับมาระหว่างการเรียก sub-proc / EXEC และฉันเคยใช้มันมาก่อนในสถานการณ์เดียวกันนี้

ทดสอบกับสิ่งต่อไปนี้เพื่อดูว่ามันทำงานอย่างไร ขอให้สังเกตว่าฉันกำลังแปลงไปก่อนCHAR(128) CONVERT(VARBINARY(128), ..นี่คือการบังคับให้เว้นว่างไว้เพื่อให้ง่ายต่อการแปลงกลับไปเป็นVARCHARเมื่อได้รับมันออกมาCONTEXT_INFO()เนื่องจากVARBINARY(128)มีบุด้วย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];

ผล:

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

วางมันทั้งหมดเข้าด้วยกัน:

  1. แอปควรเรียกขั้นตอนการจัดเก็บ "ลบ" ที่ส่งผ่านในชื่อผู้ใช้ (หรืออะไรก็ตาม) ที่จะลบบันทึก ฉันคิดว่านี่เป็นรุ่นที่ใช้อยู่แล้วเพราะดูเหมือนว่าคุณกำลังติดตามการแทรกและการอัปเดตอยู่แล้ว

  2. กระบวนงานที่เก็บไว้ "ลบ" ไม่:

    DECLARE @EncodedUser VARBINARY(128);
    SET @EncodedUser = CONVERT(VARBINARY(128),
                                CONVERT(CHAR(128), @UserName)
                              );
    SET CONTEXT_INFO @EncodedUser;
    
    -- DELETE STUFF HERE
  3. ทริกเกอร์การตรวจสอบทำ:

    -- 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. โปรดทราบว่าเนื่องจาก @SeanGallardy ชี้ให้เห็นในความคิดเห็นเนื่องจากขั้นตอนอื่น ๆ และ / หรือแบบสอบถามเฉพาะกิจการลบบันทึกจากตารางนี้อาจเป็นไปได้ว่า:

    • CONTEXT_INFOยังไม่ได้ตั้งค่าและยังคงNULL:

      ด้วยเหตุนี้ฉันได้อัปเดตข้างต้นINSERT INTO AuditTableเพื่อใช้COALESCEค่าเริ่มต้น หรือหากคุณไม่ต้องการค่าเริ่มต้นและต้องการชื่อคุณสามารถทำสิ่งที่คล้ายกับ:

      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_INFOได้รับการตั้งค่าเป็นชื่อผู้ใช้ที่ไม่ถูกต้องและอาจเกินขนาดของAuditTable.[UserWhoMadeChanges]ฟิลด์:

      ด้วยเหตุนี้ฉันเพิ่มLEFTฟังก์ชั่นเพื่อให้มั่นใจว่าสิ่งที่จะคว้าออกมาจะไม่ทำลายCONTEXT_INFO INSERTตามที่ระบุไว้ในรหัสคุณเพียงแค่ต้องตั้งค่า50ขนาดจริงของUserWhoMadeChangesฟิลด์


อัปเดตสำหรับ SQL Server 2016 และใหม่กว่า

SQL Server 2016 เพิ่มหน่วยความจำต่อเซสชันที่ปรับปรุงแล้วนี้: บริบทบริบท บริบทเซสชันใหม่เป็นหลักตารางแฮชของคู่ค่าคีย์กับ "กุญแจ" เป็นประเภทsysname(เช่นNVARCHAR(128)) และ "ราคา" SQL_VARIANTเป็นอยู่ ความหมาย:

  1. ขณะนี้มีการแยกค่าเพื่อให้มีโอกาสน้อยที่จะขัดแย้งกับการใช้งานอื่น
  2. คุณสามารถจัดเก็บประเภทต่าง ๆ ไม่จำเป็นต้องกังวลเกี่ยวกับพฤติกรรมแปลก ๆ เมื่อรับค่ากลับมาทางCONTEXT_INFO()(สำหรับรายละเอียดโปรดดูโพสต์ของฉัน: ทำไมไม่ CONTEXT_INFO () ส่งคืนค่าที่แน่นอนที่กำหนดโดย SET CONTEXT_INFO? )
  3. คุณได้รับพื้นที่มากขึ้น: สูงสุด 8000 ไบต์ต่อ "ค่า", รวมได้ถึง 256kb สำหรับทุกปุ่ม (เทียบกับ 128 ไบต์สูงสุดCONTEXT_INFO)

สำหรับรายละเอียดโปรดดูหน้าเอกสารประกอบต่อไปนี้:


ปัญหาของวิธีนี้คือมันมีความผันผวนมาก เซสชั่นใด ๆ สามารถตั้งค่านี้เพราะมันสามารถเขียนทับรายการที่ตั้งไว้ก่อนหน้านี้ ต้องการทำลายใบสมัครของคุณหรือไม่ มี dev เดียวเขียนทับสิ่งที่คุณคาดหวัง ฉันขอแนะนำว่าอย่าใช้สิ่งนี้และมีวิธีการมาตรฐานซึ่งอาจต้องมีการเปลี่ยนแปลงสถาปัตยกรรม มิฉะนั้นคุณกำลังเล่นด้วยไฟ
Sean Gallardy

@SeanGallardy คุณช่วยยกตัวอย่างจริงของเหตุการณ์นี้ได้ไหม? เซสชั่น @@SPID== นี่คือหน่วยความจำต่อเซสชัน / การเชื่อมต่อ หนึ่งเซสชันไม่สามารถเขียนทับข้อมูลบริบทของเซสชันอื่นได้ และเมื่อเซสชั่นออกจากระบบค่าจะหายไป ไม่มีสิ่งเช่น "รายการที่ตั้งไว้ก่อนหน้านี้"
โซโลมอน Rutzky

1
ฉันไม่ได้พูดว่า "เซสชันอื่น" ฉันพูดว่าวัตถุใด ๆ ในขอบเขตเซสชันสามารถทำได้ ดังนั้นหนึ่ง dev เขียน sproc เพื่อเก็บข้อมูล "บริบท" ของเขาเองและตอนนี้คุณจะถูกเขียนทับ มีแอปพลิเคชันที่ฉันต้องจัดการกับสิ่งนั้นที่ใช้รูปแบบเดียวกันนี้ฉันได้ดูมันเกิดขึ้น ... มันเป็นซอฟต์แวร์ HR ให้ฉันบอกคุณว่าคนที่มีความสุขจะไม่ได้รับเงินตรงเวลาเนื่องจาก "บั๊ก" โดยหนึ่งใน devs เขียน SP ใหม่ที่ปรับปรุงข้อมูลบริบทสำหรับเซสชันอย่างไม่ถูกต้องจากสิ่งที่มันควรจะเป็น แค่ยกตัวอย่างฉันเห็นจริง ๆ แล้วว่าทำไมไม่ใช้วิธีนี้
Sean Gallardy

@SeanGallardy Ok ขอบคุณที่ชี้แจงจุดนี้ แต่มันก็ยังคงเป็นเพียงจุดที่ถูกต้องเพียงบางส่วน เพื่อให้สถานการณ์เกิดขึ้นต้องเรียก proc "อื่น ๆ " ข้างในของอันนี้ หรือถ้าคุณกำลังพูดถึง proc อื่น ๆ ที่อาจลบออกจากตารางนี้และเริ่มต้นทริกเกอร์นั่นคือสิ่งที่สามารถทดสอบได้ มันเป็นเงื่อนไขการแข่งขันซึ่งเป็นสิ่งที่ต้องคำนึงถึง (เช่นเดียวกับในแอพพลิเคชั่นแบบมัลติเธรดทั้งหมด) และไม่ใช่เหตุผลที่จะไม่ใช้เทคนิคนี้ ดังนั้นฉันจะทำการอัพเดทเล็กน้อยเพื่อทำเช่นนั้น ขอบคุณที่นำความเป็นไปได้นี้ไปใช้
โซโลมอน Rutzky

2
ฉันกำลังบอกว่าการรักษาความปลอดภัยตามความคิดเป็นปัญหาหลักและนี่ไม่ใช่เครื่องมือในการแก้ปัญหา โครงสร้างบันทึกหรือการใช้งานอื่น ๆ ที่ไม่ทำลายแอปพลิเคชันแน่นอนว่าฉันไม่มีปัญหา มันเป็นเหตุผลที่จะไม่ใช้มันอย่างแน่นอน YMMV แต่ฉันจะไม่ใช้สิ่งที่ผันผวนและไม่มีโครงสร้างสำหรับสิ่งที่สำคัญเช่นความปลอดภัย การใช้พื้นที่เก็บข้อมูลที่ผู้ใช้สามารถเขียนได้เพื่อความปลอดภัยเป็นความคิดที่แย่มาก การออกแบบที่เหมาะสมจะขจัดความต้องการสิ่งต่าง ๆ เช่นนี้เป็นส่วนใหญ่
Sean Gallardy

5

คุณไม่สามารถทำเช่นนั้นได้เว้นแต่ว่าคุณต้องการบันทึก ID ผู้ใช้เซิร์ฟเวอร์ SQL แทนที่จะเป็นแอปพลิเคชันระดับหนึ่ง

คุณสามารถทำการลบแบบ soft โดยใช้คอลัมน์ชื่อว่า DeletedBy และการตั้งค่าตามที่ต้องการจากนั้นทริกเกอร์การอัปเดตของคุณสามารถทำการลบจริง (หรือเก็บถาวรเรคคอร์ดโดยทั่วไปแล้วฉันมักจะหลีกเลี่ยงการลบอย่างหนัก . เพื่อบังคับให้ทำการลบวิธีนั้นจะกำหนดon deleteทริกเกอร์ที่ทำให้เกิดข้อผิดพลาด หากคุณไม่ต้องการเพิ่มคอลัมน์ลงในตารางทางกายภาพของคุณคุณสามารถกำหนดมุมมองที่เพิ่มคอลัมน์และกำหนดinstead ofทริกเกอร์เพื่อจัดการการอัปเดตตารางฐาน แต่อาจจะเกินขีด จำกัด


ฉันเห็นประเด็นของคุณ แน่นอนฉันต้องการค้นหาผู้ใช้ระดับแอปพลิเคชัน
webworm

เดวิดจริง ๆ แล้วคุณสามารถส่งผ่านข้อมูลไปยังทริกเกอร์ โปรดดูคำตอบของฉันสำหรับรายละเอียด :)
โซโลมอน Rutzky

คำแนะนำที่ดีที่นี่ฉันชอบเส้นทางนี้จริงๆ ฆ่านกสองตัวโดยการจับผู้ที่อยู่ในขั้นตอนเดียวกับการทริกเกอร์การลบจริง เนื่องจากคอลัมน์นี้จะเป็น NULL สำหรับทุกระเบียนในตารางนี้ดูเหมือนว่าจะเป็นการใช้SPARSEคอลัมน์SQL Server ได้ดีหรือไม่
Airn5475

2

มีวิธีการส่งผ่านข้อมูลไปยังทริกเกอร์การลบเพื่อที่จะได้รู้ว่าใครเป็นผู้ลบบันทึกหรือไม่?

ใช่เห็นได้ชัดว่ามีสองวิธี ;-) หากมีการจองใด ๆ เกี่ยวกับการใช้CONTEXT_INFOตามที่ฉันได้แนะนำไว้ในคำตอบอื่น ๆ ของฉันที่นี่ฉันแค่คิดถึงวิธีอื่นที่มีการแยกฟังก์ชันการทำงานที่สะอาดกว่าจากโค้ด / กระบวนการอื่น: ใช้ตารางชั่วคราวท้องถิ่น

ชื่อตาราง temp ควรมีชื่อตารางที่จะถูกลบเนื่องจากจะช่วยให้แยกจากรหัสอื่น ๆ ที่อาจเกิดขึ้นให้ทำงานในเซสชันเดียวกัน บางสิ่งบางอย่างตาม:
#<TableName>DeleteAudit

ข้อดีอย่างหนึ่งของตาราง temp ในพื้นที่CONTEXT_INFOคือถ้ามีคนใน proc อื่น - นั่นคือการเรียกจาก "ลบ" proc นี้ - เกิดขึ้นกับการใช้ชื่อตาราง temp ที่ไม่ถูกต้องกระบวนการย่อยจะ a) สร้าง local ใหม่ ตาราง temp ของชื่อที่ร้องขอซึ่งจะแยกจากตาราง temp เริ่มต้นนี้ (แม้ว่าจะมีชื่อเดียวกัน) และ b) คำสั่ง DML ใด ๆ เทียบกับตาราง temp ใหม่ในกระบวนการย่อยจะไม่ส่งผลกระทบต่อข้อมูลใด ๆ ใน ตาราง temp ท้องถิ่นที่สร้างขึ้นที่นี่ในกระบวนการหลักจึงไม่มีการเขียนทับข้อมูล แน่นอนหากกระบวนการย่อยออกคำสั่ง DML กับชื่อตาราง temp นี้โดยไม่ต้องออก CREATE TABLE ของชื่อเดียวกันนั้นคำสั่ง DML เหล่านั้นจะมีผลต่อข้อมูลในตารางนี้ แต่ ณ จุดนี้เราได้รับจริงๆedge-casey ที่นี่ยิ่งกว่าความน่าจะเป็นของการใช้งานที่ทับซ้อนกันCONTEXT_INFO(ใช่ฉันรู้ว่ามันเกิดขึ้นซึ่งเป็นเหตุผลที่ฉันพูดว่า "edge-case" มากกว่า "มันจะไม่เกิดขึ้น")

  1. แอปควรเรียกขั้นตอนการจัดเก็บ "ลบ" ที่ส่งผ่านในชื่อผู้ใช้ (หรืออะไรก็ตาม) ที่จะลบบันทึก ฉันคิดว่านี่เป็นรุ่นที่ใช้อยู่แล้วเพราะดูเหมือนว่าคุณกำลังติดตามการแทรกและการอัปเดตอยู่แล้ว

  2. กระบวนงานที่เก็บไว้ "ลบ" ไม่:

    CREATE TABLE #MyTableDeleteAudit (UserName VARCHAR(50));
    INSERT INTO #MyTableDeleteAudit (UserName) VALUES (@UserName);
    
    -- DELETE STUFF HERE
  3. ทริกเกอร์การตรวจสอบทำ:

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

    ฉันได้ทดสอบโค้ดนี้ในทริกเกอร์และทำงานตามที่คาดไว้

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.