将有关已删除记录的信息传递到“删除”触发器上


11

在设置审计跟踪时,我没有问题可以跟踪谁在更新或在表中插入记录,但是,跟踪谁删除记录似乎更成问题。

我可以通过在“插入/更新”字段中包含“ UpdatedBy”字段来跟踪插入/更新。这使INSERT / UPDATE触发器可以通过来访问字段“ UpdatedBy” inserted.UpdatedBy。但是,使用Delete触发器不会插入/更新数据。有没有一种方法可以将信息传递到Delete触发器上,以便它可以知道谁删除了记录?

这是一个插入/更新触发器

ALTER TRIGGER [dbo].[trg_MyTable_InsertUpdate] 
ON [dbo].[MyTable]
FOR INSERT, UPDATE
AS  

INSERT INTO AuditTable (IdOfRecordedAffected, UserWhoMadeChanges) 
VALUES (inserted.ID, inserted.LastUpdatedBy)
FROM inserted 

使用SQL Server 2012


1
看到这个答案。SUSER_SNAME()是获取删除记录的人的关键。
Kin Shah

1
谢谢Kin,但是,我认为SUSER_SNAME()在像Web应用程序这样的情况下,如果单个用户可能用于整个应用程序的数据库通信,那将是行不通的。
webworm

1
您没有提到您正在调用网络应用程序。
Kin Shah 2014年

抱歉,我应该更具体地介绍应用程序类型。
webworm

Answers:


10

有没有一种方法可以将信息传递到Delete触发器上,以便它可以知道谁删除了记录?

是的:通过使用一个非常酷(且使用不足的功能)的CONTEXT_INFO。本质上,会话内存存在于所有作用域中,不受事务约束。它可用于传递信息(任何信息-好吧,适合有限空间的任何信息)以触发,以及在子proc / EXEC调用之间来回传递。我之前曾在完全相同的情况下使用过它。

