在我的应用程序的SQL Server后端中,我想为一堆关键表创建历史表,这些表将跟踪行的更改历史记录.
我的整个应用程序使用存储过程,没有嵌入式SQL.与数据库修改这些表的唯一连接是通过应用程序和SP接口.传统上,我曾与之合作的商店使用触发器执行此任务.
如果我可以在存储过程和触发器之间进行选择,哪个更好?哪个更快?
触发.
我们编写了一个GUI(内部称为Red Matrix Reloaded),以便轻松创建/管理审计日志记录触发器.
这是使用的东西的一些DDL:
CREATE TABLE [AuditLog] ( [AuditLogID] [int] IDENTITY (1, 1) NOT NULL , [ChangeDate] [datetime] NOT NULL CONSTRAINT [DF_AuditLog_ChangeDate] DEFAULT (getdate()), [RowGUID] [uniqueidentifier] NOT NULL , [ChangeType] [varchar] (50) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL , [TableName] [varchar] (128) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL , [FieldName] [varchar] (128) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL , [OldValue] [varchar] (8000) COLLATE SQL_Latin1_General_CP1_CI_AS NULL , [NewValue] [varchar] (8000) COLLATE SQL_Latin1_General_CP1_CI_AS NULL , [Username] [varchar] (128) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL , [Hostname] [varchar] (50) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL , [AppName] [varchar] (128) COLLATE SQL_Latin1_General_CP1_CI_AS NULL , [UserGUID] [uniqueidentifier] NULL , [TagGUID] [uniqueidentifier] NULL , [Tag] [varchar] (8000) COLLATE SQL_Latin1_General_CP1_CI_AS NULL )
CREATE TRIGGER LogInsert_Nodes ON dbo.Nodes FOR INSERT AS /* Load the saved context info UserGUID */ DECLARE @SavedUserGUID uniqueidentifier SELECT @SavedUserGUID = CAST(context_info as uniqueidentifier) FROM master.dbo.sysprocesses WHERE spid = @@SPID DECLARE @NullGUID uniqueidentifier SELECT @NullGUID = '{00000000-0000-0000-0000-000000000000}' IF @SavedUserGUID = @NullGUID BEGIN SET @SavedUserGUID = NULL END /*We dont' log individual field changes Old/New because the row is new. So we only have one record - INSERTED*/ INSERT INTO AuditLog( ChangeDate, RowGUID, ChangeType, Username, HostName, AppName, UserGUID, TableName, FieldName, TagGUID, Tag, OldValue, NewValue) SELECT getdate(), --ChangeDate i.NodeGUID, --RowGUID 'INSERTED', --ChangeType USER_NAME(), HOST_NAME(), APP_NAME(), @SavedUserGUID, --UserGUID 'Nodes', --TableName '', --FieldName i.ParentNodeGUID, --TagGUID i.Caption, --Tag null, --OldValue null --NewValue FROM Inserted i
CREATE TRIGGER LogUpdate_Nodes ON dbo.Nodes FOR UPDATE AS /* Load the saved context info UserGUID */ DECLARE @SavedUserGUID uniqueidentifier SELECT @SavedUserGUID = CAST(context_info as uniqueidentifier) FROM master.dbo.sysprocesses WHERE spid = @@SPID DECLARE @NullGUID uniqueidentifier SELECT @NullGUID = '{00000000-0000-0000-0000-000000000000}' IF @SavedUserGUID = @NullGUID BEGIN SET @SavedUserGUID = NULL END /* ParentNodeGUID uniqueidentifier */ IF UPDATE (ParentNodeGUID) BEGIN INSERT INTO AuditLog( ChangeDate, RowGUID, ChangeType, Username, HostName, AppName, UserGUID, TableName, FieldName, TagGUID, Tag, OldValue, NewValue) SELECT getdate(), --ChangeDate i.NodeGUID, --RowGUID 'UPDATED', --ChangeType USER_NAME(), HOST_NAME(), APP_NAME(), @SavedUserGUID, --UserGUID 'Nodes', --TableName 'ParentNodeGUID', --FieldName i.ParentNodeGUID, --TagGUID i.Caption, --Tag d.ParentNodeGUID, --OldValue i.ParentNodeGUID --NewValue FROM Inserted i INNER JOIN Deleted d ON i.NodeGUID = d.NodeGUID WHERE (d.ParentNodeGUID IS NULL AND i.ParentNodeGUID IS NOT NULL) OR (d.ParentNodeGUID IS NOT NULL AND i.ParentNodeGUID IS NULL) OR (d.ParentNodeGUID <> i.ParentNodeGUID) END /* Caption varchar(255) */ IF UPDATE (Caption) BEGIN INSERT INTO AuditLog( ChangeDate, RowGUID, ChangeType, Username, HostName, AppName, UserGUID, TableName, FieldName, TagGUID, Tag, OldValue, NewValue) SELECT getdate(), --ChangeDate i.NodeGUID, --RowGUID 'UPDATED', --ChangeType USER_NAME(), HOST_NAME(), APP_NAME(), @SavedUserGUID, --UserGUID 'Nodes', --TableName 'Caption', --FieldName i.ParentNodeGUID, --TagGUID i.Caption, --Tag d.Caption, --OldValue i.Caption --NewValue FROM Inserted i INNER JOIN Deleted d ON i.NodeGUID = d.NodeGUID WHERE (d.Caption IS NULL AND i.Caption IS NOT NULL) OR (d.Caption IS NOT NULL AND i.Caption IS NULL) OR (d.Caption <> i.Caption) END ... /* ImageGUID uniqueidentifier */ IF UPDATE (ImageGUID) BEGIN INSERT INTO AuditLog( ChangeDate, RowGUID, ChangeType, Username, HostName, AppName, UserGUID, TableName, FieldName, TagGUID, Tag, OldValue, NewValue) SELECT getdate(), --ChangeDate i.NodeGUID, --RowGUID 'UPDATED', --ChangeType USER_NAME(), HOST_NAME(), APP_NAME(), @SavedUserGUID, --UserGUID 'Nodes', --TableName 'ImageGUID', --FieldName i.ParentNodeGUID, --TagGUID i.Caption, --Tag (SELECT Caption FROM Nodes WHERE NodeGUID = d.ImageGUID), --OldValue (SELECT Caption FROM Nodes WHERE NodeGUID = i.ImageGUID) --New Value FROM Inserted i INNER JOIN Deleted d ON i.NodeGUID = d.NodeGUID WHERE (d.ImageGUID IS NULL AND i.ImageGUID IS NOT NULL) OR (d.ImageGUID IS NOT NULL AND i.ImageGUID IS NULL) OR (d.ImageGUID <> i.ImageGUID) END
CREATE TRIGGER LogDelete_Nodes ON dbo.Nodes FOR DELETE AS /* Load the saved context info UserGUID */ DECLARE @SavedUserGUID uniqueidentifier SELECT @SavedUserGUID = CAST(context_info as uniqueidentifier) FROM master.dbo.sysprocesses WHERE spid = @@SPID DECLARE @NullGUID uniqueidentifier SELECT @NullGUID = '{00000000-0000-0000-0000-000000000000}' IF @SavedUserGUID = @NullGUID BEGIN SET @SavedUserGUID = NULL END /*We dont' log individual field changes Old/New because the row is new. So we only have one record - DELETED*/ INSERT INTO AuditLog( ChangeDate, RowGUID, ChangeType, Username, HostName, AppName, UserGUID, TableName, FieldName, TagGUID, Tag, OldValue,NewValue) SELECT getdate(), --ChangeDate d.NodeGUID, --RowGUID 'DELETED', --ChangeType USER_NAME(), HOST_NAME(), APP_NAME(), @SavedUserGUID, --UserGUID 'Nodes', --TableName '', --FieldName d.ParentNodeGUID, --TagGUID d.Caption, --Tag null, --OldValue null --NewValue FROM Deleted d
并且为了知道软件中的哪个用户进行了更新,每个连接通过调用存储过程"将自己登录到SQL Server":
CREATE PROCEDURE dbo.SaveContextUserGUID @UserGUID uniqueidentifier AS /* Saves the given UserGUID as the session's "Context Information" */ IF @UserGUID IS NULL BEGIN PRINT 'Emptying CONTEXT_INFO because of null @UserGUID' DECLARE @BinVar varbinary(128) SET @BinVar = CAST( REPLICATE( 0x00, 128 ) AS varbinary(128) ) SET CONTEXT_INFO @BinVar RETURN 0 END DECLARE @UserGUIDBinary binary(16) --a guid is 16 bytes SELECT @UserGUIDBinary = CAST(@UserGUID as binary(16)) SET CONTEXT_INFO @UserGUIDBinary /* To load the guid back DECLARE @SavedUserGUID uniqueidentifier SELECT @SavedUserGUID = CAST(context_info as uniqueidentifier) FROM master.dbo.sysprocesses WHERE spid = @@SPID select @SavedUserGUID AS UserGUID */
笔记
Stackoverflow代码格式删除大多数空行 - 因此格式很糟糕
我们使用的是用户表,而不是集成安全性
此代码是为了方便而提供的 - 不允许对我们的设计选择进行批评.纯粹主义者可能会坚持所有的日志记录代码都应该在业务层完成 - 他们可以来这里为我们编写/维护它.
使用SQL Server中的触发器无法记录blob(没有"之前"版本的blob - 只有什么是).Text和nText是blob - 它使得笔记不可记录,或者使它们成为varchar(2000)的.
标签列用作标识行的任意文本(例如,如果客户被删除,标签将在审核日志表中显示"通用汽车北美".
TagGUID用于指向行的"父".例如,记录InvoiceLineItems指向InvoiceHeader.这样,搜索与特定发票相关的审核日志条目的任何人都将在审核跟踪中通过行项目的TagGUID找到已删除的"行项目".
有时,"OldValue"和"NewValue"值被写为子选择 - 以获得有意义的字符串.即"
OldValue:{233d-ad34234 ..} NewValue:{883-sdf34 ...}
在审计跟踪中没有什么用处:
OldValue: Daimler Chrysler NewValue: Cerberus Capital Management
最后说明:随意不做我们做的事情.这对我们来说很好,但其他人都可以不使用它.
在SQL Server 2008中,MSDN上的一个名为CDC(更改数据捕获)CDC的新功能可以提供帮助.CDC能够将表数据的更改记录到另一个表中,而无需编写触发器或其他一些机制.更改数据捕获将插入,更新和删除等更改记录到SQL Server中的表中,从而使更改的详细信息可用于关联格式.
Channel9视频
我们有一个第三方工具ApexSQL Audit,我们用它来生成触发器.
以下是触发器在后台的外观以及数据的存储方式.希望人们会发现这对于逆向工程过程非常有用.它与Ian Boyd在他的例子中显示的有点不同,因为它允许每个列分别进行审计.
表1 - 保存交易详细信息(人员,时间,应用程序,主机名等)
CREATE TABLE [dbo].[AUDIT_LOG_TRANSACTIONS]( [AUDIT_LOG_TRANSACTION_ID] [int] IDENTITY(1,1) NOT NULL, [DATABASE] [nvarchar](128) NOT NULL, [TABLE_NAME] [nvarchar](261) NOT NULL, [TABLE_SCHEMA] [nvarchar](261) NOT NULL, [AUDIT_ACTION_ID] [tinyint] NOT NULL, [HOST_NAME] [varchar](128) NOT NULL, [APP_NAME] [varchar](128) NOT NULL, [MODIFIED_BY] [varchar](128) NOT NULL, [MODIFIED_DATE] [datetime] NOT NULL, [AFFECTED_ROWS] [int] NOT NULL, [SYSOBJ_ID] AS (object_id([TABLE_NAME])), PRIMARY KEY CLUSTERED ( [AUDIT_LOG_TRANSACTION_ID] ASC ) )
表2 - 保持值之前/之后.
CREATE TABLE [dbo].[AUDIT_LOG_DATA]( [AUDIT_LOG_DATA_ID] [int] IDENTITY(1,1) NOT NULL, [AUDIT_LOG_TRANSACTION_ID] [int] NOT NULL, [PRIMARY_KEY_DATA] [nvarchar](1500) NOT NULL, [COL_NAME] [nvarchar](128) NOT NULL, [OLD_VALUE_LONG] [ntext] NULL, [NEW_VALUE_LONG] [ntext] NULL, [NEW_VALUE_BLOB] [image] NULL, [NEW_VALUE] AS (isnull(CONVERT([varchar](8000), [NEW_VALUE_LONG],0),CONVERT([varchar](8000),CONVERT([varbinary](8000),substring([NEW_VALUE_BLOB],(1),(8000)),0),0))), [OLD_VALUE] AS (CONVERT([varchar](8000),[OLD_VALUE_LONG],0)), [PRIMARY_KEY] AS ([PRIMARY_KEY_DATA]), [DATA_TYPE] [char](1) NOT NULL, [KEY1] [nvarchar](500) NULL, [KEY2] [nvarchar](500) NULL, [KEY3] [nvarchar](500) NULL, [KEY4] [nvarchar](500) NULL, PRIMARY KEY CLUSTERED ( [AUDIT_LOG_DATA_ID] ASC ) )
插入触发器
我没有显示更新触发器,因为它们很长并且具有与此相同的逻辑.
CREATE TRIGGER [dbo].[tr_i_AUDIT_Audited_Table] ON [dbo].[Audited_Table] FOR INSERT NOT FOR REPLICATION As BEGIN DECLARE @IDENTITY_SAVE varchar(50), @AUDIT_LOG_TRANSACTION_ID Int, @PRIM_KEY nvarchar(4000), @ROWS_COUNT int SET NOCOUNT ON Select @ROWS_COUNT=count(*) from inserted Set @IDENTITY_SAVE = CAST(IsNull(@@IDENTITY,1) AS varchar(50)) INSERT INTO dbo.AUDIT_LOG_TRANSACTIONS ( TABLE_NAME, TABLE_SCHEMA, AUDIT_ACTION_ID, HOST_NAME, APP_NAME, MODIFIED_BY, MODIFIED_DATE, AFFECTED_ROWS, [DATABASE] ) values( 'Audited_Table', 'dbo', 2, -- ACTION ID For INSERT CASE WHEN LEN(HOST_NAME()) < 1 THEN ' ' ELSE HOST_NAME() END, CASE WHEN LEN(APP_NAME()) < 1 THEN ' ' ELSE APP_NAME() END, SUSER_SNAME(), GETDATE(), @ROWS_COUNT, 'Database_Name' ) Set @AUDIT_LOG_TRANSACTION_ID = SCOPE_IDENTITY() --This INSERT INTO code is repeated for each columns that is audited. --Below are examples for only two columns INSERT INTO dbo.AUDIT_LOG_DATA ( AUDIT_LOG_TRANSACTION_ID, PRIMARY_KEY_DATA, COL_NAME, NEW_VALUE_LONG, DATA_TYPE , KEY1 ) SELECT @AUDIT_LOG_TRANSACTION_ID, convert(nvarchar(1500), IsNull('[PK_Column]='+CONVERT(nvarchar(4000), NEW.[PK_Column], 0), '[PK_Column] Is Null')), 'Column1', CONVERT(nvarchar(4000), NEW.[Column1], 0), 'A' , CONVERT(nvarchar(500), CONVERT(nvarchar(4000), NEW.[PK_Column], 0)) FROM inserted NEW WHERE NEW.[Column1] Is Not Null --value is inserted for each column that is selected for auditin INSERT INTO dbo.AUDIT_LOG_DATA ( AUDIT_LOG_TRANSACTION_ID, PRIMARY_KEY_DATA, COL_NAME, NEW_VALUE_LONG, DATA_TYPE , KEY1 ) SELECT @AUDIT_LOG_TRANSACTION_ID, convert(nvarchar(1500), IsNull('[PK_Column]='+CONVERT(nvarchar(4000), NEW.[PK_Column], 0), '[PK_Column] Is Null')), 'Column2', CONVERT(nvarchar(4000), NEW.[Column2], 0), 'A' , CONVERT(nvarchar(500), CONVERT(nvarchar(4000), NEW.[PK_Column], 0)) FROM inserted NEW WHERE NEW.[Column2] Is Not Null End
免责声明:我不是以任何方式与Apex有关联,但我在我目前的工作中使用他们的工具.