web-dev-qa-db-ja.com

複数行の更新を処理する更新後トリガー

現在、特定のテーブルの更新時に発生するトリガーに基づいてデータベース監査プロジェクトに取り組んでいます。トリガーは変更をテーブルに書き込みます。書き込まれる情報は、テーブル名、更新された列、タイムスタンプ、ユーザー、古い値と新しい値です。

トリガーは単一の更新では正常に機能しますが、複数行の更新に関しては機能しません。

私のコードは次のとおりです:

IF (UPDATE(Priority))  
BEGIN
    SET @UpdatedColumn = 'Priority'
    INSERT INTO dbo.AuditTable
        ( [TableName] ,
          [Source] ,
          [RecordId] ,
          [User] ,
          [TimeStamp] ,
          [UpdatedColumn] ,
          [OldValue] ,
          [NewValue]
        )
    SELECT 
        N'BookingItem' , -- TableName - nvarchar(max)
        (SELECT CODE FROM TBL_LEG_SOURCE 
                     INNER JOIN INSERTED INS ON LEG_SOURCE_ID = INS.SourceId) ,
        INS.Id , -- RecordId - bigint
        (SELECT USERNAME FROM INSERTED 
                     INNER JOIN TBL_USER 
                     ON ModifiedById = USER_ID) , -- User - nvarchar(max)
        GETDATE() , -- TimeStamp - datetime
        @UpdatedColumn , -- UpdatedColumn - nvarchar(max)
        DEL.Priority , -- OldValue - nvarchar(max)
        INS.Priority  -- NewValue - nvarchar(max)
    FROM 
        INSERTED INS INNER JOIN DELETED DEL ON INS.Id = DEL.Id
    WHERE
        (
            (INS.Priority <> DEL.Priority)
            OR (INS.Priority IS NULL AND DEL.Priority IS NOT NULL)
            OR (INS.Priority IS NOT NULL AND DEL.Priority IS NULL)
        )
END

エラーメッセージ:

メッセージ512、レベル16、状態1、手順MyTrigger、行818
サブクエリが複数の値を返しました。これは、サブクエリが=、!=、<、<=、>、> =の後に続く場合、またはサブクエリが式として使用される場合は許可されません。

複数行の操作を処理するためにトリガーを修正する方法に関する提案はありますか?

2

表示するクエリの両方のサブクエリは、集約またはtop(1)を実行せずにINSERTEDに結合します。したがって、どちらも複数の行を返す可能性があります。 INSERTEDテーブルに再度結合する代わりに、列を直接参照するだけです。これで、2番目のクエリは次のようになります。

(SELECT U.USERNAME FROM TBL_USER U WHERE INS.ModifiedById = U.USER_ID)

もう一方への変更も同様です。

4
Sebastian Meine

適切な結合を使用してエラーを修正する方法は次のとおりです(これが「十分に高速」でない場合は、インデックスを確認してください)。

INSERT dbo.AuditTable
(
  [TableName],
  [Source],
  [RecordId],
  [User],
  [TimeStamp],
  [UpdatedColumn],
  [OldValue],
  [NewValue]
)
SELECT 
  N'BookingItem', -- TableName - nvarchar(max)
  ls.CODE,
  INS.Id, -- RecordId - bigint
  u.USERNAME,
  GETDATE(), -- TimeStamp - datetime
  @UpdatedColumn, -- UpdatedColumn - nvarchar(max)
  DEL.Priority, -- OldValue - nvarchar(max)
  INS.Priority  -- NewValue - nvarchar(max)
FROM 
  INSERTED AS INS 
INNER JOIN 
  DELETED AS DEL ON INS.Id = DEL.Id
INNER JOIN 
  dbo.TBL_LEG_SOURCE AS ls ON ls.LEG_SOURCE_ID = INS.SourceId
INNER JOIN
  dbo.TBL_USER AS u ON INS.ModifiedById = u.USER_ID
WHERE
(
  (INS.Priority <> DEL.Priority)
  OR (INS.Priority IS NULL AND DEL.Priority IS NOT NULL)
  OR (INS.Priority IS NOT NULL AND DEL.Priority IS NULL)
);

しかし、すべての列の変更をキャプチャするために、この種類の50以上の異なる挿入を実行するのはかなり愚かだと思います。時間とテーブル名の列を持つテーブルを作成するだけでなく(後でユーザー名を調べることができるため、ユーザー名を保存する必要はありません)、更新があるたびに、行の古いバージョンと新しいバージョンを保存しますか? SEQUENCEを使用して、一緒に変更された行のセットを識別できるようにすることもできます(タイムスタンプがそれを行うのに十分に一意ではない場合があるため)。

CREATE SEQUENCE dbo.AuditSequence
  AS INT START WITH 1 INCREMENT BY 1;

CREATE TABLE dbo.AuditData
(
  AuditSequenceID INT,
  TableName SYSNAME,
  [TimeStamp] DATETIME,
  RowState CHAR(1), -- e.g. 'B' = before, 'A' = after
  ... all your 50 columns, including ModifidById ...
);

今あなたのトリガーに:

CREATE TRIGGER dbo.MyTrigger
  ON dbo.BookingItem
  FOR UPDATE
AS
BEGIN
  SET NOCOUNT ON;

  DECLARE @as INT = NEXT VALUE FOR dbo.AuditSequence,
          @now DATETIME = CURRENT_TIMESTAMP;

  INSERT dbo.AuditData(AuditSequenceID, TableName, [TimeStamp], RowState,
    ... the rest of your 50 columns)
  SELECT @as, N'BookingItem', @now, 'B', * FROM deleted;

  INSERT dbo.AuditData(AuditSequenceID, TableName, [TimeStamp], RowState,
    ... the rest of your 50 columns)
  SELECT @as, N'BookingItem', @now, 'A', * FROM inserted;
END
GO

次に、非効率なこの非常に単純な監査構造に対して複雑なクエリを記述し、変更された列とそのすべてを正確に追跡しようとします。監査データをレビューするときは、すべての更新操作でその価格を支払うよりも、その価格を支払う方がはるかに優れています。

9
Aaron Bertrand