【问题标题】:SQL Server: log database changes through generic triggerSQL Server:通过通用触发器记录数据库更改
【发布时间】:2020-12-03 16:07:35
【问题描述】:

this article, which is creating a trigger to log insert-, update- and delete-statements within the database 开始,我想创建一个类似的触发器,它不需要定义表及其列。这将使我们在删除或添加列时减少人为错误。

我遇到了很多问题(因此与文章相比有额外的代码),但无法克服通过触发器中的字符串化名称访问表列的问题。

-- Create the ChangeLog table
CREATE TABLE ChangeLog
(
    ID BIGINT PRIMARY KEY IDENTITY(1,1) NOT NULL,
    COMMAND NCHAR(6) NOT NULL,
    CHANGED_DATE DATETIME2 DEFAULT GETDATE() NOT NULL,
    TABLE_NAME NVARCHAR(255) NOT NULL,
    COLUMN_NAMES TEXT NULL,
    COLUMN_OLD_VALUES TEXT NULL,
    COLUMN_NEW_VALUES TEXT NULL,
    USERNAME NVARCHAR(100) NOT NULL
)
GO
-- Create Trigger for Table to log changes
ALTER TRIGGER CHANGE_MyTableName
ON MyTableName
AFTER INSERT, UPDATE, DELETE
AS
BEGIN
    -- Define which command was executed
    DECLARE @command CHAR(6)
    SET @command =
    CASE
        WHEN EXISTS(SELECT * FROM inserted) AND EXISTS(SELECT * FROM deleted) THEN 'UPDATE'
        WHEN EXISTS(SELECT * FROM inserted) THEN 'INSERT'
        WHEN EXISTS(SELECT * FROM deleted) THEN 'DELETE'
        ELSE NULL
    END

    -- Define variables
    DECLARE @seperator NVARCHAR(2)
    SET @seperator = ', '

    DECLARE @column_name NVARCHAR(255)
    DECLARE @column_names VARCHAR(MAX)
    DECLARE @column_old_values VARCHAR(MAX)
    DECLARE @column_new_values VARCHAR(MAX)

    -- Select the column names to populate @column_names separated by ', '
    SELECT @column_names = COALESCE(@column_names + @seperator, '') + COLUMN_NAME 
    FROM information_schema.columns 
    WHERE table_name = 'MyTableName'

    -- Create cursor to populate @column_old_values or/and @column_new_values
    DECLARE CURSOR_FOR_COLUMN_NAMES CURSOR FOR 
        -- Select the column name as a string
        SELECT sys.columns.name AS ColumnName
        FROM sys.columns JOIN sys.tables ON sys.columns.object_id = tables.object_id
        WHERE tables.name = 'MyTableName'

    -- Perform the first fetch.
    OPEN CURSOR_FOR_COLUMN_NAMES 

    FETCH NEXT FROM CURSOR_FOR_COLUMN_NAMES INTO @column_name

    WHILE @@FETCH_STATUS = 0
    BEGIN
        IF @command = 'UPDATE' or @command = 'DELETE'
            -- Select the old values to populate @column_old_values separated by ', '
            SELECT @column_old_values = COALESCE(@column_old_values + @seperator, '') + @column_name 
            FROM deleted

        IF @command = 'UPDATE' or @command = 'INSERT'
            -- Select the new values to populate @column_new_values separated by ', '
            SELECT @column_new_values = COALESCE(@column_new_values + @seperator, '') + @column_name 
            FROM inserted

        FETCH NEXT FROM CURSOR_FOR_COLUMN_NAMES INTO @column_name
    END

    CLOSE CURSOR_FOR_COLUMN_NAMES
    DEALLOCATE CURSOR_FOR_COLUMN_NAMES

    -- Insert into the ChangeLog table
    IF @command = 'UPDATE'
        INSERT INTO ChangeLog (COMMAND, CHANGED_DATE, TABLE_NAME, COLUMN_NAMES, COLUMN_OLD_VALUES, COLUMN_NEW_VALUES, USERNAME)
        VALUES (@command, GETDATE(), 'MyTableName', @column_names, @column_old_values, @column_new_values, USER_NAME())

    IF @command = 'INSERT'
        INSERT INTO ChangeLog (COMMAND, CHANGED_DATE, TABLE_NAME, COLUMN_NAMES, COLUMN_NEW_VALUES, USERNAME)
        VALUES (@command, GETDATE(), 'MyTableName', @column_names, @column_new_values, USER_NAME())

    IF @command = 'DELETE'
        INSERT INTO ChangeLog (COMMAND, CHANGED_DATE, TABLE_NAME, COLUMN_NAMES, COLUMN_OLD_VALUES, USERNAME)
        VALUES (@command, GETDATE(), 'MyTableName', @column_names, @column_old_values, USER_NAME())
