삭제 트리거에 레코드를 삭제 한 사람에 대한 정보 전달


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. 본질적으로 모든 범위에 존재하며 트랜잭션에 의해 제한되지 않는 세션 메모리입니다. 서브 프로세스 / EXEC 호출 사이에서 앞뒤로 트리거하기 위해 정보 (모든 정보-제한된 공간에 맞는 정보)를 전달하는 데 사용할 수 있습니다. 그리고 나는이 똑같은 상황을 위해 그것을 전에 사용했습니다.

작동 방식을 확인하려면 다음을 테스트하십시오. CHAR(128)이전으로 변환 중 CONVERT(VARBINARY(128), ..입니다. 이것은 빈 패딩을 강제로 빼기 위해 s로 오른쪽이 채워져 있기 때문에 쉽게 VARCHAR꺼낼 수 있도록합니다 .CONTEXT_INFO()VARBINARY(128)0x00

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. 앱은 레코드를 삭제하는 UserName (또는 기타)을 전달하는 "삭제"저장 프로 시저를 호출해야합니다. 이미 삽입 및 업데이트 작업을 추적하는 것처럼 들리기 때문에 이미 사용중인 모델이라고 가정합니다.

  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유효한 UserName 이 아닌 값으로 설정 되었으므로 AuditTable.[UserWhoMadeChanges]필드 크기를 초과 할 수 있습니다 .

      이런 이유로 나는 LEFT움켜 잡은 것이 무엇이든 CONTEXT_INFO깨지지 않도록 하는 기능을 추가했습니다 INSERT. 코드에서 언급했듯이 필드 50의 실제 크기 로 설정하면 됩니다 UserWhoMadeChanges.


SQL Server 2016 이상 업데이트

SQL Server 2016은 세션 별 메모리의 향상된 버전 인 세션 컨텍스트를 추가했습니다. 새로운 세션 컨텍스트는 본질적으로 "Key"유형 sysname(즉 NVARCHAR(128))이고 "Value" 인 키-값 쌍의 해시 테이블입니다 SQL_VARIANT. 의미:

  1. 이제 값이 분리되어 다른 용도와 충돌 할 가능성이 줄어 듭니다.
  2. 다양한 유형을 저장할 수 있으며 더 이상 값을 가져올 때 이상한 동작에 대해 걱정할 필요가 없습니다 CONTEXT_INFO()(자세한 내용은 내 게시물을 참조하십시오 : CONTEXT_INFO ()가 SET CONTEXT_INFO에 의해 설정된 정확한 값을 반환하지 않는 이유는 무엇입니까? )
  3. 당신이 얻을 훨씬 더 많은 공간을 : 8000 "값"당 최대 바이트, 최대 모든 키에서 256킬로바이트의 총 (128 비교하는 것은의 최대 바이트 CONTEXT_INFO)

자세한 내용은 다음 설명서 페이지를 참조하십시오.


이 접근법의 문제점은 그것이 매우 휘발성이라는 것입니다. 모든 세션은이를 설정하여 이전에 설정 한 항목을 덮어 쓸 수 있습니다. 실제로 응용 프로그램을 중단하고 싶습니까? 하나의 개발자가 당신이 기대하는 것을 덮어 씁니다. 나는 이것을 사용하지 말고 아키텍처 변경이 필요할 수있는 표준 접근법을 사용하는 것이 좋습니다. 그렇지 않으면, 당신은 불을 가지고 놀고 있습니다.
Sean Gallardy

@SeanGallardy이 상황에 대한 실제 사례를 제공해 주시겠습니까? 세션 == @@SPID. PER- 세션 / 연결 메모리입니다. 한 세션은 다른 세션의 컨텍스트 정보를 덮어 쓸 수 없습니다. 그리고 세션이 로그 오프되면 값이 사라집니다. "이전 설정 항목"과 같은 것은 없습니다.
솔로몬 루츠 키

1
"다른 세션"이라고 말하지 않았으며 세션 범위의 모든 개체가이 작업을 수행 할 수 있다고 말했습니다. 따라서 한 개발자는 자신의 "컨텍스트"정보를 보유하기 위해 sproc을 작성하고 이제는 귀하의 정보를 덮어 씁니다. 이 동일한 패턴을 사용하여 처리 해야하는 응용 프로그램이 있었지만 HR 소프트웨어였습니다. 개발자 중 한 명이 "버그"로 인해 세션에 대한 컨텍스트 정보를 "추정 된"내용에서 잘못 업데이트 한 "버그"로 인해 시간에 돈을 지불하지 않은 사람이 얼마나 행복한 지 알려 드리겠습니다. 이 방법을 사용하지 않는 이유를 실제로 목격 한 예를 들었습니다.
Sean Gallardy

@SeanGallardy Ok, 그 점을 명확히 해 주셔서 감사합니다. 그러나 여전히 부분적으로 유효한 지점입니다. 이러한 상황이 발생하기 위해서는이 "다른"프 로세스를이 내부에서 호출해야합니다. 또는이 테이블에서 삭제하고 트리거를 시작하는 다른 proc에 대해 이야기하는 경우 테스트 할 수 있습니다. 경쟁 조건으로, 모든 멀티 스레드 응용 프로그램에서와 마찬가지로 설명해야하지만이 기술을 사용하지 않는 이유는 아닙니다. 그리고 그렇게하기 위해 약간의 업데이트를 할 것입니다. 이 가능성을 제기 해 주셔서 감사합니다.
Solomon Rutzky

2
보안은 나중에 생각하는 것이 주요 문제이며 이것이 해결하기위한 도구가 아니라고 말합니다. 응용 프로그램을 중단시키지 않는 메모 구조 또는 기타 용도로 문제가 없습니다. 절대 사용하지 않는 이유입니다. YMMV 그러나 나는 보안과 같은 중요한 것에 휘발성이 있고 구조화되지 않은 것을 사용하지 않을 것입니다. 보안을 위해 모든 유형의 공유 사용자 쓰기 가능 스토리지를 사용하는 것은 전반적으로 끔찍한 아이디어입니다. 적절한 디자인은 대부분 이와 같은 것들에 대한 필요성을 제거합니다.
Sean Gallardy

5

응용 프로그램 수준 1이 아닌 SQL Server 사용자 ID를 기록하지 않는 한 그렇게 할 수 없습니다.

DeletedBy라는 열이 있고 필요에 따라 열을 설정하면 업데이트 트리거가 실제 삭제를 수행하거나 (또는 ​​레코드를 보관, 가능한 경우 합법적 인 경우 하드 삭제를 피할 수 있음) 감사 내역을 업데이트 할 수 있습니다. . 이런 식으로 삭제를 수행하려면 on delete오류를 발생 시키는 트리거를 정의하십시오 . 실제 테이블에 열을 추가하지 않으려는 경우 열을 추가하는 뷰를 정의 instead of하고 기본 테이블 업데이트를 처리하기위한 트리거를 정의 할 수 있지만 너무 과도 할 수 있습니다.


너의 의도를 알 겠어. 실제로 응용 프로그램 수준 사용자를 기록하려고합니다.
webworm

데이비드, 실제로 정보를 트리거에 전달할 수 있습니다. 자세한 내용은 내 답변을 참조하십시오 :).
솔로몬 루츠 키

