如何在更新触发器中记录更改的值

Posted

技术标签:

【中文标题】如何在更新触发器中记录更改的值【英文标题】:How to log changed values in Update trigger 【发布时间】:2018-04-21 01:48:41 【问题描述】:

我想将表 Item 中的任何字段更改记录到名为 Events 的日志表中。

CREATE TABLE [dbo].[Items]
(
    [Id] [int] IDENTITY(1,1) NOT NULL,
    [Name] [nvarchar](100) NULL,
    [Description] [nvarchar](max) NULL,
    [ParentId] [int] NULL,
    [EntityStatusId] [int] NOT NULL,
    [ItemTypeId] [int] NOT NULL,
    [StartDate] [datetimeoffset](7) NULL,
    [DueDate] [datetimeoffset](7) NULL,
    [Budget] [decimal](18, 2) NULL,
    [Cost] [decimal](18, 2) NULL,
    [Progress] [int] NULL,
    [StatusTypeId] [int] NULL,
    [ImportanceTypeId] [int] NULL,
    [PriorityTypeId] [int] NULL,
    [CreatedDate] [datetimeoffset](7) NULL,
    [HideChildren] [bit] NOT NULL,
    [TenantId] [int] NOT NULL,
    [OwnedBy] [int] NOT NULL,
    [Details] [nvarchar](max) NULL,
    [Inserted] [datetimeoffset](0) NOT NULL,
    [Updated] [datetimeoffset](0) NOT NULL,
    [InsertedBy] [int] NULL,
    [UpdatedBy] [int] NULL,

)

对于每个更改的列,我想在此表中添加一行。此表将保存 Item 表的更改,但稍后它将保存其他表的更改。我希望触发器尽可能动态,因此相同的基本触发器也可以用于其他表。如果向表中添加/删除列,SP 应该发现这一点并且不会中断。

CREATE TABLE [dbo].[Events]
(
    [Id] [int] IDENTITY(1,1) NOT NULL,
    [RecordId] [int] NOT NULL, -- Item.Id
    [EventTypeId] [int] NOT NULL, -- Always 2
    [EventDate] [datetimeoffset](0) NOT NULL, --GetUTCDate()
    [ColumnName] [nvarchar](50) NULL, --The column name that changed
    [OriginalValue] [nvarchar](max) NULL, --The original Value
    [NewValue] [nvarchar](max) NULL, --The New Value
    [TenantId] [int] NOT NULL, --Item.TentantId
    [AppUserId] [int] NOT NULL, --Item.ModifiedBy
    [TableName] [int] NOT NULL --The Name of the Table (Item in this case, but later there will be others)

)

我正在尝试编写更新触发器,但发现它很困难。

我知道有保存新旧值的 Inserted 和 Deleted 表。

那么我该如何实现呢?它似乎应该是动态的,这样如果添加列,它就不会破坏任何东西。

如果我在 C# 中编写此代码,我将获取所有列名并遍历它们并找到更改的字段,然后为每个字段创建一个事件。但我不知道如何使用 SQL 来做到这一点。

响应答案的更新: 在 SSMS 中编辑时,此答案有效。然而,在实践中,该应用程序使用 EntityFramework 并且它似乎在做一些奇怪的事情,因为这是被记录的内容。请注意,在 Original/New 中实际上只有一列具有不同的值。因此,我试图在插入之前检查这些值是否真的不同。

