SQL 服务器:通过通用触发器记录数据库更改
SQL Server: log database changes through generic trigger
从 this article, which is creating a trigger to log insert-, update- and delete-statements within the database 开始,我想创建一个不需要定义 table 及其列的类似触发器。这将减少删除或添加列时的人为错误。
我遇到了很多问题(因此与文章相比有额外的代码)但无法克服通过触发器中的字符串化名称访问 table 列的问题。
-- 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_NAMES
、COLUMN_OLD_VALUES
和 COLUMN_NEW_VALUES
创建了一个具有相同值的行,因为填充 @column_old_values
和 [=17= 的代码] 实际上执行 SELECT 'column_name' FROM AnyTable
而不是 SELECT columnn_name FROM MyTableName
但我似乎无法弄清楚如何解决这个问题。
我试过使用 EXEC() 命令,但这似乎部分结束了我的触发器?即使代码可以编译,我仍然收到有关未闭合引号的错误。
Msg 102, Level 15, State 1, Line 16
Incorrect syntax near '='.
Msg 105, Level 15, State 1, Line 16
Unclosed quotation mark after the character string ') + ColumnName1 FROM deleted'.
我认为只要这个问题得到解决,这个通用触发器就会起作用,当然,我也会对其他解决方案感到满意。
您当前代码的一些错误包括:
当0行受到影响时出现错误,因为所有tables都会为空但您没有处理NULL命令,并且在尝试插入NULL时会产生错误命令进入 ChangeLog
您的光标会将所有受影响的行串成一种奇怪的方式;即使你让它工作,如果 4 行受到影响,你的 ChangeLog 中也会有 1 行,其中 column_old_values 会包含类似 (col1, col1, col1, col1, col2, col2, col2, col2) .
您的游标需要动态 SQL 才能使用动态列名,但动态 SQL 与您的代码相比处于不同的范围,因此您需要复制一份inserted
和 deleted
触发范围 table 的使用动态 SQL.
您的动态 SQL 正在尝试使用不同范围内不存在的变量。如果将动态 SQL 放入字符串中,然后在尝试 EXEC
之前打印该字符串以供审查,调试起来会容易得多。
编辑:
这个选项怎么样,它不依赖于了解列但依赖于事先了解 table PK?这些栏目不应像其他栏目那样经常更改,而且此栏目的性能 大大 优于您尝试执行的操作。这是我在 table 上实施的一个示例,我们不确定我们的几十个用户中的一个是否仍在使用它,我需要跟踪它一年多。
-- 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_VALUES
和 COLUMN_NEW_VALUES
的列类型更改为 XML,您只需在每个 [=18= 之后添加 , type
] 并且 XML 在 SSMS 中可点击且易于阅读。
Column_OLD_Values = (SELECT deleted.* for xml path(''), type),
Column_NEW_Values = (SELECT inserted.* for xml path(''), type),
从 this article, which is creating a trigger to log insert-, update- and delete-statements within the database 开始,我想创建一个不需要定义 table 及其列的类似触发器。这将减少删除或添加列时的人为错误。
我遇到了很多问题(因此与文章相比有额外的代码)但无法克服通过触发器中的字符串化名称访问 table 列的问题。
-- 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_NAMES
、COLUMN_OLD_VALUES
和 COLUMN_NEW_VALUES
创建了一个具有相同值的行,因为填充 @column_old_values
和 [=17= 的代码] 实际上执行 SELECT 'column_name' FROM AnyTable
而不是 SELECT columnn_name FROM MyTableName
但我似乎无法弄清楚如何解决这个问题。
我试过使用 EXEC() 命令,但这似乎部分结束了我的触发器?即使代码可以编译,我仍然收到有关未闭合引号的错误。
Msg 102, Level 15, State 1, Line 16
Incorrect syntax near '='.Msg 105, Level 15, State 1, Line 16
Unclosed quotation mark after the character string ') + ColumnName1 FROM deleted'.
我认为只要这个问题得到解决,这个通用触发器就会起作用,当然,我也会对其他解决方案感到满意。
您当前代码的一些错误包括:
当0行受到影响时出现错误,因为所有tables都会为空但您没有处理NULL命令,并且在尝试插入NULL时会产生错误命令进入 ChangeLog
您的光标会将所有受影响的行串成一种奇怪的方式;即使你让它工作,如果 4 行受到影响,你的 ChangeLog 中也会有 1 行,其中 column_old_values 会包含类似 (col1, col1, col1, col1, col2, col2, col2, col2) .
您的游标需要动态 SQL 才能使用动态列名,但动态 SQL 与您的代码相比处于不同的范围,因此您需要复制一份
inserted
和deleted
触发范围 table 的使用动态 SQL.您的动态 SQL 正在尝试使用不同范围内不存在的变量。如果将动态 SQL 放入字符串中,然后在尝试
EXEC
之前打印该字符串以供审查,调试起来会容易得多。
编辑:
这个选项怎么样,它不依赖于了解列但依赖于事先了解 table PK?这些栏目不应像其他栏目那样经常更改,而且此栏目的性能 大大 优于您尝试执行的操作。这是我在 table 上实施的一个示例,我们不确定我们的几十个用户中的一个是否仍在使用它,我需要跟踪它一年多。
-- 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_VALUES
和 COLUMN_NEW_VALUES
的列类型更改为 XML,您只需在每个 [=18= 之后添加 , type
] 并且 XML 在 SSMS 中可点击且易于阅读。
Column_OLD_Values = (SELECT deleted.* for xml path(''), type),
Column_NEW_Values = (SELECT inserted.* for xml path(''), type),