测试以下内容以查看其工作方式。请注意,我转换成CHAR(128)CONVERT(VARBINARY(128), ..。这是力空白填充,使其更容易转换回VARCHAR得到它的时候CONTEXT_INFO(),因为VARBINARY(128)是右侧填充0x00秒。

SELECT CONTEXT_INFO();
-- Initially = NULL

DECLARE @EncodedUser VARBINARY(128);
SET @EncodedUser = CONVERT(VARBINARY(128),
                            CONVERT(CHAR(128), 'I deleted ALL your records! HA HA!')
                          );
SET CONTEXT_INFO @EncodedUser;

SELECT CONTEXT_INFO() AS [RawContextInfo],
       RTRIM(CONVERT(VARCHAR(128), CONTEXT_INFO())) AS [DecodedUser];

结果:

0x492064656C6574656420414C4C20796F7572207265636F7264732120484120484121202020202020...
I deleted ALL your records! HA HA!

全部放在一起:

  1. 应用程序应调用“删除”存储过程,该过程将传入正在删除记录的UserName(或其他名称)。我认为这已经是使用的模型,因为听起来您已经在跟踪插入和更新操作。

  2. “删除”存储过程可以:

    DECLARE @EncodedUser VARBINARY(128);
    SET @EncodedUser = CONVERT(VARBINARY(128),
                                CONVERT(CHAR(128), @UserName)
                              );
    SET CONTEXT_INFO @EncodedUser;
    
    -- DELETE STUFF HERE
  3. 审核触发器执行以下操作:

    -- Set the INT value in LEFT (currently 50) to the max size of [UserWhoMadeChanges]
    INSERT INTO AuditTable (IdOfRecordedAffected, UserWhoMadeChanges) 
       SELECT del.ID, COALESCE(
                         LEFT(RTRIM(CONVERT(VARCHAR(128), CONTEXT_INFO())), 50),
                         '<unknown>')
       FROM DELETED del;
  4. 请注意,正如@SeanGallardy在评论中指出的那样,由于其他过程和/或临时查询会从该表中删除记录,因此有可能:

    • CONTEXT_INFO尚未设置,仍然为NULL

      因此,我已经更新了上面的内容INSERT INTO AuditTable以使用a COALESCE作为默认值。或者,如果您不希望使用默认值并且需要一个名称,则可以执行以下操作:

      DECLARE @UserName VARCHAR(50); -- set to the size of AuditTable.[UserWhoMadeChanges]
      SET @UserName = LEFT(RTRIM(CONVERT(VARCHAR(128), CONTEXT_INFO())), 50);
      
      IF (@UserName IS NULL)
      BEGIN
         ROLLBACK TRAN; -- cancel the DELETE operation
         RAISERROR('Please set UserName via "SET CONTEXT_INFO.." and try again.', 16 ,1);
      END;
      
      -- use @UserName in the INSERT...SELECT
    • CONTEXT_INFO设置的值不是有效的用户名,因此可能会超出AuditTable.[UserWhoMadeChanges]字段的大小:

      因此,我添加了一个LEFT函数,以确保从中抓取的所有内容CONTEXT_INFO都不会破坏INSERT。如代码中所述,您只需将设置50UserWhoMadeChanges字段的实际大小。


SQL SERVER 2016及更高版本的更新

SQL Server 2016添加了此每个会话内存的改进版本:会话上下文。新的会话上下文实质上是键-值对的哈希表,其中“键”的类型为sysname(即NVARCHAR(128)),“值”的类型为SQL_VARIANT。含义:

  1. 现在存在价值分离,因此不太可能与其他用途冲突
  2. 您可以存储各种类型,不再需要担心通过取回值时的奇怪行为CONTEXT_INFO()(有关详细信息,请参阅我的文章:为什么CONTEXT_INFO()不返回SET CONTEXT_INFO设置的确切值?
  3. 你得到了很多更多的空间:8000字节每“价值”最高,可达所有按键256KB总(与128的最大字节CONTEXT_INFO

有关详细信息,请参见以下文档页面:


这种方法的问题在于它非常易变。任何会话都可以设置它,因此它可以覆盖任何先前设置的项目。是否想破坏您的应用程序?有一个开发人员覆盖您的期望。我强烈建议不要使用此方法,而应采用可能需要更改体系结构的标准方法。否则,您会玩火。
肖恩·加拉迪

@SeanGallardy您能否提供这种情况的实际示例?会话== @@SPID。这是PER会话/连接存储器。一个会话无法覆盖另一个会话的上下文信息。当会话注销时,该值消失。没有所谓的“先前设置的项目”。
所罗门·鲁兹基

1
我没有说“另一个会话的”,而是说会话范围内的任何对象都可以做到这一点。因此,一个开发人员编写了一个存储程序来保存自己的“上下文”信息,现在您的信息已被覆盖。我不得不处理一个使用相同模式的应用程序,我已经看到了它的发生……这是人力资源软件。让我告诉您,由于其中一位开发人员编写新的SP错误地将会话的上下文信息从“应有的”状态更新为错误,而由于“错误”而无法按时给人们带来多大的快乐。仅举一个例子,我实际上已经见证了为什么不使用这种方法。
肖恩·加拉迪

@SeanGallardy好的,感谢您阐明这一点。但这仍然只是部分有效的观点。为了使这种情况发生,必须在该内部调用该“其他”过程。或者,如果您要谈论的是可能要从该表中删除并启动触发器的其他过程,则可以对其进行测试。这是一个竞争条件,需要解决(就像它们在所有多线程应用程序中一样),而不是不使用此技术的原因。因此,我将做一个较小的更新来做到这一点。感谢您提出这种可能性。
所罗门·鲁兹基

2
我是说,事后才想到安全是主要问题,但这不是解决问题的工具。不会破坏应用程序的备注结构或其他用途,请确保我没有问题。绝对是不使用它的原因。YMMV,但我绝不会对某些重要内容(例如安全性)使用如此易变且无结构的内容。总体上,使用任何类型的共享用户可写存储实现安全性都是一个糟糕的主意。正确的设计将在很大程度上消除对诸如此类的需求。
肖恩·加拉迪2014年

5

除非您要记录SQL Server用户ID而不是应用程序级别的ID,否则您将无法做到这一点。

您可以通过创建一个名为DeletedBy的列并根据需要进行设置来进行软删除,然后您的更新触发器就可以进行真正的删除(或存档记录,我通常会在可能和合法的情况下避免硬删除),以及更新您的审核记录。要以这种方式强制执行删除操作,请定义on delete引发错误的触发器。如果您不想在物理表中添加一列,则可以定义一个视图,该视图添加该列并定义instead of触发器以处理更新基表,但这可能会过大。


我明白你的意思了。我确实希望登录应用程序级别的用户。
webworm 2014年

大卫,实际上您可以将信息传递给触发器。请查看我的答案以获取详细信息:)。
所罗门·鲁兹基

这里的建议很好,我真的很喜欢这条路线。通过在与触发真正删除操作相同的步骤中捕获谁来杀死两只鸟。由于此列对于此表中的每个记录都将为NULL,因此似乎可以很好地使用SQL Server SPARSE列?
Airn5475

2

有没有一种方法可以将信息传递到Delete触发器上,以便它可以知道谁删除了记录?

是的,显然有两种方法;-)。如果对使用有任何保留,CONTEXT_INFO正如我在此处的其他答复中所建议的那样,我只是想到了另一种与其他代码/进程功能更清晰的分离方式:使用本地临时表。

临时表名称应包括要从中删除的表名称,因为这将有助于使其与可能在同一会话中运行的任何其他代码分开。类似于以下内容:
#<TableName>DeleteAudit

本地临时表的一个好处CONTEXT_INFO是,如果另一个proc中的某个人(通过某种方式从该特定“删除” proc进行调用)恰好不正确地使用了相同的临时表名,则子进程将a)创建一个新的本地所请求名称的临时表将与此初始临时表分开(即使它具有相同的名称),并且b)子流程中针对新本地临时表的任何DML语句都不会影响该临时表中的任何数据。在父进程中在此处创建的本地临时表,因此不会覆盖数据。当然,如果子进程针对该临时表名称发出了DML语句而没有先发出相同名称的CREATE TABLE,则这些DML语句影响此表中的数据。但是,在这一点上,我们真的这里的CONTEXT_INFO边缘案例比使用重叠使用的可能性更大(是的,我知道它已经发生了,这就是为什么我说“边缘案例”而不是“永远不会发生”)。

  1. 应用程序应调用“删除”存储过程,该过程将传入正在删除记录的UserName(或其他名称)。我认为这已经是使用的模型,因为听起来您已经在跟踪插入和更新操作。

  2. “删除”存储过程可以:

    CREATE TABLE #MyTableDeleteAudit (UserName VARCHAR(50));
    INSERT INTO #MyTableDeleteAudit (UserName) VALUES (@UserName);
    
    -- DELETE STUFF HERE
  3. 审核触发器执行以下操作:

    -- Set the datatype and length to be the same as the [UserWhoMadeChanges] field
    DECLARE @UserName VARCHAR(50);
    IF (OBJECT_ID(N'tempdb..#TriggerTestDeleteAudit') IS NOT NULL)
    BEGIN
       SELECT @UserName = UserName
       FROM #TriggerTestDeleteAudit;
    END;
    
    -- catch the following conditions: missing table, no rows in table, or empty row
    IF (@UserName IS NULL OR @UserName NOT LIKE '%[a-z]%')
    BEGIN
      /* -- uncomment if undefined UserName == badness
       ROLLBACK TRAN; -- cancel the DELETE operation
       RAISERROR('Please set UserName via #TriggerTestDeleteAudit and try again.', 16 ,1);
       RETURN; -- exit
      */
      /* -- uncomment if undefined UserName gets default value
       SET @UserName = '<unknown>';
      */
    END;
    
    INSERT INTO AuditTable (IdOfRecordedAffected, UserWhoMadeChanges) 
       SELECT del.ID, @UserName
       FROM DELETED del;

    我已经在触发器中测试了此代码,它可以按预期工作。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.