Truyền thông tin về người đã xóa bản ghi vào trình kích hoạt Xóa


11

Khi thiết lập một lộ trình kiểm toán, tôi không gặp vấn đề gì trong việc theo dõi ai đang cập nhật hoặc chèn hồ sơ vào bảng, tuy nhiên, việc theo dõi ai xóa hồ sơ có vẻ có vấn đề hơn.

Tôi có thể theo dõi Phụ trang / Cập nhật bằng cách đưa vào phần Chèn / Cập nhật trường "Cập nhậtBy". Điều này cho phép trình kích hoạt INSERT / UPDATE có quyền truy cập vào trường "UpdateBy" thông qua inserted.UpdatedBy. Tuy nhiên, với trình kích hoạt Xóa, không có dữ liệu nào được chèn / cập nhật. Có cách nào để truyền thông tin vào trình kích hoạt Xóa để nó có thể biết ai đã xóa bản ghi không?

Đây là một kích hoạt Chèn / Cập nhật

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 

Sử dụng SQL Server 2012


1
Xem câu trả lời này . SUSER_SNAME()là chìa khóa để có được những người đã xóa hồ sơ.
Kin Shah

1
Cảm ơn Kin, tuy nhiên tôi không nghĩ SUSER_SNAME()sẽ hoạt động trong tình huống như ứng dụng web nơi một người dùng có thể được sử dụng để liên lạc cơ sở dữ liệu cho toàn bộ ứng dụng.
giun web

1
Bạn đã không đề cập rằng bạn đang gọi một ứng dụng web.
Kin Shah

Xin lỗi Kin, đáng lẽ tôi phải cụ thể hơn với loại ứng dụng.
giun web

Câu trả lời:


10

Có cách nào để truyền thông tin vào trình kích hoạt Xóa để nó có thể biết ai đã xóa bản ghi không?

Có: bằng cách sử dụng một tính năng rất hay (và được sử dụng) được gọi CONTEXT_INFO. Nó thực chất là bộ nhớ phiên tồn tại trong tất cả các phạm vi và không bị ràng buộc bởi các giao dịch. Nó có thể được sử dụng để truyền thông tin (bất kỳ thông tin nào - tốt, bất kỳ thông tin nào phù hợp với không gian hạn chế) cho các trình kích hoạt cũng như qua lại giữa các lệnh gọi phụ / EXEC. Và tôi đã sử dụng nó trước đây cho tình huống chính xác này.

Kiểm tra với những điều sau đây để xem làm thế nào nó hoạt động. Lưu ý rằng tôi đang chuyển đổi CHAR(128)trước CONVERT(VARBINARY(128), ... Điều này là để buộc đệm trống để dễ dàng chuyển đổi trở lại VARCHARkhi lấy nó ra khỏi CONTEXT_INFO()VARBINARY(128)được đệm đúng với 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];

Các kết quả:

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

ĐỂ TẤT CẢ CHÚNG CÙNG NHAU:

  1. Ứng dụng sẽ gọi một thủ tục được lưu trữ "Xóa" đi qua Tên người dùng (hoặc bất cứ điều gì) đang xóa bản ghi. Tôi cho rằng đây đã là mô hình đang được sử dụng vì có vẻ như bạn đang theo dõi các hoạt động Chèn và Cập nhật.

  2. Thủ tục lưu trữ "Xóa" không:

    DECLARE @EncodedUser VARBINARY(128);
    SET @EncodedUser = CONVERT(VARBINARY(128),
                                CONVERT(CHAR(128), @UserName)
                              );
    SET CONTEXT_INFO @EncodedUser;
    
    -- DELETE STUFF HERE
  3. Trình kích hoạt kiểm toán thực hiện:

    -- 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. Xin lưu ý rằng, như @SeanGallardy đã chỉ ra trong một nhận xét, do các thủ tục khác và / hoặc truy vấn ad hoc xóa hồ sơ khỏi bảng này, có thể là:

    • CONTEXT_INFOchưa được thiết lập và vẫn NULL:

      Vì lý do này, tôi đã cập nhật ở trên INSERT INTO AuditTableđể sử dụng một COALESCEđể mặc định giá trị. Hoặc, nếu bạn không muốn mặc định và yêu cầu một tên, thì bạn có thể làm điều gì đó tương tự:

      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đã được đặt thành một giá trị không phải là Tên người dùng hợp lệ và do đó có thể vượt quá kích thước của AuditTable.[UserWhoMadeChanges]trường:

      Vì lý do này, tôi đã thêm một LEFTchức năng để đảm bảo rằng bất cứ thứ gì được lấy ra CONTEXT_INFOsẽ không phá vỡ INSERT. Như đã lưu ý trong mã, bạn chỉ cần đặt 50kích thước thực của UserWhoMadeChangestrường.