END
GO

当前代码为COLUMN_NAMESCOLUMN_OLD_VALUESCOLUMN_NEW_VALUES 创建了一个具有相同值的行,因为填充@column_old_values@column_new_values 的代码实际上执行SELECT 'column_name' FROM AnyTable 而不是SELECT columnn_name FROM MyTableName 但是我似乎无法弄清楚如何解决这个问题。

我尝试使用 EXEC() 命令,但这似乎部分结束了我的触发器?即使代码编译,我仍然收到关于未闭合引号的错误。

消息 102,第 15 级,状态 1,第 16 行
'=' 附近的语法不正确。

消息 105,级别 15,状态 1,第 16 行
字符串 ') + ColumnName1 FROM deleted' 后面的非右引号。

我认为这个通用触发器会在这个问题得到解决后立即起作用,但当然,我也会对替代解决方案感到满意。

【问题讨论】:

  • 我真的建议不要使用该触发器,因为它仍然存在。使用CURSOR 几乎总是您在编写SQL 时应该做的最后一件事,并且将CURSOR 放在TRIGGER 中是降低任何影响表的DML 语句性能的可靠方法。如果您必须在触发器中执行任何操作,则它需要尽可能降低影响,并且当然需要基于设置; CURSOR 不是这些东西。
  • ...在 不可以时减少人为错误 - 执行影响分析是无可替代的。如果您认为您可以对 table 进行更改并期望所有依赖代码能够神奇地“调整”以适应更改,那么您只是在自欺欺人。要做到这一点,您需要在动态 tsql 方面有一些严肃的技能。混合使用 varchar 和 nvarchar 表明您需要帮助。使用 TEXT 数据类型是完全错误的。为什么不使用 CDC?
  • 当 SQL Server 提供开箱即用的功能时,您为什么要尝试从头开始编写此功能?看看“SQL Server 更改跟踪”
  • 感谢大家的评论。现在,我决定采用公认的答案,因为这以一种优雅的方式解决了这个问题。没有CURSORs 剩下并且混合类型已经减少(也改变了文本类型,因为inserted- 和`deleted-tables 不支持选择这些类型)。稍后我将研究开箱即用的功能,因为这看起来确实很有前景

标签: sql sql-server triggers database-cursor


【解决方案1】:

您当前代码的一些错误包括:

  • 0 行受到影响时出错,因为所有表都将为空但您没有处理 NULL 命令,并且在尝试将 NULL 命令插入 ChangeLog 时会产生错误

  • 您的光标会将所有受影响的行串成一种奇怪的方式;即使您让它工作,如果 4 行受到影响,您的 ChangeLog 中也会有 1 行 column_old_values 将包含类似 (col1, col1, col1, col1, col2, col2, col2, col2) 的内容。

  • 您的游标需要动态 SQL 才能使用动态列名,但与您的代码相比,动态 SQL 的范围不同,因此您需要复制 inserteddeleted 触发器范围表以使用动态 SQL。

  • 您的动态 SQL 正在尝试使用不同范围内不存在的变量。如果将动态 SQL 放入字符串中,然后在尝试EXEC 之前打印字符串以供审核,则调试动态 SQL 会容易得多。

编辑:

这个选项怎么样,它不依赖于知道列,而是依赖于事先知道表 PK 的?这些不应该像其他列那样经常更改,并且此列的性能大大优于您尝试执行的操作。这是我在一张桌子上实施的示例,我们不确定我们的几十个用户中的一个是否仍在使用它,我需要在一年内对其进行跟踪。

-- Create Trigger for Table to log changes
ALTER TRIGGER AUDIT_MyTableName
ON bookings
AFTER INSERT, UPDATE, DELETE
AS
BEGIN
    SET NOCOUNT ON;

    -- Grab trx type
    DECLARE @command char(6) 
    SET @command =
    CASE
        WHEN EXISTS(SELECT 1 FROM inserted) AND EXISTS(SELECT 1 FROM deleted) THEN 'UPDATE'
        WHEN EXISTS(SELECT 1 FROM inserted) THEN 'INSERT'
        WHEN EXISTS(SELECT 1 FROM deleted) THEN 'DELETE'
        ELSE '0 ROWS' -- if no rows affected, trigger does NOT record an entry
    END 
 
    IF @command = 'INSERT'

        -- Add audit entry
        INSERT INTO ChangeLog (COMMAND, CHANGED_DATE, TABLE_NAME, /*COLUMN_NAMES,*/ COLUMN_OLD_VALUES, COLUMN_NEW_VALUES, USERNAME)
        SELECT 
            Command     = @command, 
            ChangeDate  = GETDATE(), 
            TableName   = 'bookings', 
            --ColNames  = @column_names, 
            Column_OLD_Values   = NULL, 
            Column_NEW_Values   = (SELECT inserted.* for xml path('')), 
            Username    = SUSER_SNAME()
        FROM inserted 
    
    ELSE IF @command = 'DELETE'

        -- Add audit entry
        INSERT INTO ChangeLog (COMMAND, CHANGED_DATE, TABLE_NAME, /*COLUMN_NAMES,*/ COLUMN_OLD_VALUES, COLUMN_NEW_VALUES, USERNAME)
        SELECT 
            Command     = @command, 
            ChangeDate  = GETDATE(), 
            TableName   = 'bookings', 
            --ColNames  = @column_names, 
            Column_OLD_Values   = (SELECT deleted.* for xml path('')), 
            Column_NEW_Values   = NULL,
            Username    = SUSER_SNAME()
        FROM deleted

    ELSE -- is UPDATE 

        -- Add audit entry
        INSERT INTO ChangeLog (COMMAND, CHANGED_DATE, TABLE_NAME, /*COLUMN_NAMES,*/ COLUMN_OLD_VALUES, COLUMN_NEW_VALUES, USERNAME)
        SELECT 
            Command     = @command, 
            ChangeDate  = GETDATE(), 
            TableName   = 'bookings', 
            --ColNames  = @column_names, 
            Column_OLD_Values   = (SELECT deleted.* for xml path('')), 
            Column_NEW_Values   = (SELECT inserted.* for xml path('')), 
            Username    = SUSER_SNAME()
        FROM inserted 
        INNER JOIN deleted ON inserted.bookingID = deleted.bookingID -- join on w/e the PK is
END

无论您需要什么,结果都是完美的:

如果您愿意将 COLUMN_OLD_VALUESCOLUMN_NEW_VALUES 的列类型更改为 XML,您只需在每个 for xml path('') 之后添加 , type,并且 XML 可点击且易于在 SSMS 中阅读.

Column_OLD_Values   = (SELECT deleted.* for xml path(''), type), 
Column_NEW_Values   = (SELECT inserted.* for xml path(''), type), 

【讨论】:

  • 感谢您指出我最初的问题并将它们重写为一个优雅的解决方案。由于它比我想要的更好(XML 而不是分隔字符串),我无法要求更好的答案。仅供参考和其他实施解决方案的人:您不能使用ntexttextimage 类型在表上编写触发器,这些类型需要转换(例如text = nvarchar(max))跨度>
猜你喜欢
  • 1970-01-01
  • 2015-09-01
  • 2017-07-22
  • 2021-07-31
  • 2013-03-29
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多