web-dev-qa-db-fra.com

Création de déclencheurs d'audit dans SQL Server

Je dois implémenter le suivi des modifications sur deux tables de ma base de données SQL Server 2005. Je dois auditer les ajouts, les suppressions, les mises à jour (avec des détails sur les mises à jour). J'avais prévu d'utiliser un déclencheur pour le faire, mais après avoir fouillé dans Google, j'ai trouvé qu'il était incroyablement facile de le faire de manière incorrecte et je voulais éviter cela dès le départ.

Quelqu'un peut-il publier un exemple de déclencheur de mise à jour qui accomplit cela avec succès et de manière élégante? J'espère me retrouver avec une table d'audit avec la structure suivante:

  • ID
  • LogDate
  • Nom de la table
  • TransactionType (update/insert/delete)
  • RecordID
  • Nom de domaine
  • OldValue
  • Nouvelle valeur

... mais je suis ouvert aux suggestions.

Merci!

28
Mike Cole

Je veux juste rappeler quelques points:

Utiliser des générateurs de code Vous ne pouvez pas avoir une seule procédure pour suivre toutes les tables, vous devrez générer des déclencheurs similaires mais distincts sur chaque table suivie. Ce type de travail convient le mieux à la génération automatisée de code. À votre place, je voudrais utiliser une transformation XSLT pour générer le code à partir de XML, et le XML peut être généré automatiquement à partir de métadonnées. Cela vous permet de gérer facilement les déclencheurs en les régénérant chaque fois que vous modifiez la logique/structure d'audit ou qu'une table cible est ajoutée/modifiée.

Prenez en compte planification de la capacité pour l'audit. Une table d'audit qui suit toutes les modifications de valeur sera, de loin, la plus grande table de la base de données: elle contiendra toutes les données actuelles et tout l'historique des données actuelles. Un tel tableau augmentera la taille de la base de données de 2 à 3 ordres de grandeur (x10, x100). Et la table d'audit deviendra rapidement le goulot d'étranglement de tout:

  • chaque opération DML nécessitera des verrous dans la table d'audit
  • toutes les opérations d’administration et de maintenance devront tenir compte de la taille de la base de données en raison de la vérification

Prendre en compte les modifications de schéma. Une table nommée 'Foo' peut être supprimée et plus tard une autre table nommée 'Foo' peut être créée. La piste d'audit doit pouvoir distinguer les deux objets différents. Mieux vaut utiliser une dimension changeante lente .

Considérez la nécessité de supprimer efficacement les enregistrements d'audit. Lorsque la période de rétention dictée par vos stratégies de sujet d'application est due, vous devez pouvoir supprimer les enregistrements d'audit dus. Cela peut ne pas sembler être un gros problème maintenant, mais 5 ans plus tard, lorsque les premiers enregistrements sont dus au fait que le tableau d’audit a atteint 9,5 To, cela peut poser problème.

Considérez la nécessité de interroger l'audit. La structure de la table d’audit doit être prête à répondre efficacement aux demandes d’audit. Si votre audit ne peut pas être interrogé, alors il n'a aucune valeur. Les requêtes seront entièrement dictées par vos exigences et vous seul les connaissez. Cependant, la plupart des enregistrements d’audit sont interrogés par intervalles table? ') ou par auteur (' quels changements ont fait Bob dans la base de données? '). 

35
Remus Rusanu

Nous utilisons ApexSQL Audit qui génère des déclencheurs d’audit. Vous trouverez ci-dessous les structures de données utilisées par cet outil. Si vous ne prévoyez pas acheter une solution tierce, vous pouvez installer cet outil en mode d'évaluation, voir comment ils ont implémenté les déclencheurs et le stockage, puis créer quelque chose de similaire pour vous-même.

Je n’ai pas cherché à entrer dans trop de détails sur le fonctionnement de ces tables, mais j'espère que cela vous aidera à démarrer.

enter image description here

19
Igor Voplov

Il n'y a pas de moyen générique de le faire comme vous le souhaitez. En fin de compte, vous finissez par écrire des tonnes de code pour chaque table. Sans compter que cela peut être lent si vous devez comparer chaque colonne pour le changement.

De plus, le fait que vous puissiez mettre à jour plusieurs lignes en même temps implique que vous deviez ouvrir un curseur pour parcourir tous les enregistrements.

