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


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()、アプリケーション全体のデータベース通信に1人のユーザーが使用される可能性があるWebアプリケーションのような状況では機能しないと思います。
webworm 2014年

1
あなたはあなたがウェブアプリを呼んでいるとは言いませんでした。
Kin Shah

申し訳ありませんが、私はアプリケーションのタイプに特化していたはずです。
webworm 2014年

回答:


10

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

はい:と呼ばれる非常に優れた(そして十分に活用されていない機能)を使用するCONTEXT_INFO。基本的に、すべてのスコープに存在し、トランザクションに拘束されないセッションメモリです。これは、情報(限られたスペースに収まるすべての情報)をトリガーに渡したり、サブプロシージャ/ 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 AuditTablea 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では、このセッションごとのメモリの改良版であるセッションコンテキストが追加されました。新しいセッションコンテキストは、本質的にはキーと値のペアのハッシュテーブルで、「キー」はタイプsysname(つまりNVARCHAR(128))で、「値」はSQL_VARIANTです。意味:

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

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


このアプローチの問題は、非常に不安定であることです。どのセッションでもこれを設定できるため、以前に設定した項目を上書きできます。アプリケーションを本当に壊したいですか?単一の開発者が期待どおりに上書きします。私はこれを使用しないことを強くお勧めします。アーキテクチャの変更を必要とする可能性のある標準的なアプローチがあります。そうでなければ、あなたは火で遊んでいます。
Sean Gallardy 2014年

@SeanGallardyこの出来事の実際の例を教えていただけますか?セッション== @@SPID。これは、PERセッション/接続メモリです。1つのセッションが別のセッションのコンテキスト情報を上書きすることはできません。そして、セッションがログオフすると、値は消えます。「あらかじめセットされた商品」などありません。
ソロモンルツキー2014年

1
「別のセッション」とは言わなかったが、セッションスコープ内の任意のオブジェクトがこれを実行できると述べた。したがって、1人の開発者が自分の「コンテキスト」情報を保持するsprocを作成すると、あなたの情報が上書きされます。これと同じパターンを使用して処理しなければならないアプリケーションがありました。それが起こるのを見てきました...それはHRソフトウェアでした。セッションのコンテキスト情報を「想定」されていたものから誤って更新した新しいSPを作成している開発者の1人による「バグ」が原因で、満足のいく人々に時間どおりに支払われなかったのはどのようなものかを教えてください。実際にこの方法を使用しない理由を実際に目撃した例を挙げてみましょう。
Sean Gallardy 2014年

@SeanGallardy OK、その点を明確にしてくれてありがとう。しかし、それはまだ部分的に有効なポイントにすぎません。そのような状況が発生するためには、その「その他」のプロシージャがこの内部で呼び出される必要があります。または、このテーブルから削除してトリガーを開始する可能性がある他のprocについて話している場合、それはテストすることができるものです。これは競合状態であり、(すべてのマルチスレッドアプリと同じように)考慮すべき問題であり、この手法を使用しない理由ではありません。そして、それを行うためにマイナーな更新を行います。この可能性を引き上げてくれてありがとう。
ソロモンルツキー2014年

2
セキュリティは後回しの問題として主な問題であり、これはそれを解決するためのツールではありません。アプリケーションを壊さないメモ構造または他の使用法、私は問題がないことを確認してください。絶対に使用しない理由です。YMMVですが、セキュリティなどの重要なものには、揮発性で構造化されていないものは使用しません。セキュリティのために任意のタイプの共有ユーザー書き込み可能ストレージを使用することは、全体としてひどい考えです。適切な設計は、ほとんどの場合、このようなものの必要性を排除します。
Sean Gallardy 2014年

5

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

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


あなたの言ってる事がわかります。確かに、アプリケーションレベルのユーザーをログに記録しようとしています。
webworm 2014年

David、実際にはトリガーに情報を渡すことができます。詳細については私の答えを参照してください:)。
ソロモンルツキー2014年

ここで良い提案、私はこのルートが本当に好きです。実際の削除をトリガーするのと同じステップでWhoをキャプチャして、2羽の鳥を殺します。この列は、このテーブルのすべてのレコードでNULLになるため、SQL Server SPARSE列を適切に使用しているようです。
Airn5475

2

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

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

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

ローカル一時テーブルに対する1つの利点CONTEXT_INFOは、別のプロシージャの誰か(つまり、この特定の「削除」プロシージャから何らかの方法で呼び出された場合)が偶然同じ一時テーブル名を誤って使用した場合、サブプロセスはa)新しいローカルを作成することです。この最初の一時テーブルとは異なる(同じ名前であっても)要求された名前の一時テーブル、およびb)サブプロセスの新しいローカル一時テーブルに対するDMLステートメントは、ここで親プロセス内に作成されたローカル一時テーブル。データの上書きはありません。もちろん、最初に同じ名前のCREATE TABLEを発行せずにサブプロセスがこの一時テーブル名に対してDMLステートメントを発行する場合、それらのDMLステートメントこのテーブルのデータに影響を与えます。しかし、この時点で私たちは本当にここでの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;

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

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.