Каждая таблица, которую вы хотите отслеживать, будет нуждаться в собственном триггере. Совершенно очевидно, что, как указано в принятом ответе, генерация кода будет хорошей вещью.
Если вам нравится этот подход, возможно, вам стоит использовать этот триггер и заменить некоторые общие шаги сгенерированным кодом для каждой таблицы отдельно.
Тем не менее я создал полностью универсальный Audit-Trigger . Наблюдаемая таблица должна иметь PK , но этот PK может даже быть multi-column .
Некоторые типы столбцов (например, BLOB) могут не работать, но их можно легко исключить.
Это не будет лучшим по производительности: -D
Если честно: Это скорее упражнение ...
SET NOCOUNT ON;
GO
CREATE TABLE AuditTest(ID UNIQUEIDENTIFIER
,LogDate DATETIME
,TableSchema VARCHAR(250)
,TableName VARCHAR(250)
,AuditType VARCHAR(250),Content XML);
GO
- Некоторая таблица для проверки этого (специально использовали необычные столбцы PK ...)
CREATE TABLE dbo.Testx(ID1 DATETIME NOT NULL
,ID2 UNIQUEIDENTIFIER NOT NULL
,Test1 VARCHAR(100)
,Test2 DATETIME);
--Add a two column PK
ALTER TABLE dbo.Testx ADD CONSTRAINT PK_Test PRIMARY KEY(ID1,ID2);
- Некоторые данные испытаний
INSERT INTO dbo.Testx(ID1,ID2,Test1,Test2) VALUES
({d'2000-01-01'},NEWID(),'Test1',NULL)
,({d'2000-02-01'},NEWID(),'Test2',{d'2002-02-02'});
- это текущий контент
SELECT * FROM dbo.Testx;
GO
- триггер для аудита
CREATE TRIGGER [dbo].[UpdateTestTrigger]
ON [dbo].[Testx]
FOR UPDATE,INSERT,DELETE
AS
BEGIN
IF NOT EXISTS(SELECT 1 FROM deleted) AND NOT EXISTS(SELECT 1 FROM inserted) RETURN;
SET NOCOUNT ON;
DECLARE @tableSchema VARCHAR(250);
DECLARE @tableName VARCHAR(250);
DECLARE @AuditID UNIQUEIDENTIFIER=NEWID();
DECLARE @LogDate DATETIME=GETDATE();
SELECT @tableSchema = sch.name
,@tableName = tb.name
FROM sys.triggers AS tr
INNER JOIN sys.tables AS tb ON tr.parent_id=tb.object_id
INNER JOIN sys.schemas AS sch ON tb.schema_id=sch.schema_id
WHERE tr.object_id = @@PROCID
DECLARE @tp VARCHAR(10)=CASE WHEN EXISTS(SELECT 1 FROM deleted) AND EXISTS(SELECT 1 FROM inserted) THEN 'upd'
ELSE CASE WHEN EXISTS(SELECT 1 FROM deleted) AND NOT EXISTS(SELECT 1 FROM inserted) THEN 'del' ELSE 'ins' END END;
SELECT * INTO #tmpInserted FROM inserted;
SELECT * INTO #tmpDeleted FROM deleted;
SELECT kc.ORDINAL_POSITION, kc.COLUMN_NAME
INTO #tmpPKColumns
FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS AS tc
INNER JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE AS kc ON tc.TABLE_CATALOG=kc.TABLE_CATALOG
AND tc.TABLE_SCHEMA=kc.TABLE_SCHEMA
AND tc.TABLE_NAME=kc.TABLE_NAME
AND tc.CONSTRAINT_NAME=kc.CONSTRAINT_NAME
AND tc.CONSTRAINT_TYPE='PRIMARY KEY'
WHERE tc.TABLE_SCHEMA=@tableSchema
AND tc.TABLE_NAME=@tableName
ORDER BY kc.ORDINAL_POSITION;
DECLARE @pkCols VARCHAR(MAX)=
STUFF
(
(
SELECT 'UNION ALL SELECT ''' + pc.COLUMN_NAME + ''' AS [@name] , CAST(COALESCE(i.' + QUOTENAME(pc.COLUMN_NAME) + ',d.' + QUOTENAME(pc.COLUMN_NAME) + ') AS VARCHAR(MAX)) AS [@value] '
FROM #tmpPKColumns AS pc
ORDER BY pc.ORDINAL_POSITION
FOR XML PATH('')
),1,16,'');
DECLARE @pkColsCompare VARCHAR(MAX)=
STUFF
(
(
SELECT 'AND i.' + QUOTENAME(pc.COLUMN_NAME) + '=d.' + QUOTENAME(pc.COLUMN_NAME)
FROM #tmpPKColumns AS pc
ORDER BY pc.ORDINAL_POSITION
FOR XML PATH('')
),1,3,'');
DECLARE @cols VARCHAR(MAX)=
STUFF
(
(
SELECT ',' + CASE WHEN @tp='upd' THEN
'CASE WHEN (i.[' + COLUMN_NAME + ']!=d.[' + COLUMN_NAME + '] ' +
'OR (i.[' + COLUMN_NAME + '] IS NULL AND d.[' + COLUMN_NAME + '] IS NOT NULL) ' +
'OR (i.['+ COLUMN_NAME + '] IS NOT NULL AND d.[' + COLUMN_NAME + '] IS NULL)) ' +
'THEN ' ELSE '' END +
'(SELECT ''' + COLUMN_NAME + ''' AS [@name]' +
CASE WHEN @tp IN ('upd','del') THEN ',ISNULL(CAST(d.[' + COLUMN_NAME + '] AS NVARCHAR(MAX)),N''##NULL##'') AS [@old]' ELSE '' END +
CASE WHEN @tp IN ('ins','upd') THEN ',ISNULL(CAST(i.[' + COLUMN_NAME + '] AS NVARCHAR(MAX)),N''##NULL##'') AS [@new] ' ELSE '' END +
' FOR XML PATH(''Column''),TYPE) ' + CASE WHEN @tp='upd' THEN 'END' ELSE '' END
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA=@tableSchema AND TABLE_NAME=@tableName
FOR XML PATH('')
),1,1,''
);
DECLARE @cmd VARCHAR(MAX)=
'SET LANGUAGE ENGLISH;
WITH ChangedColumns AS
(
SELECT A.PK' +
',A.PK.query(''data(/PK/Column/@value)'').value(''text()[1]'',''nvarchar(max)'') AS PKVals' +
',Col.*
FROM #tmpInserted AS i
FULL OUTER JOIN #tmpDeleted AS d ON ' + @pkColsCompare +
' CROSS APPLY
(
SELECT ' + @cols + '
FOR XML PATH(''''),TYPE
) AS Col([Column])
CROSS APPLY(SELECT (SELECT tbl.* FROM (SELECT ' + @pkCols + ') AS tbl FOR XML PATH(''Column''), ROOT(''PK''),TYPE)) AS A(PK)
)
INSERT INTO AuditTest(ID,LogDate,TableSchema,TableName,AuditType,Content)
SELECT ''' + CAST(@AuditID AS VARCHAR(MAX)) + ''',''' + CONVERT(VARCHAR(MAX),@LogDate,126) + ''',''' + @tableSchema + ''',''' + @tableName + ''',''' + @tp + '''
,(
SELECT ''' + @tableSchema + ''' AS [@TableSchema]
,''' + @tableName + ''' AS [@TableName]
,''' + @tp + ''' AS [@ActionType]
,(
SELECT ChangedColumns.PK AS [*]
,(
SELECT x.[Column] AS [*],''''
FROM ChangedColumns AS x
WHERE x.PKVals=ChangedColumns.PKVals
FOR XML PATH(''Values''),TYPE
)
FROM ChangedColumns
FOR XML PATH(''Row''),TYPE
)
FOR XML PATH(''Changes'')
);';
EXEC (@cmd);
DROP TABLE #tmpInserted;
DROP TABLE #tmpDeleted;
END
GO
- Теперь давайте проверим это с некоторыми операциями:
UPDATE dbo.Testx SET Test1='New 1' WHERE ID1={d'2000-01-01'};
UPDATE dbo.Testx SET Test1='New 1',Test2={d'2000-01-01'} ;
DELETE FROM dbo.Testx WHERE ID1={d'2000-02-01'};
DELETE FROM dbo.Testx WHERE ID1=GETDATE(); --no affect
INSERT INTO dbo.Testx(ID1,ID2,Test1,Test2) VALUES
({d'2000-03-01'},NEWID(),'Test3',{d'2001-03-03'})
,({d'2000-04-01'},NEWID(),'Test4',{d'2001-04-04'})
,({d'2000-05-01'},NEWID(),'Test5',{d'2001-05-05'});
UPDATE dbo.Testx SET Test2=NULL; --all rows
DELETE FROM dbo.Testx WHERE ID1 IN ({d'2000-02-01'},{d'2000-03-01'});
GO
- Проверить окончательный статус
SELECT * FROM dbo.Testx;
SELECT * FROM AuditTest;
GO
- Очистить ( осторожно с реальными данными! )
DROP TABLE dbo.Testx;
GO
DROP TABLE dbo.AuditTest;
GO
Результат вставки
<Changes TableSchema="dbo" TableName="Testx" ActionType="ins">
<Row>
<PK>
<Column name="ID1" value="May 1 2000 12:00AM" />
<Column name="ID2" value="C2EB4D11-63F8-434E-8470-FB4A422A4ED1" />
</PK>
<Values>
<Column name="ID1" new="May 1 2000 12:00AM" />
<Column name="ID2" new="C2EB4D11-63F8-434E-8470-FB4A422A4ED1" />
<Column name="Test1" new="Test5" />
<Column name="Test2" new="May 5 2001 12:00AM" />
</Values>
</Row>
<Row>
<PK>
<Column name="ID1" value="Apr 1 2000 12:00AM" />
<Column name="ID2" value="28625CE7-9424-4FA6-AEDA-1E4853451655" />
</PK>
<Values>
<Column name="ID1" new="Apr 1 2000 12:00AM" />
<Column name="ID2" new="28625CE7-9424-4FA6-AEDA-1E4853451655" />
<Column name="Test1" new="Test4" />
<Column name="Test2" new="Apr 4 2001 12:00AM" />
</Values>
</Row>
<Row>
<PK>
<Column name="ID1" value="Mar 1 2000 12:00AM" />
<Column name="ID2" value="7AB56E6C-2ADC-4945-9D94-15BC9B3F270C" />
</PK>
<Values>
<Column name="ID1" new="Mar 1 2000 12:00AM" />
<Column name="ID2" new="7AB56E6C-2ADC-4945-9D94-15BC9B3F270C" />
<Column name="Test1" new="Test3" />
<Column name="Test2" new="Mar 3 2001 12:00AM" />
</Values>
</Row>
</Changes>
выборочный результат обновления
<Changes TableSchema="dbo" TableName="Testx" ActionType="upd">
<Row>
<PK>
<Column name="ID1" value="Feb 1 2000 12:00AM" />
<Column name="ID2" value="D7AB263A-EEFC-47DB-A6BB-A559FE8F2119" />
</PK>
<Values>
<Column name="Test1" old="Test2" new="New 1" />
<Column name="Test2" old="Feb 2 2002 12:00AM" new="Jan 1 2000 12:00AM" />
</Values>
</Row>
<Row>
<PK>
<Column name="ID1" value="Jan 1 2000 12:00AM" />
<Column name="ID2" value="318C0A66-8833-4F03-BCEF-7AB78C91704F" />
</PK>
<Values>
<Column name="Test2" old="##NULL##" new="Jan 1 2000 12:00AM" />
</Values>
</Row>
</Changes>
И результат удаления
<Changes TableSchema="dbo" TableName="Testx" ActionType="del">
<Row>
<PK>
<Column name="ID1" value="Mar 1 2000 12:00AM" />
<Column name="ID2" value="7AB56E6C-2ADC-4945-9D94-15BC9B3F270C" />
</PK>
<Values>
<Column name="ID1" old="Mar 1 2000 12:00AM" />
<Column name="ID2" old="7AB56E6C-2ADC-4945-9D94-15BC9B3F270C" />
<Column name="Test1" old="Test3" />
<Column name="Test2" old="##NULL##" />
</Values>
</Row>
</Changes>