CẬP NHẬT CHO SQL SERVER 2016 VÀ MỚI hơn

SQL Server 2016 đã thêm phiên bản cải tiến của bộ nhớ mỗi phiên này: Ngữ cảnh phiên. Bối cảnh Phiên mới về cơ bản là một bảng băm của các cặp Khóa-Giá trị với "Khóa" là loại sysname(nghĩa là NVARCHAR(128)) và "Giá trị" SQL_VARIANT. Ý nghĩa:

  1. Hiện tại có sự phân tách các giá trị nên ít có khả năng xung đột với các mục đích sử dụng khác
  2. Bạn có thể lưu trữ nhiều loại khác nhau, không còn phải lo lắng về hành vi kỳ quặc khi lấy lại giá trị thông qua CONTEXT_INFO()(để biết chi tiết, vui lòng xem bài đăng của tôi: Tại sao không phải TIẾP THEO BẮT ĐẦU () Trả về giá trị chính xác được đặt bởi SET CONTEXT_INFO? )
  3. Bạn nhận được nhiều dung lượng hơn: tối đa 8000 byte cho mỗi "Giá trị", tổng cộng tối đa 256kb trên tất cả các khóa (so với tối đa 128 byte CONTEXT_INFO)

Để biết chi tiết, xin vui lòng xem các trang tài liệu sau:


Vấn đề với cách tiếp cận này là nó rất dễ bay hơi. Bất kỳ phiên nào cũng có thể thiết lập điều này, vì vậy nó có thể ghi đè lên bất kỳ mục nào được đặt trước đó. Bạn muốn thực sự phá vỡ ứng dụng của bạn? có một dev ghi đè lên những gì bạn mong đợi. Tôi đặc biệt khuyên KHÔNG nên sử dụng điều này và có một cách tiếp cận tiêu chuẩn có thể yêu cầu thay đổi kiến ​​trúc. Nếu không, bạn đang chơi với lửa.
Sean Gallardy

@SeanGallardy Bạn có thể vui lòng cung cấp một ví dụ thực tế về điều này xảy ra không? Phiên == @@SPID. Đây là bộ nhớ PER-session / Connection. Một phiên không thể ghi đè thông tin ngữ cảnh của phiên khác. Và khi phiên đăng xuất, giá trị biến mất. Không có thứ gọi là "mục được đặt trước đó".
Solomon Rutzky

1
Tôi không nói "phiên khác" Tôi đã nói bất kỳ đối tượng nào trong phạm vi phiên có thể làm điều này. Vì vậy, một nhà phát triển viết một sproc để giữ thông tin "theo ngữ cảnh" của riêng mình và bây giờ của bạn đã bị ghi đè. Có một ứng dụng tôi phải xử lý với mô hình tương tự, tôi đã xem nó xảy ra ... đó là phần mềm nhân sự. Hãy để tôi nói cho bạn biết mọi người đã hạnh phúc như thế nào khi KHÔNG được trả tiền đúng hạn do một "lỗi" do một trong những nhà phát triển viết một SP mới cập nhật sai thông tin ngữ cảnh cho phiên từ "được cho là". Chỉ cần đưa ra một ví dụ tôi đã thực sự chứng kiến ​​tại sao không sử dụng phương pháp này.
Sean Gallardy

@SeanGallardy Ok, cảm ơn vì đã làm rõ điểm đó. Nhưng nó vẫn chỉ là một điểm hợp lệ một phần. Để tình huống đó xảy ra, "Proc" khác sẽ phải được gọi bên trong tình huống này. Hoặc, nếu bạn đang nói về một số Proc khác có thể đang xóa khỏi bảng này và khởi động trình kích hoạt, đó là điều có thể được kiểm tra. Đó là một điều kiện chủng tộc, là điều cần tính đến (giống như chúng có trong tất cả các ứng dụng đa luồng), và không phải là lý do để không sử dụng kỹ thuật này. Và vì vậy tôi sẽ thực hiện một bản cập nhật nhỏ để làm điều đó. Cảm ơn bạn đã đưa khả năng này lên.
Solomon Rutzky

2
Tôi đang nói rằng bảo mật như sau khi suy nghĩ là vấn đề chính và đây không phải là công cụ để giải quyết nó. Cấu trúc ghi nhớ hoặc sử dụng khác không phá vỡ ứng dụng, chắc chắn tôi không có vấn đề gì. Nó hoàn toàn là một lý do để KHÔNG sử dụng nó. YMMV nhưng tôi sẽ không bao giờ sử dụng thứ gì đó quá dễ bay hơi và không có cấu trúc cho thứ gì đó quan trọng như bảo mật. Sử dụng bất kỳ loại lưu trữ có thể ghi người dùng chung để bảo mật là một ý tưởng tồi tệ nói chung. Thiết kế phù hợp sẽ loại bỏ sự cần thiết cho những thứ như thế này, đối với hầu hết các phần.
Sean Gallardy

