【问题标题】:SQL-Server Trigger on update for AuditSQL-Server 触发更新以进行审计
【发布时间】:2011-12-02 14:33:31
【问题描述】:

我找不到一种简单/通用的方法来将某些表上的列更改注册到审计表。

我尝试以这种方式在更新后使用触发器来做到这一点:

首先是审计表定义:

CREATE TABLE [Audit](
[Id] [int] IDENTITY(1,1) NOT NULL,
[Date] [datetime] NOT NULL default GETDATE(),
[IdTypeAudit] [int] NOT NULL, --2 for Modify
[UserName] [varchar](50) NULL,
[TableName] [varchar](50) NOT NULL,
[ColumnName] [varchar](50) NULL,
[OldData] [varchar](50) NULL,
[NewData] [varchar](50) NULL )

接下来在任何表中的 AFTER UPDATE 上触发:

DECLARE 
    @sql varchar(8000),
    @col int,
    @colcount int

select @colcount = count(*) from INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = 'MyTable'
set @col = 1

while(@col < @colcount )
begin

    set @sql=
    'INSERT INTO Audit
    SELECT 2, UserNameLastModif, ''MyTable'', COL_NAME(Object_id(''MyTable''), '+ convert(varchar,@col) +'), Deleted.' 
    + COL_NAME(Object_id('MyTable'), @col) + ', Inserted.' + COL_NAME(Object_id('MyTable'), @col) + '
    FROM Inserted LEFT JOIN Deleted ON Inserted.[MyTableId] = Deleted.[MyTableId]
    WHERE COALESCE(Deleted.' + COL_NAME(Object_id('MyTable'), @col) + ', '''') <> COALESCE(Inserted.' + COL_NAME(Object_id('MyTable'), @col) + ', '''')'

    --UserNameLastModif is an optional column on MyTable
    exec(@sql)
    set @col = @col + 1

end

问题

  1. 当我使用 exec 函数时,Inserted 和 Deleted 丢失了上下文
  2. 似乎 colnumber 它并不总是一个相关数字,似乎如果您创建一个包含 20 列的表,然后删除一个并创建另一个,最后一个有一个数字 > @colcount

我一直在为整个网络寻找解决方案,但我无法弄清楚

有什么想法吗?

谢谢!

【问题讨论】:

    标签: sql-server triggers audit-tables


    【解决方案1】:

    这凸显了结构选择的一个更大问题。尝试编写基于集合的解决方案。删除循环和动态 SQL 并编写插入审计行的单个语句。这是可能的,但为了更容易考虑不同的表格布局,例如将所有列保持在 1 行而不是拆分它们。

    在 SQL 2000 中使用系统列。在 SQL 2005+ 中使用 sys.columns。即

    SELECT column_id FROM sys.columns WHERE object_id = OBJECT_ID(DB_NAME()+'.dbo.Table'); 
    

    【讨论】:

    • 我理解您所说的解决方案,但这意味着我需要为要审核的每个表创建一个审核表,因此您在跨数据库搜索更改时会变得更加复杂。感谢您的帮助!也许我会推荐你​​。
    • @Sanitago,您确实需要为每个表创建一个审计表,否则您将遇到锁定问题。
    • @HLGEM :非常好。我最近编写的一个触发器使用服务代理(不是用于审计,但它是一种可能的解决方案)。它会异步排队,直到锁被释放。尽管如果您要付出那么多努力,为什么不使用单独的表格。他们也会更快地查询。
    【解决方案2】:

    @Santiago : 如果你还想用动态 SQL 来写,你应该先准备好所有的语句,然后再执行它们。 8000 个字符可能不足以满足所有语句。一个好的解决方案是使用表格来存储它们。

    IF NOT OBJECT_ID('tempdb..#stmt') IS NULL
        DROP TABLE #stmt; 
    CREATE TABLE #stmt (ID int NOT NULL IDENTITY(1,1), SQL varchar(8000) NOT NULL); 
    

    然后将exec(@sql)这一行替换为INSERT INTO #stmt (SQL) VALUES (@sql);

    然后执行每一行。

    WHILE EXISTS (SELECT TOP 1 * FROM #stmt)
    BEGIN
        BEGIN TRANSACTION; 
            EXEC (SELECT TOP 1 SQL FROM #stmt ORDER BY ID); 
            DELETE FROM #stmt WHERE ID = (SELECT MIN(ID) FROM #stmt); 
        COMMIT TRANSACTION; 
    END
    

    记住使用 sys.columns 进行列循环(我假设您使用 SQL 2005/2008)。

    SET @col = 0; 
    WHILE EXISTS (SELECT TOP 1 * FROM sys.columns WHERE object_id = OBJECT_ID('MyTable') AND column_id > @col) 
    BEGIN
        SELECT TOP 1 @col = column_id FROM sys.columns 
        WHERE object_id = OBJECT_ID('MyTable') AND column_id > @col ORDER BY column_id ASC; 
        SET @sql ....
        INSERT INTO #stmt ....
    END
    

    删除第 4 行 @colcount int 和前面的逗号。删除信息架构选择。

    【讨论】:

      【解决方案3】:

      永远不要使用任何类型的循环触发器。不要使用动态 SQl 或调用存储过程或发送电子邮件。所有这些事情在触发器中都是非常不合适的。

      如果你想使用动态 sql 使用它来创建脚本来创建触发器。并为您要审核的每个表创建一个审核表(实际上每个表都有两个),否则您将由于锁定“一个表来统治所有表”而出现性能问题。

      【讨论】:

      • 同意你的观点,我既不想使用循环也不想使用动态 sql,但我的问题是我找不到以非阻塞方式逐列保存更改的方法.. . 就像触发器可以做的那样。
      猜你喜欢
      • 2020-11-07
      • 2017-10-08
      • 1970-01-01
      • 2021-08-04
      • 1970-01-01
      • 2016-09-18
      • 1970-01-01
      • 2020-03-24
      • 2011-06-03
      相关资源
      最近更新 更多