La façon dont je le ferais consistera à utiliser un tableau dont la structure est identique à celle que vous suivez et à décomposer ultérieurement pour indiquer les colonnes réellement modifiées. Je garderais également une trace de la session qui a réellement apporté le changement. Cela suppose que vous ayez une clé primaire dans la table suivie.

Donc, étant donné une table comme celle-ci

CREATE TABLE TestTable  
(ID INT NOT NULL CONSTRAINT PK_TEST_TABLE PRIMARY KEY,
Name1 NVARCHAR(40) NOT NULL,  
Name2 NVARCHAR(40))

Je créerais une table d'audit comme celle-ci dans l'audit schmea.

CREATE TABLE Audit.TestTable  
(SessionID UNIQUEIDENTIFER NOT NULL,  
ID INT NOT NULL,
Name1  NVARCHAR(40) NOT NULL,  
Name2  NVARCHAR(40),  
Action NVARCHAR(10) NOT NULL CONSTRAINT CK_ACTION CHECK(Action In 'Deleted','Updated'),  
RowType NVARCHAR(10) NOT NULL CONSTRAINT CK_ROWTYPE CHECK (RowType in 'New','Old','Deleted'),  
ChangedDate DATETIME NOT NULL Default GETDATE(),  
ChangedBy SYSNHAME NOT NULL DEFAULT USER_NAME())

Et un déclencheur pour la mise à jour comme celle-ci

CREATE Trigger UpdateTestTable ON DBO.TestTable FOR UPDATE AS  
BEGIN  
    SET NOCOUNT ON
    DECLARE @SessionID UNIQUEIDENTIFER
    SET @SessionID = NEWID()
    INSERT Audit.TestTable(Id,Name1,Name2,Action,RowType,SessionID)
    SELECT ID,name1,Name2,'Updated','Old',@SessionID FROM Deleted

    INSERT Audit.TestTable(Id,Name1,Name2,Action,RowType,SessionID)
    SELECT ID,name1,Name2,'Updated','New',@SessionID FROM Inserted

END

Cela va assez vite. Lors de la création de rapports, il vous suffit de joindre les lignes en fonction de sessionID et de la clé primaire, puis de générer un rapport. Vous pouvez également avoir un travail par lots qui parcourt périodiquement toutes les tables de la table d'audit et prépare une paire nom-valeur indiquant les modifications.

HTH

13
no_one

Mike, nous utilisons l’outil www.auditdatabase.com. Cet outil gratuit génère des déclencheurs d’audit. Il fonctionne bien avec SQL Server 2008, 2005 et 2000. C’est un outil sophistiqué qui permet de personnaliser les déclencheurs d’audit pour une table.

Apex SQL Audit est un autre excellent outil.

1
Camejo

Le déclencheur est utilisé pour Si vous modifiez ou insérez dans une table particulière, ceci sera exécuté et vous pourrez vérifier la colonne particulière dans le déclencheur. Vous trouverez un exemple complet avec une explication sur le site Web suivant: ____ http://www.allinworld99.blogspot.com/2015/04/triggers-in-sql.html

0
Merbin Joe

Cela a l'air simple et devrait bien fonctionner jusqu'à ce que vous ayez des éléments image/varbinary etc. dans vos tablesVous avez tout un ancien enregistrement et un nouvel enregistrement entier en tant que xml . Devrait également fonctionner correctement pour l'insertion de plusieurs colonnes à un seul lot.

CREATE TABLE _AuditTable
(Aud_Id int identity(1,1) primary key,
Aud_TableName varchar(100), 
Aud_ActionType char(1),
Aud_Username varchar(100),
Aud_OLDValues xml, 
Aud_NEWValues xml,
Aud_OperationDate datetime DEFAULT GETDATE()
)

Et le code de déclenchement

CREATE TRIGGER _test2_InsertUpdate on _test2
FOR INSERT, UPDATE
AS
BEGIN
SET NOCOUNT ON;
 IF NOT EXISTS(SELECT 1 FROM deleted) AND NOT EXISTS(SELECT 1 FROM inserted) 
    RETURN;

declare @tablename varchar(100)
SELECT @tablename = OBJECT_NAME(parent_object_id) 
             FROM sys.objects 
             WHERE sys.objects.name = OBJECT_NAME(@@PROCID)

/*Action*/
DECLARE @ActionType char(1)
IF EXISTS (SELECT * FROM inserted)
       IF EXISTS (SELECT * FROM deleted)
               SELECT @ActionType = 'U'
       ELSE
               SELECT @ActionType = 'I'
ELSE
       SELECT @ActionType = 'D'

