web-dev-qa-db-ja.com

誰がレコードを削除したかに関する情報を削除トリガーに渡す

監査証跡を設定する際に、テーブルのレコードを更新または挿入している人物を追跡することには問題はありませんが、レコードを削除した人物を追跡することはさらに問題が多いようです。

挿入/更新フィールドに「UpdatedBy」を含めることで、挿入/更新を追跡できます。これにより、INSERT/UPDATEトリガーはinserted.UpdatedByを介してフィールド "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の使用

11
webworm

誰がレコードを削除したかを知ることができるように、情報を削除トリガーに渡す方法はありますか?

はい:CONTEXT_INFOと呼ばれるveryクールな(そして十分に活用されていない機能)を使用します。基本的に、すべてのスコープに存在し、トランザクションに拘束されないセッションメモリです。これは、情報(限られたスペースに収まるすべての情報)をトリガーに渡したり、サブプロシージャ/ EXEC呼び出し間でやり取りしたりするために使用できます。そして、私はこれとまったく同じ状況で以前にそれを使用しました。

次のようにテストして、動作を確認します。 CHAR(128)の前にCONVERT(VARBINARY(128), ..に変換していることに注意してください。これは、CONTEXT_INFO()0x00sで右パディングされているため、VARBINARY(128)から取得するときにVARCHARに変換しやすくするために、空白埋めを強制するためです。

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を壊さないようにしました。コードで述べたように、50UserWhoMadeChangesフィールドの実際のサイズに設定するだけです。


SQL Server 2016以降の更新

SQL Server 2016では、このセッションごとのメモリの改良版であるセッションコンテキストが追加されました。新しいセッションコンテキストは、本質的にはキーと値のペアのハッシュテーブルで、「キー」のタイプはsysname(つまりNVARCHAR(128))で、「値」はSQL_VARIANTです。意味:

  1. 値が分離されているため、他の用途と競合する可能性が低い
  2. さまざまなタイプを保存でき、CONTEXT_INFO()を介して値を取得するときの奇妙な動作を心配する必要がなくなりました(詳細については、私の投稿を参照してください: CONTEXT_INFO()が返されない理由) SET CONTEXT_INFOによって設定された正確な値?
  3. あなたはたくさんより多くのスペースを得ます:「値」ごとに最大8000バイト、すべてのキー全体で合計256kbまで(CONTEXT_INFOの最大128バイトと比較して)

詳細については、次のドキュメントページをご覧ください。

10
Solomon Rutzky

アプリケーションレベルではなくSQLサーバーのユーザーIDを記録する場合を除き、この方法は使用できません。

DeletedByという列を用意し、必要に応じて設定することで、ソフト削除を実行できます。その後、更新トリガーは、実際の削除(またはレコードをアーカイブします。通常、ハード削除は可能な限り合法です)を実行し、監査証跡を更新します。 。この方法で削除を強制するには、エラーを発生させるon deleteトリガーを定義します。物理テーブルに列を追加したくない場合は、列を追加するビューを定義し、基本テーブルの更新を処理するinstead ofトリガーを定義できますが、それはやり過ぎかもしれません。

5
David Spillett

誰がレコードを削除したかを知ることができるように、情報を削除トリガーに渡す方法はありますか?

はい、明らかに2つの方法があります;-)。私が提案したようにCONTEXT_INFOの使用について予約がある場合 ここに私の他の回答 を他のコード/プロセスから明確に機能的に分離する別の方法を考えました:ローカルを使用する一時テーブル。

一時テーブル名には、同じセッションで実行される可能性のある他のコードと区別するために、削除されるテーブル名を含める必要があります。以下に沿ったもの:
#<TableName>DeleteAudit

CONTEXT_INFOよりもローカル一時テーブルの利点の1つは、別のプロシージャの誰か(つまり、この特定の "削除"プロシージャから何らかの方法で呼び出された場合)が偶然に同じ一時テーブル名を誤って使用すると、サブプロセスが)リクエストされた名前の新しいローカル一時テーブルを作成します(これは同じ名前であっても)この初期一時テーブルとは異なります。b)サブプロセスの新しいローカル一時テーブルに対するDMLステートメントは作成されません。ここで親プロセスに作成されたローカル一時テーブルのデータに影響を与えるため、データの上書きはありません。もちろん、最初に同じ名前のCREATE TABLEを発行せずにサブプロセスがこの一時テーブル名に対してDMLステートメントを発行する場合、それらのDMLステートメントは影響を及ぼしますこのテーブルのデータ。しかし、この時点でreallyエッジケーシーが得られます。これは、CONTEXT_INFOの重複使用の可能性よりもさらに高くなります(はい、私はそれが起こったことを知っています、それが私が「決して起こらない」ではなく「エッジケース」と言う理由です)。

  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;
    

    このコードをトリガーでテストしましたが、期待どおりに動作します。

2
Solomon Rutzky