5

Bạn không thể theo cách đó, trừ khi bạn đang tìm cách ghi lại ID người dùng máy chủ SQL thay vì cấp độ ứng dụng.

Bạn có thể thực hiện xóa mềm bằng cách có một cột có tên DeletingBy và cài đặt nếu cần, sau đó trình kích hoạt cập nhật của bạn có thể thực hiện xóa thực sự (hoặc lưu trữ bản ghi, tôi thường tránh xóa cứng khi có thể và hợp pháp) cũng như cập nhật dấu vết kiểm toán của bạn . Để buộc xóa được thực hiện theo cách đó xác định một on deletekích hoạt gây ra lỗi. Nếu bạn không muốn thêm một cột vào bảng vật lý của mình, bạn có thể xác định chế độ xem thêm cột và xác định các instead ofkích hoạt để xử lý cập nhật bảng cơ sở, nhưng điều đó có thể là quá mức cần thiết.


Tôi thấy điểm của bạn. Tôi thực sự sẽ tìm cách để đăng nhập người dùng cấp ứng dụng.
giun web

David, thực sự bạn có thể chuyển thông tin để kích hoạt. Xin vui lòng xem câu trả lời của tôi để biết chi tiết :).
Solomon Rutzky

Đề nghị tốt ở đây, tôi thực sự thích tuyến đường này. Giết hai con chim bằng cách bắt Ai trong cùng một bước khi kích hoạt xóa thực sự. Vì cột này sẽ là NULL cho mỗi bản ghi trong bảng này, có vẻ như nó sẽ là một cách sử dụng tốt SPARSEcột SQL Server ?
Airn5485

2

Có cách nào để truyền thông tin vào trình kích hoạt Xóa để nó có thể biết ai đã xóa bản ghi không?

Vâng, rõ ràng có hai cách ;-). Nếu có bất kỳ bảo lưu nào về việc sử dụng CONTEXT_INFOnhư tôi đã đề xuất trong câu trả lời khác của tôi ở đây , tôi chỉ nghĩ đến một cách khác có chức năng tách biệt sạch hơn với các mã / quy trình khác: sử dụng bảng tạm thời cục bộ.

Tên bảng tạm thời phải bao gồm tên bảng bị xóa vì điều đó sẽ giúp tách biệt với bất kỳ mã nào khác có thể xảy ra để chạy trong cùng một phiên. Một cái gì đó dọc theo dòng:
#<TableName>DeleteAudit

Một lợi ích cho bảng tạm thời cục bộ CONTEXT_INFOlà nếu ai đó trong một Proc khác - bằng cách nào đó gọi từ Proc "Xóa" cụ thể này - chỉ xảy ra không chính xác khi sử dụng cùng tên bảng tạm thời, quy trình con sẽ a) tạo một cục bộ mới bảng tạm thời của tên được yêu cầu sẽ tách biệt với bảng tạm thời ban đầu này (mặc dù nó có cùng tên) và b) mọi câu lệnh DML đối với bảng tạm thời cục bộ mới trong quy trình phụ sẽ không ảnh hưởng đến bất kỳ dữ liệu nào trong bảng tạm thời cục bộ được tạo ở đây trong quy trình cha, do đó không ghi đè dữ liệu. Tất nhiên, nếu một quy trình con đưa ra một câu lệnh DML đối với tên bảng tạm thời này mà không ban hành BẢNG TẠO cùng tên đó, thì các câu lệnh DML đó sẽ ảnh hưởng đến dữ liệu trong bảng này. NHƯNG, tại thời điểm này, chúng tôi đang thực sự nhận đượccạnh-casey ở đây, thậm chí còn nhiều hơn so với xác suất sử dụng chồng chéo CONTEXT_INFO(vâng, tôi biết điều đó đã xảy ra, đó là lý do tại sao tôi nói "trường hợp cạnh" thay vì "nó sẽ không bao giờ xảy ra").

  1. Ứng dụng sẽ gọi một thủ tục được lưu trữ "Xóa" đi qua Tên người dùng (hoặc bất cứ điều gì) đang xóa bản ghi. Tôi cho rằng đây đã là mô hình đang được sử dụng vì có vẻ như bạn đang theo dõi các hoạt động Chèn và Cập nhật.

  2. Thủ tục lưu trữ "Xóa" không:

    CREATE TABLE #MyTableDeleteAudit (UserName VARCHAR(50));
    INSERT INTO #MyTableDeleteAudit (UserName) VALUES (@UserName);
    
    -- DELETE STUFF HERE
  3. Trình kích hoạt kiểm toán thực hiện:

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

    Tôi đã kiểm tra mã này trong một kích hoạt và nó hoạt động như mong đợi.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.