declare @inserted xml, @deleted xml 
SET @inserted = (SELECT * FROM inserted FOR XML PATH)
SET @deleted = (SELECT * FROM deleted FOR XML PATH)

             INSERT INTO _AuditTable(Aud_TableName, Aud_ActionType, Aud_Username, Aud_OLDValues, Aud_NEWValues)
             SELECT @tablename, @ActionType, SUSER_SNAME(), @deleted, @inserted
END

SORTIE

Aud_Id | Aud_TableName  | Aud_ActionType | Aud_Username | Aud_OLDValues | Aud_NEWValues |   Aud_OperationDate
1      |_test2          |   I            |abc\mR        |   NULL        |<row><name>abc</name></row> |  2018-11-07 12:38:34.937
0
mr R

Chaque table que l’on veut surveiller aura besoin de son propre déclencheur. Comme il est indiqué dans la réponse acceptée, il est assez évident que la génération de code sera une bonne chose. 

Si vous aimez cette approche, vous pouvez utiliser ce déclencheur et remplacer certaines étapes génériques par du code généré pour chaque table séparément.

Néanmoins, j'ai créé un générique Audit-Trigger . La table observée doit avoir une PK , mais cette PK pourrait même être multi-colonne.

Certains types de colonnes (comme les BLOBs) peuvent ne pas fonctionner, mais vous pouvez facilement les exclure.

Ce ne sera pas la meilleure performance :-D

Pour être honnête: C'est plus une sorte d'exercice ...

SET NOCOUNT ON;
GO
CREATE TABLE AuditTest(ID UNIQUEIDENTIFIER
                      ,LogDate DATETIME
                      ,TableSchema VARCHAR(250)
                      ,TableName VARCHAR(250)
                      ,AuditType VARCHAR(250),Content XML);
GO

--Quelque tableau pour le tester (utilisé des colonnes excentriques de PK à dessein ...)

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);

--Quelques données de test

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'});

--C'est le contenu actuel

SELECT * FROM dbo.Testx;
GO

--Le déclencheur de l'audit

    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

--Maintenant, testons-le avec quelques opérations:

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

--Vérifier le statut final

SELECT * FROM dbo.Testx;
SELECT * FROM AuditTest;
GO

--Nettoyez ( avec des données réelles! )

DROP TABLE dbo.Testx;
GO
DROP TABLE dbo.AuditTest;
GO

Le résultat de l'insertion

<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>

Le sélectif résultat d'une mise à jour

<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>

Et le résultat d'une suppression

<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>
0
Shnugo

Je vais ajouter mon approche et mes suggestions au mélange.

J'ai un tableau très similaire à votre conception proposée que j'ai utilisé pour les sept dernières années sur une base de données SQL 2005 (maintenant 2008).

J'ai ajouté des déclencheurs d'insertion, de mise à jour et de suppression aux tables sélectionnées, puis vérifié les modifications apportées aux champs sélectionnés. À l'époque, c'était simple et ça fonctionne bien.