+----+----------+-------------+----------------------------+------------------+----------------------------+----------------------------+----------+-----------+---------+-----------+
| Id | RecordId | EventTypeId |         EventDate          |    ColumnName    |       OriginalValue        |          NewValue          | TenantId | AppUserId | TableId | TableName |
+----+----------+-------------+----------------------------+------------------+----------------------------+----------------------------+----------+-----------+---------+-----------+
| 21 |      397 |           2 | 2018-04-22 15:42:16 +00:00 | Name             | Task 2                     | Task 2A                    |        8 |        11 | NULL    | Item      |
| 22 |      397 |           2 | 2018-04-22 15:42:16 +00:00 | Description      | NULL                       | NULL                       |        8 |        11 | NULL    | Item      |
| 23 |      397 |           2 | 2018-04-22 15:42:16 +00:00 | ParentId         | 238                        | 238                        |        8 |        11 | NULL    | Item      |
| 24 |      397 |           2 | 2018-04-22 15:42:16 +00:00 | EntityStatusId   | 1                          | 1                          |        8 |        11 | NULL    | Item      |
| 25 |      397 |           2 | 2018-04-22 15:42:16 +00:00 | ItemTypeId       | 3                          | 3                          |        8 |        11 | NULL    | Item      |
| 26 |      397 |           2 | 2018-04-22 15:42:16 +00:00 | StartDate        | NULL                       | NULL                       |        8 |        11 | NULL    | Item      |
| 27 |      397 |           2 | 2018-04-22 15:42:16 +00:00 | DueDate          | NULL                       | NULL                       |        8 |        11 | NULL    | Item      |
| 28 |      397 |           2 | 2018-04-22 15:42:16 +00:00 | Budget           | NULL                       | NULL                       |        8 |        11 | NULL    | Item      |
| 29 |      397 |           2 | 2018-04-22 15:42:16 +00:00 | Cost             | NULL                       | NULL                       |        8 |        11 | NULL    | Item      |
| 30 |      397 |           2 | 2018-04-22 15:42:16 +00:00 | Progress         | NULL                       | NULL                       |        8 |        11 | NULL    | Item      |
| 31 |      397 |           2 | 2018-04-22 15:42:16 +00:00 | StatusTypeId     | 1                          | 1                          |        8 |        11 | NULL    | Item      |
| 32 |      397 |           2 | 2018-04-22 15:42:16 +00:00 | ImportanceTypeId | NULL                       | NULL                       |        8 |        11 | NULL    | Item      |
| 33 |      397 |           2 | 2018-04-22 15:42:16 +00:00 | PriorityTypeId   | NULL                       | NULL                       |        8 |        11 | NULL    | Item      |
| 34 |      397 |           2 | 2018-04-22 15:42:16 +00:00 | OwnedBy          | 11                         | 11                         |        8 |        11 | NULL    | Item      |
| 35 |      397 |           2 | 2018-04-22 15:42:16 +00:00 | Details          | <p><span></span></p>       | <p><span></span></p>       |        8 |        11 | NULL    | Item      |
| 36 |      397 |           2 | 2018-04-22 15:42:16 +00:00 | Inserted         | 0001-01-01 00:00:00 +00:00 | 0001-01-01 00:00:00 +00:00 |        8 |        11 | NULL    | Item      |
| 37 |      397 |           2 | 2018-04-22 15:42:16 +00:00 | Updated          | 0001-01-01 00:00:00 +00:00 | 0001-01-01 00:00:00 +00:00 |        8 |        11 | NULL    | Item      |
| 38 |      397 |           2 | 2018-04-22 15:42:16 +00:00 | InsertedBy       | 11                         | 11                         |        8 |        11 | NULL    | Item      |
| 39 |      397 |           2 | 2018-04-22 15:42:16 +00:00 | UpdatedBy        | 11                         | 11                         |        8 |        11 | NULL    | Item      |
+----+----------+-------------+----------------------------+------------------+----------------------------+----------------------------+----------+-----------+---------+-----------+

【问题讨论】:

***.com/questions/1254787/…的可能重复 阅读那个,它是关于创建 XML 的。 您应该在Events 表中添加一些解释。什么是 RecordId, EventTypeId 列?它们不在Items 表中。如果您只从一个表中记录,那么TableId 列的目的是什么。而如果几列同时更新,你会插入几行吗? 因此,您想为每个更新的行和每个更新的列在dbo.Events 表中添加一个新行?我将此假设基于其中包含 ColumnName 属性。如果这是正确的假设,那么您正在寻找的是编写即使您更改 dbo.Items 架构也能正常工作的代码?只是想弄清楚你到底在找什么,甚至可以在你上面的问题中添加我的问题的答案,这样当有人读到它时,它会立即阐明你的意图。 @MK_ 好点,我已经编辑了我上面的问题以澄清。 【参考方案1】:

这是使用COLUMNS_UPDATED 的一种方式。触发器不依赖于列名,因此您可以毫无问题地添加或删除列。我在查询中添加了一些 cmets

create trigger audit on Items
after update
as
begin
    set nocount on;
        create table #updatedCols (Id int identity(1, 1), updateCol nvarchar(200))

        --find all columns that were updated and write them to temp table
        insert into #updatedCols (updateCol)
        select
            column_name
        from
            information_schema.columns
        where   
            table_name = 'Items'   
            and convert(varbinary, reverse(columns_updated())) & power(convert(bigint, 2), ordinal_position - 1) > 0

        --temp tables are used because inserted and deleted tables are not available in dynamic SQL
        select * into #tempInserted from inserted
        select * into #tempDeleted from deleted

        declare @cnt int = 1
        declare @rowCnt int
        declare @columnName varchar(1000)
        declare @sql nvarchar(4000)

        select @rowCnt = count(*) from #updatedCols

        --execute insert statement for each updated column
        while @cnt <= @rowCnt
        begin
            select @columnName = updateCol from #updatedCols where id = @cnt

            set @sql = N'
                insert into [Events] ([RecordId], [EventTypeId], [EventDate], [ColumnName], [OriginalValue], [NewValue], [TenantId], [AppUserId], [TableName])
                select
                    i.Id, 2, GetUTCDate(), ''' + @columnName + ''', d.' + @columnName + ', i.' + @columnName +', i.TenantId, i.UpdatedBy, ''Item''
                from
                    #tempInserted i
                    join #tempDeleted d on i.Id = d.Id and isnull(Cast(i.' + @columnName + ' as varchar), '''') <> isnull(Cast(d.' +@columnName + ' as varchar), '''')
                '
            exec sp_executesql @sql
            set @cnt = @cnt + 1
        end
end

我已将Events 表的TableName 列的数据类型更改为nvarchar

【讨论】:

我发现的一个问题是它会记录所有内容,而不仅仅是更改的字段。 这是我能想到的最好的,但它从来没有记录任何东西set @sql = N' Select case when Exists( select 1 from #tempInserted i join #tempDeleted d on i.Id = d.Id where i.' + @columnName + '!= d.' + @columnName + ') then cast(1 as bit) else cast(0 as bit) end ' exec @isChanged = sp_executesql @sql if @isChanged = 1 我已经检查了触发器。一切都按预期工作,只记录更改的列。你能展示你的更新声明吗?记录了哪些列? 我重新测试了,你是正确的。它完美地工作。非常感谢。 很高兴它成功了。但不要忘记在投射过程中设置varchar 的长度。【参考方案2】:

您可以查询目录(sys.columnssys.tablessys.schemas 等)以将当前表的列放入游标中。然后迭代该游标并将您的单个插入作为字符串构建到日志表中。然后使用EXECUTE 或sp_executesql 或类似的方法执行它们。 (请注意,链接的文档不一定与您的 DBMS 版本匹配,仅作为第一个提示。)

顺便说一句,您可能希望将[TableName][ColumnName] 的数据类型更改为sysname,这也用于此类列的目录中。

【讨论】:

以上是关于如何在更新触发器中记录更改的值的主要内容,如果未能解决你的问题,请参考以下文章

Oracle - 触发器以在更新时创建历史记录行

在更新触发器之前或之后更改同一表中的值(oracle)

Sql Server 2005 - 插入更新触发器 - 获取更新,插入行

如何在sqlalchemy中的值更改后触发事件

触发孩子时如何更改父母的值?

mysql 日志记录触发器,查找更改的列