【问题标题】:Log record changes in SQL server in an audit table在审计表中记录 SQL 服务器中的记录更改
【发布时间】:2013-11-13 07:13:15
【问题描述】:

桌子:

    CREATE TABLE GUESTS (
      GUEST_ID int IDENTITY(1,1) PRIMARY KEY, 
      GUEST_NAME VARCHAR(50), 
      GUEST_SURNAME VARCHAR(50), 
      ADRESS VARCHAR(100), 
      CITY VARCHAR(50), 
      CITY_CODE VARCHAR(10), 
      COUNTRY VARCHAR(50), 
      STATUS VARCHAR(20), 
      COMMENT nvarchar(max);

对于日志记录:

CREATE TABLE AUDIT_GUESTS (
  ID int IDENTITY(1,1) PRIMARY KEY, 
  GUEST_ID int,
  OLD_GUEST_NAME VARCHAR(50), 
  NEW_GUEST_NAME VARCHAR(50), 
  OLD_GUEST_SURNAME VARCHAR(50), 
  NEW_GUEST_SURNAME VARCHAR(50),
  OLD_ADRESS VARCHAR(100), 
  NEW_ADRESS VARCHAR(100),
  OLD_CITY VARCHAR(50), 
  NEW_CITY VARCHAR(50),
  OLD_CITY_CODE VARCHAR(10), 
  NEW_CITY_CODE VARCHAR(10), 
  OLD_COUNTRY VARCHAR(50), 
  NEW_COUNTRY VARCHAR(50), 
  OLD_STATUS VARCHAR(20), 
  NEW_STATUS VARCHAR(20), 
  OLD_COMMENT nvarchar(max), 
  NEW_COMMENT nvarchar(max), 
  AUDIT_ACTION varchar(100),
  AUDIT_TIMESTAMP datetime);

我想在我的GUESTS 表上创建一个触发器来记录我的AUDIT_GUESTS 表中的所有更改。如何在 SQL Server 2014 Express 中做到这一点?

我试过了:

create TRIGGER trgAfterUpdate ON [dbo].[GUESTS] 
FOR UPDATE
AS
    declare @GUEST_ID int;
    declare @GUEST_NAME varchar(50);
    declare @GUEST_SURNAME VARCHAR(50);
    declare @ADRESS VARCHAR(100); 
    declare @CITY VARCHAR(50);
    declare @CITY_CODE VARCHAR(10); 
    declare @COUNTRY VARCHAR(50);
    declare @STATUS VARCHAR(20);
    declare @COMMENT nvarchar(max);
    declare @AUDIT_ACTION varchar(100);
    declare @AUDIT_TIMESTAMP datetime;

    select @GUEST_ID=i.GUEST_ID from inserted i;            
    select @GUEST_NAME=i.GUEST_NAME from inserted i;    
    select @GUEST_SURNAME=i.GUEST_SURNAME from inserted i;
    select @ADRESS=i.ADRESS from inserted i;
    select @CITY=i.CITY from inserted i;
    select @CITY_CODE=i.CITY_CODE from inserted i;
    select @COUNTRY=i.COUNTRY from inserted i;
    select @STATUS=i.STATUS from inserted i;
    select @COMMENT=i.COMMENT from inserted i;

        if update(GUEST_NAME)
        set @audit_action='Updated Record -- After Update Trigger.';

        if update(GUEST_SURNAME)
        set @audit_action='Updated Record -- After Update Trigger.';

        if update(ADRESS)
        set @audit_action='Updated Record -- After Update Trigger.';

        if update(CITY)
        set @audit_action='Updated Record -- After Update Trigger.';

        if update(CITY_CODE)
        set @audit_action='Updated Record -- After Update Trigger.';

        if update(COUNTRY)
        set @audit_action='Updated Record -- After Update Trigger.';

        if update(STATUS)
        set @audit_action='Updated Record -- After Update Trigger.';

        if update(COMMENT)
        set @audit_action='Updated Record -- After Update Trigger.';

        insert into AUDIT_GUESTS
           (GUEST_ID,GUEST_NAME,GUEST_SURNAME,ADRESS,CITY,CITY_CODE,COUNTRY,STATUS,COMMENT,audit_action,AUDIT_TIMESTAMP) 
    values(@GUEST_ID,@GUEST_NAME,@GUEST_SURNAME,@ADRESS,@CITY,@CITY_CODE,@COUNTRY,@STATUS,@COMMENT,@audit_action,getdate());
    GO

工作还可以,但我希望看到旧的新值。

在 SQLite 中我有:

CREATE TRIGGER [LOG_UPDATE]
AFTER UPDATE OF [GUEST_NAME], [GUEST_SURNAME], [ADRESS], [CITY], [CITY_CODE], [COUNTRY], [STATUS], [COMMENT]
ON [GUESTS]
BEGIN
INSERT INTO GUESTS_LOG
 ( GUEST_ID,
   NAME_OLD,NAME_NEW,
   SURNAME_OLD,SURNAME_NEW,
   ADRESS_OLD,ADRESS_NEW,
   CITY_OLD,CITY_NEW,
   CITY_CODE_OLD,CITY_CODE_NEW,
   COUNTRY_OLD,COUNTRY_NEW,
   STATUS_OLD,STATUS_NEW,   
   COMMENT_OLD,COMMENT_NEW,sqlAction,DATE_TIME)   

   VALUES   

 (OLD.GUEST_ID,
  OLD.GUEST_NAME,NEW.GUEST_NAME, 
  OLD.GUEST_SURNAME,NEW.GUEST_SURNAME,
  OLD.ADRESS,NEW.ADRESS,
  OLD.CITY,NEW.CITY,
  OLD.CITY_CODE,NEW.CITY_CODE,
  OLD.COUNTRY,NEW.COUNTRY,  
  OLD.STATUS,NEW.STATUS,
  OLD.COMMENT,NEW.COMMENT,'record changed',datetime('now','localtime'));  

END

它工作正常。只是不知道如何将其传递给 SQL Server。刚开始学。

【问题讨论】:

  • 您在触发器中的第一个也是基本的缺陷是假设InsertedDeleted 只包含一个行 - 这是不是的情况!如果您的 UPDATE 语句一次影响 10 行,您的触发器将被触发 一次 并且在 InsertedDeleted 中都有 10 行 - 您需要使用基于集合的方法,而不仅仅是从那些伪表中选择您认为唯一的值!
  • 从 SQL Server 2016 及更高版本开始支持此功能,请参阅 docs.microsoft.com/en-us/sql/relational-databases/tables/…

标签: sql sql-server


【解决方案1】:

看看Pop Rivett 在 Simple-talk.com 上的this article。它引导您创建一个通用触发器,该触发器将为所有更新的列记录 OLDVALUE 和 NEWVALUE。该代码非常通用,您可以将其应用于您要审核的任何表,也可以用于任何 CRUD 操作,即 INSERT、UPDATE 和 DELETE。唯一的要求是要审核的表应该有一个 PRIMARY KEY(大多数设计良好的表都应该有)。

这是与您的 GUESTS 表相关的代码。

  1. 创建审核表。
    IF NOT EXISTS
          (SELECT * FROM sysobjects WHERE id = OBJECT_ID(N'[dbo].[Audit]') 
                   AND OBJECTPROPERTY(id, N'IsUserTable') = 1)
           CREATE TABLE Audit 
                   (Type CHAR(1), 
                   TableName VARCHAR(128), 
                   PK VARCHAR(1000), 
                   FieldName VARCHAR(128), 
                   OldValue VARCHAR(1000), 
                   NewValue VARCHAR(1000), 
                   UpdateDate datetime, 
                   UserName VARCHAR(128))
    GO
  1. 在 GUESTS 表上创建一个 UPDATE 触发器,如下所示。
    CREATE TRIGGER TR_GUESTS_AUDIT ON GUESTS FOR UPDATE
    AS
    
    DECLARE @bit INT ,
           @field INT ,
           @maxfield INT ,
           @char INT ,
           @fieldname VARCHAR(128) ,
           @TableName VARCHAR(128) ,
           @PKCols VARCHAR(1000) ,
           @sql VARCHAR(2000), 
           @UpdateDate VARCHAR(21) ,
           @UserName VARCHAR(128) ,
           @Type CHAR(1) ,
           @PKSelect VARCHAR(1000)
           
    
    --You will need to change @TableName to match the table to be audited. 
    -- Here we made GUESTS for your example.
    SELECT @TableName = 'GUESTS'
    
    -- date and user
    SELECT         @UserName = SYSTEM_USER ,
           @UpdateDate = CONVERT (NVARCHAR(30),GETDATE(),126)
    
    -- Action
    IF EXISTS (SELECT * FROM inserted)
           IF EXISTS (SELECT * FROM deleted)
                   SELECT @Type = 'U'
           ELSE
                   SELECT @Type = 'I'
    ELSE
           SELECT @Type = 'D'
    
    -- get list of columns
    SELECT * INTO #ins FROM inserted
    SELECT * INTO #del FROM deleted
    
    -- Get primary key columns for full outer join
    SELECT @PKCols = COALESCE(@PKCols + ' and', ' on') 
                   + ' i.' + c.COLUMN_NAME + ' = d.' + c.COLUMN_NAME
           FROM    INFORMATION_SCHEMA.TABLE_CONSTRAINTS pk ,
    
                  INFORMATION_SCHEMA.KEY_COLUMN_USAGE c
           WHERE   pk.TABLE_NAME = @TableName
           AND     CONSTRAINT_TYPE = 'PRIMARY KEY'
           AND     c.TABLE_NAME = pk.TABLE_NAME
           AND     c.CONSTRAINT_NAME = pk.CONSTRAINT_NAME
    
    -- Get primary key select for insert
    SELECT @PKSelect = COALESCE(@PKSelect+'+','') 
           + '''<' + COLUMN_NAME 
           + '=''+convert(varchar(100),
    coalesce(i.' + COLUMN_NAME +',d.' + COLUMN_NAME + '))+''>''' 
           FROM    INFORMATION_SCHEMA.TABLE_CONSTRAINTS pk ,
                   INFORMATION_SCHEMA.KEY_COLUMN_USAGE c
           WHERE   pk.TABLE_NAME = @TableName
           AND     CONSTRAINT_TYPE = 'PRIMARY KEY'
           AND     c.TABLE_NAME = pk.TABLE_NAME
           AND     c.CONSTRAINT_NAME = pk.CONSTRAINT_NAME
    
    IF @PKCols IS NULL
    BEGIN
           RAISERROR('no PK on table %s', 16, -1, @TableName)
           RETURN
    END
    
    SELECT         @field = 0, 
           @maxfield = MAX(ORDINAL_POSITION) 
           FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = @TableName
    WHILE @field < @maxfield
    BEGIN
           SELECT @field = MIN(ORDINAL_POSITION) 
                   FROM INFORMATION_SCHEMA.COLUMNS 
                   WHERE TABLE_NAME = @TableName 
                   AND ORDINAL_POSITION > @field
           SELECT @bit = (@field - 1 )% 8 + 1
           SELECT @bit = POWER(2,@bit - 1)
           SELECT @char = ((@field - 1) / 8) + 1
           IF SUBSTRING(COLUMNS_UPDATED(),@char, 1) & @bit > 0
                                           OR @Type IN ('I','D')
           BEGIN
                   SELECT @fieldname = COLUMN_NAME 
                           FROM INFORMATION_SCHEMA.COLUMNS 
                           WHERE TABLE_NAME = @TableName 
                           AND ORDINAL_POSITION = @field
                   SELECT @sql = '
    insert Audit (    Type, 
                   TableName, 
                   PK, 
                   FieldName, 
                   OldValue, 
                   NewValue, 
                   UpdateDate, 
                   UserName)
    select ''' + @Type + ''',''' 
           + @TableName + ''',' + @PKSelect
           + ',''' + @fieldname + ''''
           + ',convert(varchar(1000),d.' + @fieldname + ')'
           + ',convert(varchar(1000),i.' + @fieldname + ')'
           + ',''' + @UpdateDate + ''''
           + ',''' + @UserName + ''''
           + ' from #ins i full outer join #del d'
           + @PKCols
           + ' where i.' + @fieldname + ' <> d.' + @fieldname 
           + ' or (i.' + @fieldname + ' is null and  d.'
                                    + @fieldname
                                    + ' is not null)' 
           + ' or (i.' + @fieldname + ' is not null and  d.' 
                                    + @fieldname
                                    + ' is null)' 
                   EXEC (@sql)
           END
    END
    
    GO

【讨论】:

  • 这需要各种组合,这些组合往往会变得太长。好的方法是拥有新旧价值观。这就是我问的原因。如何记录新旧值?
  • 好的,所以您正在寻找一种方法来记录更新到日志表的任何字段。如果更新了 3 个字段,您想在审计中记录 3 个单独的行,每个字段 1 个更改为该审计行中的 OLD 和 NEW 值?
  • 真正的答案是湿婆从流行铆钉那里得到的东西
  • 你应该改变你的代码。见this
  • @Shiva noob 问题:为什么触发器仅适用于 UPDATE 事件?它有效吗,如果我删除或插入记录,它会有效吗?我们也在使用 DELETE 触发器,以防人们删除他们不应该这样做的数据。谢谢
【解决方案2】:

这是包含两个错误修复的代码。 Royi Namir 在对该问题的已接受答案的评论中提到了第一个错误修复。该错误在 StackOverflow 上的 Bug in Trigger Code 上进行了描述。第二个是由@Fandango68 发现的,并修复了名称为多个单词的列。

ALTER TRIGGER [dbo].[TR_person_AUDIT]
ON [dbo].[person]
FOR UPDATE
AS
           DECLARE @bit            INT,
                   @field          INT,
                   @maxfield       INT,
                   @char           INT,
                   @fieldname      VARCHAR(128),
                   @TableName      VARCHAR(128),
                   @PKCols         VARCHAR(1000),
                   @sql            VARCHAR(2000),
                   @UpdateDate     VARCHAR(21),
                   @UserName       VARCHAR(128),
                   @Type           CHAR(1),
                   @PKSelect       VARCHAR(1000)


           --You will need to change @TableName to match the table to be audited.
           -- Here we made GUESTS for your example.
           SELECT @TableName = 'PERSON'

           SELECT @UserName = SYSTEM_USER,
                  @UpdateDate = CONVERT(NVARCHAR(30), GETDATE(), 126)

           -- Action
           IF EXISTS (
                  SELECT *
                  FROM   INSERTED
              )
               IF EXISTS (
                      SELECT *
                      FROM   DELETED
                  )
                   SELECT @Type = 'U'
               ELSE
                   SELECT @Type = 'I'
           ELSE
               SELECT @Type = 'D'

           -- get list of columns
           SELECT * INTO #ins
           FROM   INSERTED

           SELECT * INTO #del
           FROM   DELETED

           -- Get primary key columns for full outer join
           SELECT @PKCols = COALESCE(@PKCols + ' and', ' on') 
                  + ' i.[' + c.COLUMN_NAME + '] = d.[' + c.COLUMN_NAME + ']'
           FROM   INFORMATION_SCHEMA.TABLE_CONSTRAINTS pk,
                  INFORMATION_SCHEMA.KEY_COLUMN_USAGE c
           WHERE  pk.TABLE_NAME = @TableName
                  AND CONSTRAINT_TYPE = 'PRIMARY KEY'
                  AND c.TABLE_NAME = pk.TABLE_NAME
                  AND c.CONSTRAINT_NAME = pk.CONSTRAINT_NAME

           -- Get primary key select for insert
           SELECT @PKSelect = COALESCE(@PKSelect + '+', '') 
                  + '''<[' + COLUMN_NAME 
                  + ']=''+convert(varchar(100),
           coalesce(i.[' + COLUMN_NAME + '],d.[' + COLUMN_NAME + ']))+''>'''
           FROM   INFORMATION_SCHEMA.TABLE_CONSTRAINTS pk,
                  INFORMATION_SCHEMA.KEY_COLUMN_USAGE c
           WHERE  pk.TABLE_NAME = @TableName
                  AND CONSTRAINT_TYPE = 'PRIMARY KEY'
                  AND c.TABLE_NAME = pk.TABLE_NAME
                  AND c.CONSTRAINT_NAME = pk.CONSTRAINT_NAME

           IF @PKCols IS NULL
           BEGIN
               RAISERROR('no PK on table %s', 16, -1, @TableName)

               RETURN
           END

           SELECT @field = 0,
                  -- @maxfield = MAX(COLUMN_NAME) 
                  @maxfield = -- FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = @TableName


                  MAX(
                      COLUMNPROPERTY(
                          OBJECT_ID(TABLE_SCHEMA + '.' + @TableName),
                          COLUMN_NAME,
                          'ColumnID'
                      )
                  )
           FROM   INFORMATION_SCHEMA.COLUMNS
           WHERE  TABLE_NAME = @TableName






           WHILE @field < @maxfield
           BEGIN
               SELECT @field = MIN(
                          COLUMNPROPERTY(
                              OBJECT_ID(TABLE_SCHEMA + '.' + @TableName),
                              COLUMN_NAME,
                              'ColumnID'
                          )
                      )
               FROM   INFORMATION_SCHEMA.COLUMNS
               WHERE  TABLE_NAME = @TableName
                      AND COLUMNPROPERTY(
                              OBJECT_ID(TABLE_SCHEMA + '.' + @TableName),
                              COLUMN_NAME,
                              'ColumnID'
                          ) > @field

               SELECT @bit = (@field - 1)% 8 + 1

               SELECT @bit = POWER(2, @bit - 1)

               SELECT @char = ((@field - 1) / 8) + 1





               IF SUBSTRING(COLUMNS_UPDATED(), @char, 1) & @bit > 0
                  OR @Type IN ('I', 'D')
               BEGIN
                   SELECT @fieldname = COLUMN_NAME
                   FROM   INFORMATION_SCHEMA.COLUMNS
                   WHERE  TABLE_NAME = @TableName
                          AND COLUMNPROPERTY(
                                  OBJECT_ID(TABLE_SCHEMA + '.' + @TableName),
                                  COLUMN_NAME,
                                  'ColumnID'
                              ) = @field



                   SELECT @sql = 
                          '
           insert into Audit (    Type, 
           TableName, 
           PK, 
           FieldName, 
           OldValue, 
           NewValue, 
           UpdateDate, 
           UserName)
           select ''' + @Type + ''',''' 
                          + @TableName + ''',' + @PKSelect
                          + ',''' + @fieldname + ''''
                          + ',convert(varchar(1000),d.' + @fieldname + ')'
                          + ',convert(varchar(1000),i.' + @fieldname + ')'
                          + ',''' + @UpdateDate + ''''
                          + ',''' + @UserName + ''''
                          + ' from #ins i full outer join #del d'
                          + @PKCols
                          + ' where i.' + @fieldname + ' <> d.' + @fieldname 
                          + ' or (i.' + @fieldname + ' is null and  d.'
                          + @fieldname
                          + ' is not null)' 
                          + ' or (i.' + @fieldname + ' is not null and  d.' 
                          + @fieldname
                          + ' is null)' 



                   EXEC (@sql)
               END
           END

【讨论】:

  • 您能否进一步解释您的答案,关于“错误”是什么以及您如何修复它?没有解释的拍代码很糟糕。
  • @Fandango68 感谢您的请求,不需要评判性语言。该错误已在上一个答案的其中一个 cmets 中注明,我将在我的答案中清楚地提供链接。顺便说一句,我没有修复它,我只是想通过提供接近有 bug 的代码的固定代码来提供帮助。
  • 我很抱歉。它发生得太频繁了。还有另一个错误。列名在单词之间有空格的表将不起作用。所有列名都必须是单个单词。修复很容易。在逻辑周围放置“[”和“]”字符。
  • @Fandango68 你是对的,我当然有自己的烦恼。无论如何,你的观点是好的。不清楚我在发布什么。我希望现在好多了。您是否打算让我在我发布的代码中包含您的修复?
  • 是的,为什么不呢?将这些字符放在引用 COLUMN_NAME 的位置。那我想你的答案应该是官方的和最终的答案! :P
【解决方案3】:

我知道这是旧的,但也许这对其他人有帮助。

不要记录“新”值。您现有的表 GUESTS 具有新值。你会重复输入数据,而且你的数据库大小会增长得太快。

我对此示例进行了清理并将其最小化,但这里是您注销更改所需的表:

CREATE TABLE GUESTS (
      GuestID INT IDENTITY(1,1) PRIMARY KEY, 
      GuestName VARCHAR(50), 
      ModifiedBy INT, 
      ModifiedOn DATETIME
)

CREATE TABLE GUESTS_LOG (
      GuestLogID INT IDENTITY(1,1) PRIMARY KEY, 
      GuestID INT, 
      GuestName VARCHAR(50), 
      ModifiedBy INT, 
      ModifiedOn DATETIME
)

当 GUESTS 表中的值发生更改(例如:来宾姓名)时,只需使用触发器将整行数据按原样注销到您的日志/审核表中。您的 GUESTS 表包含当前数据,Log/Audit 表包含旧数据。

然后使用 select 语句从两个表中获取数据:

SELECT 0 AS 'GuestLogID', GuestID, GuestName, ModifiedBy, ModifiedOn FROM [GUESTS] WHERE GuestID = 1
UNION
SELECT GuestLogID, GuestID, GuestName, ModifiedBy, ModifiedOn FROM [GUESTS_LOG] WHERE GuestID = 1
ORDER BY ModifiedOn ASC

您的数据将与表格的外观一致,从最旧到最新,第一行是创建的数据,最后一行是当前数据。您可以准确地看到更改的内容、更改的人员以及更改时间。

可选地,我曾经有一个循环通过 RecordSet 的函数(在经典 ASP 中),并且只显示网页上更改的值。它提供了一个很好的审计跟踪,以便用户可以看到随着时间的推移发生了什么变化。

【讨论】:

  • @user763539 一个字段?它会注销您正在编辑的表中的整个当前行。因此,如果将适用于您更新的所有字段。
【解决方案4】:

在 Shiva 和 dwilli 的回答中,不是静态定义表名(即 SELECT @TableName = 'PERSON'),而是一种动态获取它的方法:

SELECT @TableName = OBJECT_NAME(parent_object_id) FROM sys.objects
WHERE sys.objects.name = OBJECT_NAME(@@PROCID)

澄清:这不是我的代码,我在互联网上的某个地方找到了它,不过是很久以前的事了,所以我不记得在哪里了。另外,由于我无法在这里发表评论,因此我将其发布为答案。

【讨论】:

    【解决方案5】:

    这里有一些很好的答案可以将源代码变成更好的社区体验。如果你们中的任何一个人像我一样,正在寻找带有测试表的完整代码集,可以复制、粘贴(GitHub 上的 RAW)并执行,我在下面组装了它:

    /*this is a journey... 
        Source links:
    
        https://www.red-gate.com/simple-talk/databases/sql-server/database-administration-sql-server/pop-rivetts-sql-server-faq-no-5-pop-on-the-audit-trail/ 
        https://stackoverflow.com/questions/19737723/log-record-changes-in-sql-server-in-an-audit-table
        https://chat.stackexchange.com/transcript/message/34774768#34774768 
        http://jsbin.com/lafayiluri/1/edit?html,output 
    */
    
    -- #region | Set up the tables
    /*Firstly, we create the audit table.
    There will only need to be one of these in a database*/
    DROP TABLE IF EXISTS dbo.audit_trigger; 
    CREATE TABLE dbo.audit_trigger (
        Type CHAR(1),
        TableName VARCHAR(128),
        PK VARCHAR(1000),
        FieldName VARCHAR(128),
        OldValue VARCHAR(1000),
        NewValue VARCHAR(1000),
        UpdateDate datetime,
        UserName VARCHAR(128)
    );
    GO
    /*now we will illustrate the use of this tool
    by creating a dummy test table called TrigTest.*/
    DROP TABLE IF EXISTS dbo.trigtest;
    CREATE TABLE trigtest (
        i INT NOT NULL,
        j INT NOT NULL,
        s VARCHAR(10),
        t VARCHAR(10)
    );
    GO
    
    /*note that for this system to work there must be a primary key
    to the table but then a table without a primary key
    isn’t really a table is it?*/
    ALTER TABLE trigtest ADD CONSTRAINT pk PRIMARY KEY (i, j);
    GO
    -- #endregion 
    
    /*and now create the trigger itself. This has to be created for every
    table you want to monitor*/
    
    -- #region | trigger with bug fix
    DROP TRIGGER IF EXISTS dbo.tr_audit_trigtest;
    GO
    CREATE TRIGGER tr_audit_trigtest ON dbo.trigtest 
        FOR  
            /*uncomment INSERT if you want. The insert data is on the source table
                but sometimes your end users wanna see ALL the data in the audit table
                and hey, storage is cheap-ish now /shrug    
            */
            -- INSERT, 
            UPDATE, 
            DELETE 
    AS
    SET NOCOUNT ON;
    /*declare all the variables*/
    DECLARE @bit INT;
    DECLARE @field INT;
    DECLARE @maxfield INT;
    DECLARE @char INT;
    DECLARE @fieldname VARCHAR(128);
    DECLARE @TableName VARCHAR(128);
    DECLARE @PKCols VARCHAR(1000);
    DECLARE @sql VARCHAR(2000);
    DECLARE @UpdateDate VARCHAR(21);
    DECLARE @UserName VARCHAR(128);
    DECLARE @Type CHAR(1);
    DECLARE @PKSelect VARCHAR(1000);
    
    /*now set some of these variables*/
    SET @TableName = (
        SELECT 
            OBJECT_NAME(parent_object_id) 
        FROM sys.objects
        WHERE 
            sys.objects.name = OBJECT_NAME(@@PROCID)
    );
    SET @UserName = SYSTEM_USER;
    SET @UpdateDate = CONVERT(NVARCHAR(30), GETDATE(), 126);
    
    /*Action*/
    IF EXISTS (SELECT * FROM INSERTED)
        IF EXISTS (SELECT * FROM DELETED)
             SET @Type = 'U'
        ELSE SET @Type = 'I'
        ELSE SET @Type = 'D'
    ;
    /*get list of columns*/
    SELECT * 
    INTO #ins
    FROM INSERTED;
    
    SELECT * 
    INTO #del
    FROM DELETED;
    
    /*set @PKCols and @PKSelect via SELECT statement.*/
    SELECT @PKCols = /*Get primary key columns for full outer join*/
            COALESCE(@PKCols + ' and', ' on') 
            + ' i.[' + c.COLUMN_NAME + '] = d.[' + c.COLUMN_NAME + ']'
        FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS AS pk
            INNER JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE AS c ON (
                c.TABLE_NAME = pk.TABLE_NAME
                AND c.CONSTRAINT_NAME = pk.CONSTRAINT_NAME
            )
        WHERE pk.TABLE_NAME = @TableName
            AND CONSTRAINT_TYPE = 'PRIMARY KEY'
    ;
    SELECT @PKSelect = /*Get primary key select for insert*/
            COALESCE(@PKSelect + '+', '') 
            + '''<[' + COLUMN_NAME + ']=''+convert(varchar(100),
            coalesce(i.[' + COLUMN_NAME + '],d.[' + COLUMN_NAME + ']))+''>'''
        FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS pk,
            INFORMATION_SCHEMA.KEY_COLUMN_USAGE c
        WHERE  pk.TABLE_NAME = @TableName
            AND CONSTRAINT_TYPE = 'PRIMARY KEY'
            AND c.TABLE_NAME = pk.TABLE_NAME
            AND c.CONSTRAINT_NAME = pk.CONSTRAINT_NAME
    ;
    IF @PKCols IS NULL
    BEGIN
        RAISERROR('no PK on table %s', 16, -1, @TableName);
        RETURN;
    END
    
    SET @field = 0;
    SET @maxfield = (
        SELECT 
            MAX(
                COLUMNPROPERTY(
                    OBJECT_ID(TABLE_SCHEMA + '.' + @TableName),
                    COLUMN_NAME,
                    'ColumnID'
                )
            )
        FROM INFORMATION_SCHEMA.COLUMNS
        WHERE  
            TABLE_NAME = @TableName
    );
    
    WHILE @field < @maxfield
    BEGIN
        SET @field = (
            SELECT 
                MIN(
                    COLUMNPROPERTY(
                        OBJECT_ID(TABLE_SCHEMA + '.' + @TableName),
                        COLUMN_NAME,
                        'ColumnID'
                    )
                )
            FROM INFORMATION_SCHEMA.COLUMNS
            WHERE  
                TABLE_NAME = @TableName
                AND COLUMNPROPERTY(
                        OBJECT_ID(TABLE_SCHEMA + '.' + @TableName),
                        COLUMN_NAME,
                        'ColumnID'
                    ) > @field
        );
        SET @bit = (@field - 1)% 8 + 1;
        SET @bit = POWER(2, @bit - 1);
        SET @char = ((@field - 1) / 8) + 1;
    
        IF (
            SUBSTRING(COLUMNS_UPDATED(), @char, 1) & @bit > 0
            OR @Type IN ('I', 'D')
        )
        BEGIN
            SET @fieldname = (
                SELECT 
                    COLUMN_NAME
                FROM INFORMATION_SCHEMA.COLUMNS
                WHERE  
                    TABLE_NAME = @TableName
                    AND COLUMNPROPERTY(
                            OBJECT_ID(TABLE_SCHEMA + '.' + @TableName),
                            COLUMN_NAME,
                            'ColumnID'
                        ) = @field
            );
            SET @sql = ('
                INSERT INTO audit_trigger (    
                    Type, 
                    TableName, 
                    PK, 
                    FieldName, 
                    OldValue, 
                    NewValue, 
                    UpdateDate, 
                    UserName
                )
                SELECT ''' 
                    + @Type + ''',''' 
                    + @TableName + ''',' 
                    + @PKSelect + ',''' 
                    + @fieldname + ''''
                    + ',convert(varchar(1000),d.' + @fieldname + ')'
                    + ',convert(varchar(1000),i.' + @fieldname + ')'
                    + ',''' + @UpdateDate + ''''
                    + ',''' + @UserName + '''' + 
                ' FROM #ins AS i FULL OUTER JOIN #del AS d'
                        + @PKCols + 
                ' WHERE i.' + @fieldname + ' <> d.' + @fieldname 
                        + ' or (i.' + @fieldname + ' is null and  d.'
                        + @fieldname
                        + ' is not null)' 
                        + ' or (i.' + @fieldname + ' is not null and  d.' 
                        + @fieldname
                        + ' is null)' 
            );
            EXEC (@sql)
        END
    END
    SET NOCOUNT OFF;
    GO 
    -- #endregion 
    
    
    -- #region | now we can test the trigger out
    INSERT trigtest SELECT 1,1,'hi', 'bye';
    INSERT trigtest SELECT 2,2,'hi', 'bye';
    INSERT trigtest SELECT 3,3,'hi', 'bye';
    SELECT * FROM dbo.audit_trigger;
    SELECT * FROM trigtest;
    UPDATE trigtest SET s = 'hibye' WHERE i <> 1;
    UPDATE trigtest SET s = 'bye' WHERE i = 1;
    UPDATE trigtest SET s = 'bye' WHERE i = 1;
    UPDATE trigtest SET t = 'hi' WHERE i = 1;
    SELECT * FROM dbo.audit_trigger;
    SELECT * FROM dbo.trigtest;
    DELETE dbo.trigtest;
    SELECT * FROM dbo.audit_trigger;
    SELECT * FROM dbo.trigtest;
    GO
    
    DROP TABLE dbo.audit_trigger;
    GO
    DROP TABLE dbo.trigtest ;
    GO
    -- #endregion
    
    

    【讨论】:

      【解决方案6】:

      从 SQL Server 2016 及更高版本开始对此提供内置支持:使用 System-Versioned Temporal Tables

      【讨论】:

        【解决方案7】:

        嘿,看这个很简单

        @OLD_GUEST_NAME = d.GUEST_NAME from deleted d;

        此变量将存储您删除的旧值,然后您可以将其插入到您想要的位置。

        例如-

        Create trigger testupdate on test for update, delete
          as
        declare @tableid varchar(50);
        declare @testid varchar(50);
        declare @newdata varchar(50);
        declare @olddata varchar(50);
        
        
        select @tableid = count(*)+1 from audit_test
        select @testid=d.tableid from inserted d;
        select @olddata = d.data from deleted d;
        select @newdata = i.data from inserted i;
        
        insert into audit_test (tableid, testid, olddata, newdata) values (@tableid, @testid, @olddata, @newdata)
        
        go
        

        【讨论】:

          猜你喜欢
          • 2011-07-16
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2014-12-07
          • 2023-03-04
          • 1970-01-01
          • 1970-01-01
          • 2019-11-20
          相关资源
          最近更新 更多