SQL Server 更新触发器,仅获取已修改的字段
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/1254787/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
SQL Server Update Trigger, Get Only modified fields
提问by Akash Kava
I am aware of COLUMNS_UPDATED
, well I need some quick shortcut (if anyone has made, I am already making one, but if anyone can save my time, I will appriciate it)
我知道COLUMNS_UPDATED
,我需要一些快速的捷径(如果有人做过,我已经在做,但如果有人能节省我的时间,我会喜欢它)
I need basicaly an XML of only updated column values, I need this for replication purpose.
我基本上只需要一个仅更新列值的 XML,我需要这个用于复制目的。
SELECT * FROM inserted gives me each column, but I need only updated ones.
SELECT * FROM insert 给了我每一列,但我只需要更新的列。
something like following...
类似以下...
CREATE TRIGGER DBCustomers_Insert
ON DBCustomers
AFTER UPDATE
AS
BEGIN
DECLARE @sql as NVARCHAR(1024);
SET @sql = 'SELECT ';
I NEED HELP FOR FOLLOWING LINE ...., I can manually write every column, but I need
an automated routin which can work regardless of column specification
for each column, if its modified append $sql = ',' + columnname...
SET @sql = $sql + ' FROM inserted FOR XML RAW';
DECLARE @x as XML;
SET @x = CAST(EXEC(@sql) AS XML);
.. use @x
END
回答by Stephen Turner
I've another completely different solution that doesn't use COLUMNS_UPDATED at all, nor does it rely on building dynamic SQL at runtime. (You might want to use dynamic SQL at design time but thats another story.)
我有另一个完全不同的解决方案,它根本不使用 COLUMNS_UPDATED,也不依赖于在运行时构建动态 SQL。(您可能想在设计时使用动态 SQL,但那是另一回事。)
Basically you start with the inserted and deleted tables, unpivot each of them so you are just left with the unique key, field value and field name columns for each. Then you join the two and filter for anything that's changed.
基本上,您从插入和删除的表开始,对每个表进行逆透视,这样您就只剩下每个表的唯一键、字段值和字段名称列。然后你加入两者并过滤任何改变的东西。
Here is a full working example, including some test calls to show what is logged.
这是一个完整的工作示例,包括一些测试调用以显示记录的内容。
-- -------------------- Setup tables and some initial data --------------------
CREATE TABLE dbo.Sample_Table (ContactID int, Forename varchar(100), Surname varchar(100), Extn varchar(16), Email varchar(100), Age int );
INSERT INTO Sample_Table VALUES (1,'Bob','Smith','2295','[email protected]',24);
INSERT INTO Sample_Table VALUES (2,'Alice','Brown','2255','[email protected]',32);
INSERT INTO Sample_Table VALUES (3,'Reg','Jones','2280','[email protected]',19);
INSERT INTO Sample_Table VALUES (4,'Mary','Doe','2216','[email protected]',28);
INSERT INTO Sample_Table VALUES (5,'Peter','Nash','2214','[email protected]',25);
CREATE TABLE dbo.Sample_Table_Changes (ContactID int, FieldName sysname, FieldValueWas sql_variant, FieldValueIs sql_variant, modified datetime default (GETDATE()));
GO
-- -------------------- Create trigger --------------------
CREATE TRIGGER TriggerName ON dbo.Sample_Table FOR DELETE, INSERT, UPDATE AS
BEGIN
SET NOCOUNT ON;
--Unpivot deleted
WITH deleted_unpvt AS (
SELECT ContactID, FieldName, FieldValue
FROM
(SELECT ContactID
, cast(Forename as sql_variant) Forename
, cast(Surname as sql_variant) Surname
, cast(Extn as sql_variant) Extn
, cast(Email as sql_variant) Email
, cast(Age as sql_variant) Age
FROM deleted) p
UNPIVOT
(FieldValue FOR FieldName IN
(Forename, Surname, Extn, Email, Age)
) AS deleted_unpvt
),
--Unpivot inserted
inserted_unpvt AS (
SELECT ContactID, FieldName, FieldValue
FROM
(SELECT ContactID
, cast(Forename as sql_variant) Forename
, cast(Surname as sql_variant) Surname
, cast(Extn as sql_variant) Extn
, cast(Email as sql_variant) Email
, cast(Age as sql_variant) Age
FROM inserted) p
UNPIVOT
(FieldValue FOR FieldName IN
(Forename, Surname, Extn, Email, Age)
) AS inserted_unpvt
)
--Join them together and show what's changed
INSERT INTO Sample_Table_Changes (ContactID, FieldName, FieldValueWas, FieldValueIs)
SELECT Coalesce (D.ContactID, I.ContactID) ContactID
, Coalesce (D.FieldName, I.FieldName) FieldName
, D.FieldValue as FieldValueWas
, I.FieldValue AS FieldValueIs
FROM
deleted_unpvt d
FULL OUTER JOIN
inserted_unpvt i
on D.ContactID = I.ContactID
AND D.FieldName = I.FieldName
WHERE
D.FieldValue <> I.FieldValue --Changes
OR (D.FieldValue IS NOT NULL AND I.FieldValue IS NULL) -- Deletions
OR (D.FieldValue IS NULL AND I.FieldValue IS NOT NULL) -- Insertions
END
GO
-- -------------------- Try some changes --------------------
UPDATE Sample_Table SET age = age+1;
UPDATE Sample_Table SET Extn = '5'+Extn where Extn Like '221_';
DELETE FROM Sample_Table WHERE ContactID = 3;
INSERT INTO Sample_Table VALUES (6,'Stephen','Turner','2299','[email protected]',25);
UPDATE Sample_Table SET ContactID = 7 where ContactID = 4; --this will be shown as a delete and an insert
-- -------------------- See the results --------------------
SELECT *, SQL_VARIANT_PROPERTY(FieldValueWas, 'BaseType') FieldBaseType, SQL_VARIANT_PROPERTY(FieldValueWas, 'MaxLength') FieldMaxLength from Sample_Table_Changes;
-- -------------------- Cleanup --------------------
DROP TABLE dbo.Sample_Table; DROP TABLE dbo.Sample_Table_Changes;
So no messing around with bigint bitfields and arth overflow problems. If you know the columns you want to compare at design time then you don't need any dynamic SQL.
所以不要搞乱 bigint 位域和 arth 溢出问题。如果您知道在设计时要比较的列,那么您不需要任何动态 SQL。
On the downside the output is in a different format and all the field values are converted to sql_variant, the first could be fixed by pivoting the output again, and the second could be fixed by recasting back to the required types based on your knowledge of the design of the table, but both of these would require some complex dynamic sql. Both of these might not be an issue in your XML output. This questiondoes something similar to getting the output back in the same format.
不利的一面是,输出采用不同的格式,并且所有字段值都转换为 sql_variant,第一个可以通过再次旋转输出来修复,第二个可以通过根据您的知识重新转换回所需类型来修复表的设计,但这两者都需要一些复杂的动态sql。在您的 XML 输出中,这两者都可能不是问题。这个问题的作用类似于以相同的格式返回输出。
Edit: Reviewing the comments below, if you have a natural primary key that could change then you can still use this method. You just need to add a column that is populated by default with a GUID using the NEWID() function. You then use this column in place of the primary key.
编辑:查看下面的评论,如果您有一个可以更改的自然主键,那么您仍然可以使用此方法。您只需要使用 NEWID() 函数添加一个默认填充有 GUID 的列。然后使用此列代替主键。
You may want to add an index to this field, but as the deleted and inserted tables in a trigger are in memory it might not get used and may have a negative effect on performance.
您可能希望向该字段添加索引,但由于触发器中已删除和插入的表都在内存中,它可能不会被使用并且可能对性能产生负面影响。
回答by podosta
Inside the trigger, you can use COLUMNS_UPDATED()
like this in order to get updated value
在触发器内部,您可以COLUMNS_UPDATED()
像这样使用以获得更新的值
-- Get the table id of the trigger
--
DECLARE @idTable INT
SELECT @idTable = T.id
FROM sysobjects P JOIN sysobjects T ON P.parent_obj = T.id
WHERE P.id = @@procid
-- Get COLUMNS_UPDATED if update
--
DECLARE @Columns_Updated VARCHAR(50)
SELECT @Columns_Updated = ISNULL(@Columns_Updated + ', ', '') + name
FROM syscolumns
WHERE id = @idTable
AND CONVERT(VARBINARY,REVERSE(COLUMNS_UPDATED())) & POWER(CONVERT(BIGINT, 2), colorder - 1) > 0
But this snipet of code fails when you have a table with more than 62 columns.. Arth.Overflow...
但是当你有一个超过 62 列的表时,这段代码会失败...... Arth.Overflow ...
Here is the final version which handles more than 62 columns but give only the number of the updated columns. It's easy to link with 'syscolumns' to get the name
这是处理超过 62 列但仅提供更新列数的最终版本。很容易与“syscolumns”链接以获取名称
DECLARE @Columns_Updated VARCHAR(100)
SET @Columns_Updated = ''
DECLARE @maxByteCU INT
DECLARE @curByteCU INT
SELECT @maxByteCU = DATALENGTH(COLUMNS_UPDATED()),
@curByteCU = 1
WHILE @curByteCU <= @maxByteCU BEGIN
DECLARE @cByte INT
SET @cByte = SUBSTRING(COLUMNS_UPDATED(), @curByteCU, 1)
DECLARE @curBit INT
DECLARE @maxBit INT
SELECT @curBit = 1,
@maxBit = 8
WHILE @curBit <= @maxBit BEGIN
IF CONVERT(BIT, @cByte & POWER(2,@curBit - 1)) <> 0
SET @Columns_Updated = @Columns_Updated + '[' + CONVERT(VARCHAR, 8 * (@curByteCU - 1) + @curBit) + ']'
SET @curBit = @curBit + 1
END
SET @curByteCU = @curByteCU + 1
END
回答by Micha? Powaga
I've done it as simple "one-liner". Without using, pivot, loops, many variables etc. that makes it looking like procedural programming. SQL should be used to process data sets :-), the solution is:
我已经把它做为简单的“单线”。没有使用、枢轴、循环、许多变量等,这使它看起来像过程式编程。应该使用SQL来处理数据集:-),解决方法是:
DECLARE @sql as NVARCHAR(1024);
select @sql = coalesce(@sql + ',' + quotename(column_name), quotename(column_name))
from INFORMATION_SCHEMA.COLUMNS
where substring(columns_updated(), columnproperty(object_id(table_schema + '.' + table_name, 'U'), column_name, 'columnId') / 8 + 1, 1) & power(2, -1 + columnproperty(object_id(table_schema + '.' + table_name, 'U'), column_name, 'columnId') % 8 ) > 0
and table_name = 'DBCustomers'
-- and column_name in ('c1', 'c2') -- limit to specific columns
-- and column_name not in ('c3', 'c4') -- or exclude specific columns
SET @sql = 'SELECT ' + @sql + ' FROM inserted FOR XML RAW';
DECLARE @x as XML;
SET @x = CAST(EXEC(@sql) AS XML);
It uses COLUMNS_UPDATED, takes care of more than eight columns - it handles as many columns as you want.
它使用COLUMNS_UPDATED,处理超过八列 - 它可以处理任意数量的列。
It takes care on proper columns order which should be get using COLUMNPROPERTY.
它注意正确的列顺序,应该使用COLUMNPROPERTY获得。
It is based on view COLUMNSso it may include or exclude only specific columns.
它基于视图COLUMNS,因此它可能只包含或排除特定的列。
回答by Rick
The below code works for over 64 columns and logs only the updated columns. Follow the instruction in the comments and all should be well.
下面的代码适用于超过 64 列,并且只记录更新的列。按照评论中的说明进行操作,一切都会好起来的。
/*******************************************************************************************
* Add the below table to your database to track data changes using the trigger *
* below. Remember to change the variables in the trigger to match the table that *
* will be firing the trigger *
*******************************************************************************************/
SET ANSI_NULLS ON;
GO
SET QUOTED_IDENTIFIER ON;
GO
CREATE TABLE [dbo].[AuditDataChanges]
(
[RecordId] [INT] IDENTITY(1, 1)
NOT NULL ,
[TableName] [VARCHAR](50) NOT NULL ,
[RecordPK] [VARCHAR](50) NOT NULL ,
[ColumnName] [VARCHAR](50) NOT NULL ,
[OldValue] [VARCHAR](50) NULL ,
[NewValue] [VARCHAR](50) NULL ,
[ChangeDate] [DATETIME2](7) NOT NULL ,
[UpdatedBy] [VARCHAR](50) NOT NULL ,
CONSTRAINT [PK_AuditDataChanges] PRIMARY KEY CLUSTERED
( [RecordId] ASC )
WITH ( PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF,
IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON,
ALLOW_PAGE_LOCKS = ON ) ON [PRIMARY]
)
ON [PRIMARY];
GO
ALTER TABLE [dbo].[AuditDataChanges] ADD CONSTRAINT [DF_AuditDataChanges_ChangeDate] DEFAULT (GETDATE()) FOR [ChangeDate];
GO
/************************************************************************************************
* Add the below trigger to any table you want to audit data changes on. Changes will be saved *
* in the AuditChangesTable. *
************************************************************************************************/
ALTER TRIGGER trg_Survey_Identify_Updated_Columns ON Survey --Change to match your table name
FOR INSERT, UPDATE
AS
SET NOCOUNT ON;
DECLARE @sql VARCHAR(5000) ,
@sqlInserted NVARCHAR(500) ,
@sqlDeleted NVARCHAR(500) ,
@NewValue NVARCHAR(100) ,
@OldValue NVARCHAR(100) ,
@UpdatedBy VARCHAR(50) ,
@ParmDefinitionD NVARCHAR(500) ,
@ParmDefinitionI NVARCHAR(500) ,
@TABLE_NAME VARCHAR(100) ,
@COLUMN_NAME VARCHAR(100) ,
@modifiedColumnsList NVARCHAR(4000) ,
@ColumnListItem NVARCHAR(500) ,
@Pos INT ,
@RecordPk VARCHAR(50) ,
@RecordPkName VARCHAR(50);
SELECT *
INTO #deleted
FROM deleted;
SELECT *
INTO #Inserted
FROM inserted;
SET @TABLE_NAME = 'Survey'; ---Change to your table name
SELECT @UpdatedBy = UpdatedBy --Change to your column name for the user update field
FROM inserted;
SELECT @RecordPk = SurveyId --Change to the table primary key field
FROM inserted;
SET @RecordPkName = 'SurveyId';
SET @modifiedColumnsList = STUFF(( SELECT ',' + name
FROM sys.columns
WHERE object_id = OBJECT_ID(@TABLE_NAME)
AND SUBSTRING(COLUMNS_UPDATED(),
( ( column_id
- 1 ) / 8 + 1 ),
1) & ( POWER(2,
( ( column_id
- 1 ) % 8 + 1 )
- 1) ) = POWER(2,
( column_id - 1 )
% 8)
FOR
XML PATH('')
), 1, 1, '');
WHILE LEN(@modifiedColumnsList) > 0
BEGIN
SET @Pos = CHARINDEX(',', @modifiedColumnsList);
IF @Pos = 0
BEGIN
SET @ColumnListItem = @modifiedColumnsList;
END;
ELSE
BEGIN
SET @ColumnListItem = SUBSTRING(@modifiedColumnsList, 1,
@Pos - 1);
END;
SET @COLUMN_NAME = @ColumnListItem;
SET @ParmDefinitionD = N'@OldValueOut NVARCHAR(100) OUTPUT';
SET @ParmDefinitionI = N'@NewValueOut NVARCHAR(100) OUTPUT';
SET @sqlDeleted = N'SELECT @OldValueOut=' + @COLUMN_NAME
+ ' FROM #deleted where ' + @RecordPkName + '='
+ CONVERT(VARCHAR(50), @RecordPk);
SET @sqlInserted = N'SELECT @NewValueOut=' + @COLUMN_NAME
+ ' FROM #Inserted where ' + @RecordPkName + '='
+ CONVERT(VARCHAR(50), @RecordPk);
EXECUTE sp_executesql @sqlDeleted, @ParmDefinitionD,
@OldValueOut = @OldValue OUTPUT;
EXECUTE sp_executesql @sqlInserted, @ParmDefinitionI,
@NewValueOut = @NewValue OUTPUT;
IF ( LTRIM(RTRIM(@NewValue)) != LTRIM(RTRIM(@OldValue)) )
BEGIN
SET @sql = 'INSERT INTO [dbo].[AuditDataChanges]
([TableName]
,[RecordPK]
,[ColumnName]
,[OldValue]
,[NewValue]
,[UpdatedBy])
VALUES
(' + QUOTENAME(@TABLE_NAME, '''') + '
,' + QUOTENAME(@RecordPk, '''') + '
,' + QUOTENAME(@COLUMN_NAME, '''') + '
,' + QUOTENAME(@OldValue, '''') + '
,' + QUOTENAME(@NewValue, '''') + '
,' + QUOTENAME(@UpdatedBy, '''') + ')';
EXEC (@sql);
END;
SET @COLUMN_NAME = '';
SET @NewValue = '';
SET @OldValue = '';
IF @Pos = 0
BEGIN
SET @modifiedColumnsList = '';
END;
ELSE
BEGIN
-- start substring at the character after the first comma
SET @modifiedColumnsList = SUBSTRING(@modifiedColumnsList,
@Pos + 1,
LEN(@modifiedColumnsList)
- @Pos);
END;
END;
DROP TABLE #Inserted;
DROP TABLE #deleted;
GO
回答by cmsjr
The only way that occurs to me that you could accomplish this without hard coding column names would be to drop the contents of the deleted table to a temp table, then build a query based on the table definition to to compare the contents of your temp table and the actual table, and return a delimited column list based on whether they do or do not match. Admittedly, the below is elaborate.
在我看来,您可以在没有硬编码列名的情况下完成此操作的唯一方法是将已删除表的内容删除到临时表,然后根据表定义构建查询以比较临时表的内容和实际表,并根据它们是否匹配返回一个分隔的列列表。诚然,下面是详细说明。
Declare @sql nvarchar(4000)
DECLARE @ParmDefinition nvarchar(500)
Declare @OutString varchar(8000)
Declare @tbl sysname
Set @OutString = ''
Set @tbl = 'SomeTable' --The table we are interested in
--Store the contents of deleted in temp table
Select * into #tempDelete from deleted
--Build sql string based on definition
--of table
--to retrieve the column name
--or empty string
--based on comparison between
--target table and temp table
set @sql = ''
Select @sql = @sql + 'Case when IsNull(i.[' + Column_Name +
'],0) = IsNull(d.[' + Column_name + '],0) then ''''
else ' + quotename(Column_Name, char(39)) + ' + '',''' + ' end +'
from information_schema.columns
where table_name = @tbl
--Define output parameter
set @ParmDefinition = '@OutString varchar(8000) OUTPUT'
--Format sql
set @sql = 'Select @OutString = '
+ Substring(@sql,1 , len(@sql) -1) +
' From SomeTable i ' --Will need to be updated for target schema
+ ' inner join #tempDelete d on
i.PK = d.PK ' --Will need to be updated for target schema
--Execute sql and retrieve desired column list in output parameter
exec sp_executesql @sql, @ParmDefinition, @OutString OUT
drop table #tempDelete
--strip trailing column if a non-zero length string
--was returned
if Len(@Outstring) > 0
Set @OutString = Substring(@OutString, 1, Len(@Outstring) -1)
--return comma delimited list of changed columns.
Select @OutString
End
回答by Pavel Samoylenko
I transformed the accepted answer to get list of column names separated by comma (according to author's recommendation). Output - "Columns_Updated" as 'Column1,Column2,Column5'
我将接受的答案转换为以逗号分隔的列名列表(根据作者的建议)。输出 - "Columns_Updated" 为 'Column1,Column2,Column5'
-- get names of updated columns
DECLARE @idTable INT
declare @ColumnName nvarchar(300)
declare @ColId int
SELECT @idTable = T.id
FROM sysobjects P JOIN sysobjects T ON P.parent_obj = T.id
WHERE P.id = @@procid
DECLARE @changedProperties nvarchar(max) = ''
DECLARE @Columns_Updated VARCHAR(2000) = ''
DECLARE @maxByteCU INT
DECLARE @curByteCU INT
SELECT @maxByteCU = DATALENGTH(COLUMNS_UPDATED()),
@curByteCU = 1
WHILE @curByteCU <= @maxByteCU BEGIN
DECLARE @cByte INT
SET @cByte = SUBSTRING(COLUMNS_UPDATED(), @curByteCU, 1)
DECLARE @curBit INT
DECLARE @maxBit INT
SELECT @curBit = 1,
@maxBit = 8
WHILE @curBit <= @maxBit BEGIN
IF CONVERT(BIT, @cByte & POWER(2, @curBit - 1)) <> 0 BEGIN
SET @ColId = cast( CONVERT(VARCHAR, 8 * (@curByteCU - 1) + @curBit) as int)
select @ColumnName = [Name]
FROM syscolumns
WHERE id = @idTable and colid = @ColId
SET @Columns_Updated = @Columns_Updated + ',' + @ColumnName
END
SET @curBit = @curBit + 1
END
SET @curByteCU = @curByteCU + 1
END
回答by Bhadresh Patel
This is perfect example for track log of updated columnwise value with unique records and UpdatedBy user.
这是使用唯一记录和 UpdatedBy 用户跟踪更新列值的完美示例。
IF NOT EXISTS
(SELECT * FROM sysobjects WHERE id = OBJECT_ID(N'[dbo].[ColumnAuditLogs]')
AND OBJECTPROPERTY(id, N'IsUserTable') = 1)
CREATE TABLE ColumnAuditLogs
(Type CHAR(1),
TableName VARCHAR(128),
PK VARCHAR(1000),
FieldName VARCHAR(128),
OldValue VARCHAR(1000),
NewValue VARCHAR(1000),
UpdateDate datetime,
UserName VARCHAR(128),
UniqueId uniqueidentifier,
UpdatedBy int
)
GO
create TRIGGER TR_ABCTable_AUDIT ON ABCTable 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),
@UniqueId varchar(100),
@UpdatedBy VARCHAR(50)
--You will need to change @TableName to match the table to be audited.
-- Here we made ABCTable for your example.
SELECT @TableName = 'ABCTable' -- change table name accoring your table name
-- use for table unique records for everytime updation.
set @UniqueId = CONVERT(varchar(100),newID())
-- date and user
SELECT @UserName = SYSTEM_USER ,
@UpdateDate = CONVERT (NVARCHAR(30),GETDATE(),126)
SELECT @UpdatedBy = ModifiedBy --Change to your column name for the user update field
FROM inserted;
-- 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+'+','')
+ '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 ColumnAuditLogs ( Type,
TableName,
PK,
FieldName,
OldValue,
NewValue,
UpdateDate,
UserName,
UniqueId,
[UpdatedBy])
select ''' + @Type + ''','''
+ @TableName + ''',' + @PKSelect
+ ',''' + @fieldname + ''''
+ ',convert(varchar(1000),d.' + @fieldname + ')'
+ ',convert(varchar(1000),i.' + @fieldname + ')'
+ ',''' + @UpdateDate + ''''
+ ',''' + @UserName + ''''
+ ',''' + @UniqueId + ''''
+ ',' + QUOTENAME(@UpdatedBy, '''')
+ ' 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
回答by RAY
The sample code provided by Rick lack handling for multiple rows update.
Rick 提供的示例代码缺乏对多行更新的处理。
Please let me enhance Rick's version as below:
请让我增强 Rick 的版本如下:
USE [AFC]
GO
/****** Object: Trigger [dbo].[trg_Survey_Identify_Updated_Columns] Script Date: 27/7/2018 14:08:49 ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
ALTER TRIGGER [dbo].[trg_Survey_Identify_Updated_Columns] ON [dbo].[Sample_Table] --Change to match your table name
FOR INSERT
,UPDATE
AS
SET NOCOUNT ON;
DECLARE @sql VARCHAR(5000)
,@sqlInserted NVARCHAR(500)
,@sqlDeleted NVARCHAR(500)
,@NewValue NVARCHAR(100)
,@OldValue NVARCHAR(100)
,@UpdatedBy VARCHAR(50)
,@ParmDefinitionD NVARCHAR(500)
,@ParmDefinitionI NVARCHAR(500)
,@TABLE_NAME VARCHAR(100)
,@COLUMN_NAME VARCHAR(100)
,@modifiedColumnsList NVARCHAR(4000)
,@ColumnListItem NVARCHAR(500)
,@Pos INT
,@RecordPk VARCHAR(50)
,@RecordPkName VARCHAR(50);
SELECT *
INTO #deleted
FROM deleted;
SELECT *
INTO #Inserted
FROM inserted;
SET @TABLE_NAME = 'Sample_Table';---Change to your table name
DECLARE t_cursor CURSOR
FOR
SELECT ContactID
FROM inserted
OPEN t_cursor
FETCH NEXT
FROM t_cursor
INTO @RecordPk
WHILE @@FETCH_STATUS = 0
BEGIN
--SELECT @UpdatedBy = Surname --Change to your column name for the user update field
--FROM inserted;
--SELECT @RecordPk = ContactID --Change to the table primary key field
--FROM inserted;
SET @RecordPkName = 'ContactID';
SET @modifiedColumnsList = STUFF((
SELECT ',' + name
FROM sys.columns
WHERE object_id = OBJECT_ID(@TABLE_NAME)
AND SUBSTRING(COLUMNS_UPDATED(), ((column_id - 1) / 8 + 1), 1) & (POWER(2, ((column_id - 1) % 8 + 1) - 1)) = POWER(2, (column_id - 1) % 8)
FOR XML PATH('')
), 1, 1, '');
WHILE LEN(@modifiedColumnsList) > 0
BEGIN
SET @Pos = CHARINDEX(',', @modifiedColumnsList);
IF @Pos = 0
BEGIN
SET @ColumnListItem = @modifiedColumnsList;
END;
ELSE
BEGIN
SET @ColumnListItem = SUBSTRING(@modifiedColumnsList, 1, @Pos - 1);
END;
SET @COLUMN_NAME = @ColumnListItem;
SET @ParmDefinitionD = N'@OldValueOut NVARCHAR(100) OUTPUT';
SET @ParmDefinitionI = N'@NewValueOut NVARCHAR(100) OUTPUT';
SET @sqlDeleted = N'SELECT @OldValueOut=' + @COLUMN_NAME + ' FROM #deleted where ' + @RecordPkName + '=' + CONVERT(VARCHAR(50), @RecordPk);
SET @sqlInserted = N'SELECT @NewValueOut=' + @COLUMN_NAME + ' FROM #Inserted where ' + @RecordPkName + '=' + CONVERT(VARCHAR(50), @RecordPk);
EXECUTE sp_executesql @sqlDeleted
,@ParmDefinitionD
,@OldValueOut = @OldValue OUTPUT;
EXECUTE sp_executesql @sqlInserted
,@ParmDefinitionI
,@NewValueOut = @NewValue OUTPUT;
--PRINT @newvalue
--PRINT @oldvalue
IF (LTRIM(RTRIM(@NewValue)) != LTRIM(RTRIM(@OldValue)))
BEGIN
SET @sql = 'INSERT INTO [dbo].[AuditDataChanges]
([TableName]
,[RecordPK]
,[ColumnName]
,[OldValue]
,[NewValue] )
VALUES
(' + QUOTENAME(@TABLE_NAME, '''') + '
,' + QUOTENAME(@RecordPk, '''') + '
,' + QUOTENAME(@COLUMN_NAME, '''') + '
,' + QUOTENAME(@OldValue, '''') + '
,' + QUOTENAME(@NewValue, '''') + '
' + ')';
EXEC (@sql);
END;
SET @COLUMN_NAME = '';
SET @NewValue = '';
SET @OldValue = '';
IF @Pos = 0
BEGIN
SET @modifiedColumnsList = '';
END;
ELSE
BEGIN
-- start substring at the character after the first comma
SET @modifiedColumnsList = SUBSTRING(@modifiedColumnsList, @Pos + 1, LEN(@modifiedColumnsList) - @Pos);
END;
END;
FETCH NEXT
FROM t_cursor
INTO @RecordPk
END
DROP TABLE #Inserted;
DROP TABLE #deleted;
CLOSE t_cursor;
DEALLOCATE t_cursor;