여기에 좋은 제안이 있습니다. 실제 삭제를 트리거하는 것과 같은 단계에서 누가를 캡처하여 두 마리의 새를 죽입니다. 이 테이블의 모든 레코드에 대해이 열이 NULL이되므로 SQL Server SPARSE열을 잘 사용하는 것 같습니다 .
Airn5475

2

누가 레코드를 삭제했는지 알 수 있도록 삭제 트리거에 정보를 전달하는 방법이 있습니까?

네, 분명히 두 가지 방법이 있습니다 ;-). 여기에 다른 답변에서CONTEXT_INFO 제안한대로 사용에 대한 예약이있는 경우 다른 코드 / 프로세스와 기능적으로 분리 된 다른 방법을 생각했습니다. 로컬 임시 테이블을 사용하십시오.

임시 테이블 이름에는 동일한 세션에서 실행될 수있는 다른 코드와 분리 할 수 ​​있도록 테이블 이름이 삭제되어야합니다. 다음과 같은 내용이 있습니다.
#<TableName>DeleteAudit

로컬 임시 테이블에 대한 한 가지 이점 CONTEXT_INFO은 다른 특정 프로세스의 누군가가 어떻게 든이 특정 "삭제"프로 시저에서 호출 된 경우 동일한 임시 테이블 이름을 잘못 사용하면 하위 프로세스가 a) 새 로컬을 작성한다는 것입니다 요청 된 이름의 임시 테이블 (이 초기 임시 테이블과 이름이 같더라도) 및 b) 하위 프로세스의 새 로컬 임시 테이블에 대한 DML 문은 데이터의 데이터에 영향을 미치지 않습니다. 여기에서 상위 프로세스에서 로컬 임시 테이블이 작성되므로 데이터를 겹쳐 쓰지 않습니다. 물론, 서브 프로세스가 같은 이름의 CREATE TABLE을 먼저 발행하지 않고이 임시 테이블 이름에 대해 DML 문을 발행하면 해당 DML 문 이 테이블의 데이터에 영향을줍니다. 하지만이 시점에서 우리는 정말겹치는 사용 가능성보다 훨씬 더 엣지-케이시 CONTEXT_INFO(그렇습니다, 나는 그것이 일어났다는 것을 알고 있습니다. "나는 결코 일어나지 않을 것"보다는 "에지-케이스"라고 말합니다).

  1. 앱은 레코드를 삭제하는 UserName (또는 기타)을 전달하는 "삭제"저장 프로 시저를 호출해야합니다. 이미 삽입 및 업데이트 작업을 추적하는 것처럼 들리기 때문에 이미 사용중인 모델이라고 가정합니다.

  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.