Voici les problèmes que je trouve avec cette approche:

  1. Les champs de valeurs anciennes/nouvelles de la table d'audit devaient être de type varchar (MAX) pour pouvoir traiter toutes les valeurs pouvant être auditées: int, bool, décimal, float, varchar, etc.

  2. Le code à vérifier pour chaque champ est fastidieux pour écrire une maintenance. Il est également facile de rater des choses (par exemple, changer un champ null en une valeur n'a pas été intercepté, car NULL! = Value est NULL.

  3. Supprimer l'enregistrement: comment enregistrez-vous cela? Tous les champs? Les sélectionnés? Ça se complique

Ma vision future consiste à utiliser du code SQL-CLR et à écrire un déclencheur générique qui est exécuté et vérifie les méta-données de table pour voir ce qu'il faut auditer. Deuxièmement, les valeurs New/Old seront converties en champs XML et l'objet entier sera enregistré: cela produira plus de données mais une suppression aura un enregistrement complet. Il existe plusieurs articles sur le Web concernant les déclencheurs d’audit XML.

0
Quango
CREATE TRIGGER TriggerName 
ON TableName 
FOR INSERT, UPDATE, DELETE AS 
BEGIN
 SET NOCOUNT ON

 DECLARE @ExecStr varchar(50), @Qry nvarchar(255)

 CREATE TABLE #inputbuffer 
 (
  EventType nvarchar(30), 
  Parameters int, 
  EventInfo nvarchar(255)
 )

 SET @ExecStr = 'DBCC INPUTBUFFER(' + STR(@@SPID) + ')'

 INSERT INTO #inputbuffer 
 EXEC (@ExecStr)

 SET @Qry = (SELECT EventInfo FROM #inputbuffer)

 SELECT @Qry AS 'Query that fired the trigger', 
 SYSTEM_USER as LoginName, 
 USER AS UserName, 
 CURRENT_TIMESTAMP AS CurrentTime
END
0
Rak

J'ai finalement trouvé une solution universelle, qui n'exige pas SQL dynamique et enregistre les modifications de toutes les colonnes.

Il n'est pas nécessaire de changer le déclencheur si la table change.

C'est le journal d'audit:

CREATE TABLE [dbo].[Audit](
    [ID] [bigint] IDENTITY(1,1) NOT NULL,
    [Type] [char](1) COLLATE Latin1_General_CI_AS NULL,
    [TableName] [nvarchar](128) COLLATE Latin1_General_CI_AS NULL,
    [PK] [int] NULL,
    [FieldName] [nvarchar](128) COLLATE Latin1_General_CI_AS NULL,
    [OldValue] [nvarchar](max) COLLATE Latin1_General_CI_AS NULL,
    [NewValue] [nvarchar](max) COLLATE Latin1_General_CI_AS NULL,
    [UpdateDate] [datetime] NULL,
    [Username] [nvarchar](8) COLLATE Latin1_General_CI_AS NULL,
 CONSTRAINT [PK_AuditB] PRIMARY KEY CLUSTERED 
(
    [ID] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]

C'est le déclencheur pour une table:

INSERT INTO ILSe.dbo.Audit ([Type], TableName, PK, FieldName, OldValue, NewValue, Username)
      SELECT 
            CASE  WHEN NOT EXISTS (SELECT ID FROM deleted WHERE ID = ISNULL(ins.PK,del.PK)) THEN 'I' 
                WHEN NOT EXISTS (SELECT ID FROM inserted WHERE ID = ISNULL(ins.PK,del.PK)) THEN 'D' 
                  ELSE 'U' END as [Type],
            'AGB' as TableName, 
            ISNULL(ins.PK,del.PK) as PK,
            ISNULL(ins.FieldName,del.FieldName) as FieldName,
            del.FieldValue as OldValue,
            ins.FieldValue as NewValue,
            ISNULL(ins.Username,del.Username) as Username 
FROM (SELECT
      insRowTbl.PK,
      insRowTbl.Username,
      attr.insRow.value('local-name(.)', 'nvarchar(128)') as FieldName,
      attr.insRow.value('.', 'nvarchar(max)') as FieldValue
  FROM (Select
            i.ID as PK,
            i.LastModifiedBy as Username,
            convert(xml, (select i.* for xml raw)) as insRowCol
        from inserted as i
       ) as insRowTbl
       CROSS APPLY insRowTbl.insRowCol.nodes('/row/@*') as attr(insRow)
  ) as ins
FULL OUTER JOIN (SELECT
      delRowTbl.PK,
      delRowTbl.Username,
      attr.delRow.value('local-name(.)', 'nvarchar(128)') as FieldName,
      attr.delRow.value('.', 'nvarchar(max)') as FieldValue
  FROM (Select      
               d.ID as PK,
               d.LastModifiedBy as Username,
               convert(xml, (select d.* for xml raw)) as delRowCol
         from deleted as d
         ) as delRowTbl
        CROSS APPLY delRowTbl.delRowCol.nodes('/row/@*') as attr(delRow)
      ) as del
            on ins.PK = del.PK and ins.FieldName = del.FieldName
 WHERE 
      isnull(ins.FieldName,del.FieldName) not in ('LastModifiedBy', 'ID', 'TimeStamp') 
 and  ((ins.FieldValue is null and del.FieldValue is not null) 
      or (ins.FieldValue is not null and del.FieldValue is null) 
      or (ins.FieldValue != del.FieldValue))

Ce déclencheur concerne une table nommée AGB. La table avec le nom AGB a une colonne de clé primaire avec le nom ID et une colonne avec le nom LastModifiedBy qui contient le nom d'utilisateur ayant effectué la dernière modification.

Le déclencheur est composé de deux parties. Il convertit d’abord les colonnes des tables insérées et supprimées en lignes. Ceci est expliqué en détail ici: https://stackoverflow.com/a/43799776/4160788

Ensuite, il joint les lignes (une ligne par colonne) des tables insérées et supprimées par clé primaire et nom de champ, et enregistre une ligne pour chaque colonne modifiée. Il n'enregistre PAS les modifications d'ID, TimeStamp ou LastModifiedByColumn.

Vous pouvez insérer vos propres noms TableName, Columns.

Vous pouvez également créer la procédure stockée suivante, puis appeler cette procédure stockée pour générer vos déclencheurs:

IF NOT EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[_create_audit_trigger]') AND type in (N'P', N'PC'))
BEGIN
EXEC dbo.sp_executesql @statement = N'CREATE PROCEDURE [dbo].[_create_audit_trigger] AS' 
END
ALTER PROCEDURE [dbo].[_create_audit_trigger]
     @TableName varchar(max),
     @IDColumnName varchar(max) = 'ID',
     @LastModifiedByColumnName varchar(max) = 'LastModifiedBy',
     @TimeStampColumnName varchar(max) = 'TimeStamp'
AS
BEGIN  

PRINT 'start ' + @TableName + ' (' + @IDColumnName + ', ' + @LastModifiedByColumnName + ', ' + @TimeStampColumnName + ')'

/* if you have other audit trigger on this table and want to disable all triggers, enable this: 
EXEC ('ALTER TABLE ' + @TableName + ' DISABLE TRIGGER ALL')*/

IF EXISTS (SELECT * FROM sys.objects WHERE [type] = 'TR' AND [name] = 'tr_audit_'+@TableName)
    EXEC ('DROP TRIGGER [dbo].tr_audit_'+@TableName)


EXEC ('
CREATE TRIGGER [dbo].[tr_audit_'+@TableName+'] ON [ILSe].[dbo].['+@TableName+'] FOR INSERT, UPDATE, DELETE
AS
BEGIN
    SET NOCOUNT ON;

      INSERT INTO ILSe.dbo.Audit ([Type], TableName, PK, FieldName, OldValue, NewValue, Username)
      SELECT CASE  WHEN NOT EXISTS (SELECT '+@IDColumnName+' FROM deleted WHERE '+@IDColumnName+' = ISNULL(ins.PK,del.PK)) THEN ''I'' WHEN NOT EXISTS (SELECT '+@IDColumnName+' FROM inserted WHERE '+@IDColumnName+' = ISNULL(ins.PK,del.PK)) THEN ''D'' ELSE ''U'' END as [Type],
        '''+@TableName+''' as TableName, ISNULL(ins.PK,del.PK) as PK, ISNULL(ins.FieldName,del.FieldName) as FieldName, del.FieldValue as OldValue, ins.FieldValue as NewValue, ISNULL(ins.Username,del.Username) as Username FROM 
      (SELECT insRowTbl.PK, insRowTbl.Username, attr.insRow.value(''local-name(.)'', ''nvarchar(128)'') as FieldName, attr.insRow.value(''.'', ''nvarchar(max)'') as FieldValue FROM (Select      
                  i.'+@IDColumnName+' as PK,
                  i.'+@LastModifiedByColumnName+' as Username,
                  convert(xml, (select i.* for xml raw)) as insRowCol
                from inserted as i) as insRowTbl
                CROSS APPLY insRowTbl.insRowCol.nodes(''/row/@*'') as attr(insRow)) as ins
            FULL OUTER JOIN 
      (SELECT delRowTbl.PK, delRowTbl.Username, attr.delRow.value(''local-name(.)'', ''nvarchar(128)'') as FieldName, attr.delRow.value(''.'', ''nvarchar(max)'') as FieldValue FROM (Select      
                  d.'+@IDColumnName+' as PK,
                  d.'+@LastModifiedByColumnName+' as Username,
                  convert(xml, (select d.* for xml raw)) as delRowCol
                from deleted as d) as delRowTbl
                CROSS APPLY delRowTbl.delRowCol.nodes(''/row/@*'') as attr(delRow)) as del on ins.PK = del.PK and ins.FieldName = del.FieldName
    WHERE isnull(ins.FieldName,del.FieldName) not in ('''+@LastModifiedByColumnName+''', '''+@IDColumnName+''', '''+@TimeStampColumnName+''') and
    ((ins.FieldValue is null and del.FieldValue is not null) or (ins.FieldValue is not null and del.FieldValue is null) or (ins.FieldValue != del.FieldValue))

END
')

PRINT 'end ' + @TableName

PRINT ''

END
0
flack