Historisation de l’évolution des données

Le besoin d’historiser l’évolution des données est de plus en plus fort. Dans certains domaines, comme dans l’informatique médicale et tout ce qui touche à la santé, le demande de traçabilité de l’information, tant dans ses évolutions que dans son origine est important.
Il existe différentes méthodes dont j’ai parlé à travers une série d’article de SQL Server Magazine. Voici une méthode mise en place pour Santé Service dans la cadre de la refont de l’ERP interne.

Bien entendu, la solution retenue pour Santé Service est à la fois plus fine et plus profonde, car spécifique à SQL Server.
Cet article montre une synthèse que la plupart des SGBDR sont capable de mettre en œuvre facilement.
Les exemples sont donnés pour MS SQL Server, mais peuvent être reproduit sous n’importe quel SGBDR acceptant de produire des résultats de requête sous forme XML

1 – LE PRINCIPE

Notre mécanisme d’historisation des données (aussi appelé data tracking, change data capture ou encore audit trailing…) est constitué de deux parties :

1.1 – l’historisation en ligne (IN LINE) :

Elle est constituée de colonnes rajoutées à la table afin de connaître deux groupes d’informations :
– la date/heure et le nom d’utilisateur SQL de celui qui à inséré la ligne;
– la date/heure et le nom d’utilisateur SQL de celui qui à modifié la ligne en dernier.
Et de déclencheurs associés pour mettre à jour ces données (triggers INSERT et UPDATE).

1.2 – l’historisation hors ligne (OFF LINE) :

Elle est constitué de trois tables permettant d’obtenir toutes les changements d’informations intervenus lors des modifications (UPDATE) et de la suppression (DELETE) :
– T_TRACKED_TABLES_TBL : la table listant les tables dont on désire suivre l’évolution;
– T_TRACKED_DATATYPES_TDT : la table listant les seuls types de données dont on veut suivre l’évolution;
– T_TRACKED_DATACHANGES_TKC : la table de suivi du changement des données.
Et de déclencheurs associés pour insérer les données de l’évolution dans la table de suivi (triggers UPDATE et DELETE).

La suite de cet article est constitués essentiellement de code commenté, dont une partie constitue un jeu de test.

*.*.*.*.*.*.*.*.*.*.*.*.*.*.*.*.*.*.*.*.*.*.*.*.*.*.*.*.*.*

TEST : 1ere partie

Création de tables pour s’amuser avec l’historisation des données (une table client et une table commande).


CREATE TABLE T_CLIENT_CLI
(
CLI_ID               INT NOT NULL IDENTITY PRIMARY KEY,
CLI_NOM              VARCHAR(16),
CLI_OBSERVATIONS     VARCHAR(max)
);
 
CREATE TABLE T_COMMANDE_CMD
(
CMD_ID               INT NOT NULL IDENTITY PRIMARY KEY,
CLI_ID               INT NOT NULL FOREIGN KEY REFERENCES T_CLIENT_CLI (CLI_ID),
CMD_DATE             DATE,
CMD_OBSERVATIONS     XML
)

*.*.*.*.*.*.*.*.*.*.*.*.*.*.*.*.*.*.*.*.*.*.*.*.*.*.*.*.*.*

2 – LE MÉCANISME

Voici maintenant une série de 7 scripts SQL permettant de mettre en œuvre ce dispositif…
Veuillez noter que si vous être sur MS SQL Server, vous avez la possibilité d’exécuter le script n°0 en fin de document pour création de la base conformément aux besoin de test de cette solution.

2.1 – script de création des tables d’historisation


--===========================================================================--
-- 1 : script de création des tables d'historisation                         --
--===========================================================================--
 
-- isoler les tables de tracking dans un schema particulier de la base est une bonne idée.
-- on ne mélange pas les torchons et les serviettes !
CREATE SCHEMA S_TRK;
GO
 
-- cette table comporte la liste des tables dont on veut suivre l'évolution :
CREATE TABLE S_TRK.T_TRACKED_TABLES_TBL
(
TBL_ID                   INT NOT NULL IDENTITY  
                         CONSTRAINT PK_TBL PRIMARY KEY,
TBL_SCHEMA               NVARCHAR(128) NOT NULL,
TBL_NAME                 NVARCHAR(128) NOT NULL,
CONSTRAINT UK_TBL UNIQUE (TBL_SCHEMA, TBL_NAME)
);
GO
 
-- cette table contient les évolutions des données entreprise à chaque UPDATE ou DELETE dans les tables suivies :    
CREATE TABLE S_TRK.T_TRACKED_DATACHANGES_TKC
(
TKC_ID                   BIGINT NOT NULL IDENTITY  
                         CONSTRAINT PK_TKC PRIMARY KEY,
TBL_ID                   INT    NOT NULL
                         CONSTRAINT FK_TKC_TBL FOREIGN KEY
                         REFERENCES S_TRK.T_TRACKED_TABLES_TBL (TBL_ID),
TKC_LOGIN_NAME           NVARCHAR(128) NOT NULL  
                         CONSTRAINT DK_TKC_LOG DEFAULT SYSTEM_USER,        
TKC_USER_NAME            NVARCHAR(128) NOT NULL  
                         CONSTRAINT DK_TKC_USR DEFAULT USER,        
TKC_DATETIME             DATETIME NOT NULL
                         CONSTRAINT DK_TKC_DHC DEFAULT GETDATE() ,
TKC_DELETE               BIT NOT NULL  
                         CONSTRAINT DK_TKC_DEL DEFAULT 0,
-- partie spécifique à MS SQL Server :
TKC_CLIENT_NET_ADDRESS   VARCHAR(48),
TKC_LOCAL_NET_ADDRESS    VARCHAR(48),
TKC_HOST_NAME            NVARCHAR(128),
TKC_PROGRAM_NAME         NVARCHAR(128),
TKC_LOG_NAME             NVARCHAR(128),
TKC_ORIGINAL_LOGIN_NAME  NVARCHAR(128),
TKC_NT_DOMAIN            NVARCHAR(128),
TKC_NT_USER              NVARCHAR(128),
-- fin de la partie spécifique à MS SQL Server
TKC_XML_DATA             XML NOT NULL
);  
GO
 
-- cette table comportera la liste des types de données autorisés à être tracés.
CREATE TABLE S_TRK.T_TRACKED_DATATYPES_TDT
(
TDT_ID                  INT NOT NULL IDENTITY PRIMARY KEY,
TDT_DATATYPE            NVARCHAR(128)
);
GO

2.2 – alimentation de la liste des tables et des types de données historisés


--===========================================================================--
-- 2 : alimentation de la liste des tables et des types de données historisés
--===========================================================================--
 
-- voici comment ou alimente la table des tables à suivre :
INSERT INTO S_TRK.T_TRACKED_TABLES_TBL (TBL_SCHEMA, TBL_NAME)
SELECT TABLE_SCHEMA, TABLE_NAME
FROM   INFORMATION_SCHEMA.TABLES
WHERE  TABLE_TYPE = 'BASE TABLE'
-- on évitera d'auditer les tables d'audit !
  AND  TABLE_SCHEMA + '.' + TABLE_NAME  
       NOT IN ('S_TRK.T_TRACKED_TABLES_TBL',  
               'S_TRK.T_TRACKED_DATACHANGES_TKC',
               'S_TRK.T_TRACKED_DATATYPES_TDT');
-- vous pouvez exclure certaines autres tables de l'historisation
--  AND  TABLE_NAME   NOT IN (...)
--  AND  TABLE_SCHEMA NOT IN (...)                
 
-- on évitera de spécifier les types BLOBS ou des types complexes (XML, UDT, types RO...).
INSERT INTO S_TRK.T_TRACKED_DATATYPES_TDT
VALUES ('char'), ('varchar'), ('nchar'), ('nvarchar'),  
       ('decimal'), ('numeric'), ('float'), ('real'), ('int'), ('smallint'), ('bigint'),
       ('date'), ('time'),
-- types sépcifiques SQL Server :        
       ('datetime'), ('datetime2'), ('smalldatetime'), ('datetimeoffset'),  
       ('money'), ('smallmoney'),
       ('tinyint'),
       ('bit');
GO

2.3 – script générant les requêtes DDL de création des nouvelles colonnes :


--===========================================================================--
-- 3 : script générant les requêtes DDL de création des nouvelles colonnes   --
--===========================================================================--
 
-- à l'aide d'une CTE on scripte les ordres SQL DDL pour rajouter les colonnes
WITH  
T0 AS
(
SELECT TBL_SCHEMA, TBL_NAME
FROM   S_TRK.T_TRACKED_TABLES_TBL
)
SELECT 'ALTER TABLE ' + TBL_SCHEMA + '.' + TBL_NAME + ' ADD ' +
       'TRK_DH_INSERT DATETIME DEFAULT GETDATE();'
FROM   T0        
UNION  ALL
SELECT 'ALTER TABLE ' + TBL_SCHEMA + '.' + TBL_NAME + ' ADD ' +
       'TRK_DH_UPDATE DATETIME;'
FROM   T0
UNION  ALL
SELECT 'ALTER TABLE ' + TBL_SCHEMA + '.' + TBL_NAME + ' ADD ' +
       'TRK_ID_INSERT NVARCHAR(128);'
FROM   T0
UNION  ALL
 
SELECT 'ALTER TABLE ' + TBL_SCHEMA + '.' + TBL_NAME + ' ADD ' +
       'TRK_ID_UPDATE NVARCHAR(128);'
FROM   T0;
GO
-- le résultat de ce script doit être exécuté.

2.4 – création d’une fonction de calcul d’auto jointure :


--===========================================================================--
-- 4 : création d'une fonction de calcul d'auto jointure
--===========================================================================--
CREATE FUNCTION dbo.F_SCRIPT_SELF_JOIN  
   (@SHEMA  SYSNAME, @TABLE  SYSNAME, @ALIAS_LEFT  SYSNAME, @ALIAS_RIGHT SYSNAME)
RETURNS VARCHAR(max)
AS
/******************************************************************************  
* fonction de calcul d'auto jointure                                         *
*******************************************************************************  
* Fred. Brouard - http://sqlpro.developpez.com - www.sqlspot.com - 2009-04-10 *
*******************************************************************************  
* Cette fonction calcule la jointure naturelle de deux tables,                *  
******************************************************************************/  
BEGIN
   DECLARE @OUT VARCHAR(max);
   SET @OUT = '';
   SELECT @OUT = @OUT + @ALIAS_LEFT +'.' + COLUMN_NAME +' = '  
                      + @ALIAS_RIGHT + '.' + COLUMN_NAME + ' AND '
   FROM   (SELECT COLUMN_NAME
           FROM   INFORMATION_SCHEMA.KEY_COLUMN_USAGE AS KCU
                  INNER JOIN INFORMATION_SCHEMA.TABLE_CONSTRAINTS AS TC
                        ON KCU.CONSTRAINT_SCHEMA = TC.CONSTRAINT_SCHEMA
                           AND KCU.CONSTRAINT_NAME = TC.CONSTRAINT_NAME
           WHERE  TC.TABLE_SCHEMA = @SHEMA
             AND  TC.TABLE_NAME   = @TABLE
             AND  CONSTRAINT_TYPE = 'PRIMARY KEY') AS T;  
 
   IF LEN(@OUT) > 4
      SET @OUT = SUBSTRING(@OUT, 1, LEN(@OUT) - 4);
   RETURN @OUT;
   
END    
GO

Cette fonction est rendue nécessaire par le fait que le script suivante met en place des jointures de la table sur son double dans le trigger (pseudo table inserted ou deleted). Pour ce faire on utilise cette fonction qui calcule automatiquement la jointure en se basant sur les colonnes composant la clef primaire.

2.5 – script générant les déclencheurs alimentant les données en ligne :


--===========================================================================--
-- 5 : script générant les déclencheurs alimentant les données en ligne      --
--===========================================================================--
 
-- pour indenter l'écriture du déclencheur, nous utilisons la variable @CRLF
-- qui contient un Carriage Return et un Line Feed
DECLARE @CRLF NCHAR(2);
SET @CRLF = CHAR(13) + CHAR(10);
 
-- la requête générant les ordres SQL de création des triggers IN LINE :
WITH
T0 AS
(
SELECT TBL_SCHEMA AS S, TBL_NAME AS T,
       CASE  
          WHEN LEN(TBL_SCHEMA) <= 32 THEN TBL_SCHEMA  
          ELSE SUBSTRING(TBL_SCHEMA, 1, 32)  
       END + '_' +
       CASE  
          WHEN LEN(TBL_NAME) <= 64 THEN TBL_NAME  
          ELSE SUBSTRING(TBL_NAME, 1, 64)  
       END AS TAG
FROM   S_TRK.T_TRACKED_TABLES_TBL
)
SELECT 'CREATE TRIGGER E_I_' + TAG +'_TRACK_INLINE '                                       + @CRLF  
       + 'ON ' + S + '.' + T                                                               + @CRLF
       + 'FOR INSERT'                                                                      + @CRLF  
       + 'AS'                                                                              + @CRLF  
       + '/******************************************************************************' + @CRLF  
       + '* Trigger de gestion des méta données d''insertion (utilisateur + dateheure)  *' + @CRLF  
       + '*******************************************************************************' + @CRLF  
       + '* Fred. Brouard - http://sqlpro.developpez.com - www.sqlspot.com - 2009-12-12 *' + @CRLF  
       + '*******************************************************************************' + @CRLF  
       + '* Déclencheur FOR INSERT créé automatiquement                                 *' + @CRLF  
       + '******************************************************************************/' + @CRLF  
       + 'UPDATE ' +  S + '.' + T                                                          + @CRLF  
       + 'SET    ' + 'TRK_ID_INSERT = USER,'                                               + @CRLF  
       + '       ' + 'TRK_DH_INSERT = CURRENT_TIMESTAMP'                                   + @CRLF
       + 'FROM   ' + S + '.' + T + ' AS T '                                                + @CRLF
       + '       INNER JOIN inserted as I '                                                + @CRLF
       + '             ON ' + dbo.F_SCRIPT_SELF_JOIN (S, T, 'T', 'I') +';'                 + @CRLF  
FROM   T0
UNION  ALL                
SELECT 'CREATE TRIGGER E_U_' + TAG +'_TRACK_INLINE '                                       + @CRLF  
       + 'ON ' + S + '.' + T                                                               + @CRLF
       + 'FOR UPDATE'                                                                      + @CRLF  
       + 'AS'                                                                              + @CRLF  
       + '/******************************************************************************' + @CRLF  
       + '* Trigger de gestion des méta données de modification (utilisateur+dateheure) *' + @CRLF  
       + '*******************************************************************************' + @CRLF  
       + '* Fred. Brouard - http://sqlpro.developpez.com - www.sqlspot.com - 2009-12-12 *' + @CRLF  
       + '*******************************************************************************' + @CRLF  
       + '* Déclencheur FOR UPDATE créé automatiquement                                 *' + @CRLF  
       + '******************************************************************************/' + @CRLF  
       + 'UPDATE ' +  S + '.' + T                                                          + @CRLF  
       + 'SET    ' + 'TRK_ID_UPDATE = USER,'                                               + @CRLF  
       + '       ' + 'TRK_DH_UPDATE = CURRENT_TIMESTAMP'                                   + @CRLF        
       + 'FROM   ' + S + '.' + T + ' AS T '                                                + @CRLF
       + '       INNER JOIN inserted as I '                                                + @CRLF
       + '             ON ' + dbo.F_SCRIPT_SELF_JOIN (S, T, 'T', 'I') +';'                 + @CRLF  
FROM   T0
GO
 
-- le résultat de ce script doit être exécuté.

2.6 – création d’une fonction de calcul d’auto jointure :


--===========================================================================--
-- 6 : création d'une fonction de calcul d'auto jointure
--===========================================================================--
 
CREATE FUNCTION dbo.F_GET_ALL_COLUMNS (@SCH    NVARCHAR(128),  
                                       @TAB    NVARCHAR(128),  
                                       @NOBLOB BIT = 0,
                                       @NOBIN  BIT = 0,
                                       @EXCLUDE_PATTERN NVARCHAR(128) = '',
                                       @ESCAPE_PATTERN NCHAR(1) = '',                                        
                                       @ALIAS  NVARCHAR(128))
RETURNS NVARCHAR(max)                                        
AS
/******************************************************************************  
* fonction de listage des colonnes d'une table                                *
*******************************************************************************  
* Fred. Brouard - http://sqlpro.developpez.com - www.sqlspot.com - 2009-12-10 *
*******************************************************************************  
* Cette fonction calcule la lisre des colonnes d'une table                    *  
******************************************************************************/  
BEGIN
   DECLARE @SQL VARCHAR(MAX);
   SELECT @SQL = '';
   SELECT @SQL = @SQL + COALESCE(@ALIAS +'.', '') + COLUMN_NAME +', '
   FROM
   (
   SELECT COLUMN_NAME, ORDINAL_POSITION
   FROM   INFORMATION_SCHEMA.COLUMNS
   WHERE  TABLE_NAME = @TAB
     AND  TABLE_SCHEMA = COALESCE(@SCH, 'dbo')
     AND  DATA_TYPE =
          CASE
             WHEN @NOBLOB = 1  
                  AND DATA_TYPE IN ('text', 'ntext', 'image',
                                    'varchar(max)', 'nvarchar(max)', 'varbinary(max)',
                                    'hierarchyid', 'geometry', 'geography',
                                    'sql_variant', 'xml', 'timestamp', 'rowversion')  
                THEN ''  
             WHEN @NOBIN = 1  
                  AND DATA_TYPE IN ('binary', 'varbinary', 'varbinary(max)', 'image')  
                THEN ''    
             ELSE DATA_TYPE
          END
   INTERSECT
   SELECT COLUMN_NAME, ORDINAL_POSITION  
   FROM   INFORMATION_SCHEMA.COLUMNS
   WHERE  TABLE_NAME = @TAB
     AND  TABLE_SCHEMA = COALESCE(@SCH, 'dbo')
     AND  DATA_TYPE IN (SELECT TDT_DATATYPE
                        FROM   S_TRK.T_TRACKED_DATATYPES_TDT)  
   INTERSECT
   SELECT COLUMN_NAME, ORDINAL_POSITION  
   FROM   INFORMATION_SCHEMA.COLUMNS
   WHERE  TABLE_NAME = @TAB
     AND  TABLE_SCHEMA = COALESCE(@SCH, 'dbo')
     AND  COLUMN_NAME NOT LIKE @EXCLUDE_PATTERN ESCAPE @ESCAPE_PATTERN                                    
   ) AS T                      
   ORDER BY ORDINAL_POSITION;       ;  
   IF @SQL = ''  
      RETURN ''  
   RETURN SUBSTRING(@SQL, 1, LEN(@SQL) -1);
END    
GO

Cette fonction est rendue nécessaire par le fait que le script suivante met en place des listes de colonnes de la table tracée dans le trigger. Pour ce faire on utilise cette fonction qui calcule automatiquement la liste des colonnes de la table en se basant sur la table INFORMATION_SCHEMA.COLUMNS et ne ne prenant que les colonnes dont le type SQL est dans la table S_TRK.T_TRACKED_DATATYPES_TDT.

2.7 – script générant les déclencheurs alimentant les données hors ligne


--===========================================================================--
-- 7 : script générant les déclencheurs alimentant les données hors ligne
--===========================================================================--
 
-- pour indenter l'écriture du déclencheur, nous utilisons la variable @CRLF
-- qui contient un "Carriage Return" et un "Line Feed"
DECLARE @CRLF NCHAR(2);
SET @CRLF = CHAR(13) + CHAR(10);
 
-- la requête générant les ordres SQL de création des triggers OFF LINE :
WITH  
T0 AS
(
SELECT TBL_SCHEMA AS S, TBL_NAME AS T
FROM   S_TRK.T_TRACKED_TABLES_TBL
EXCEPT
SELECT TABLE_SCHEMA, TABLE_NAME
FROM   INFORMATION_SCHEMA.COLUMNS AS C
WHERE  DATA_TYPE NOT IN (SELECT TDT_DATATYPE
                         FROM   S_TRK.T_TRACKED_DATATYPES_TDT)
  AND  SUBSTRING(COLUMN_NAME, 1, 4) = 'TRK_'
),
T1 AS
(
SELECT DISTINCT S, T,
       CASE  
          WHEN LEN(S) <= 32 THEN S  
          ELSE SUBSTRING(S, 1, 32)  
       END + '_' +
       CASE  
          WHEN LEN(T) <= 64 THEN T  
          ELSE SUBSTRING(T, 1, 64)  
       END AS TAG
FROM   T0)
SELECT 'CREATE TRIGGER E_U_' + TAG +'_TRACK_OFFLINE '                                      + @CRLF  
       + 'ON ' + S + '.' + T                                                               + @CRLF  
       + 'FOR UPDATE '                                                                     + @CRLF  
       + 'AS '                                                                             + @CRLF  
       + '/******************************************************************************' + @CRLF  
       + '* Trigger d''historisation des données à l''insertion. (métadonnées + XML)    *' + @CRLF  
       + '*******************************************************************************' + @CRLF  
       + '* Fred. Brouard - http://sqlpro.developpez.com - www.sqlspot.com - 2009-12-12 *' + @CRLF  
       + '*******************************************************************************' + @CRLF  
       + '* Déclencheur FOR DELETE créé automatiquement                                 *' + @CRLF  
       + '******************************************************************************/' + @CRLF  
       + 'INSERT INTO S_TRK.T_TRACKED_DATACHANGES_TKC '                                    + @CRLF  
       + '       (TBL_ID, TKC_LOGIN_NAME, TKC_USER_NAME, TKC_DATETIME, TKC_DELETE, '       + @CRLF  
       + '        /* partie spécifique à MS SQL Server : */'                               + @CRLF  
       + '        TKC_CLIENT_NET_ADDRESS, TKC_LOCAL_NET_ADDRESS, TKC_HOST_NAME, '          + @CRLF  
       + '        TKC_PROGRAM_NAME, TKC_LOG_NAME, TKC_ORIGINAL_LOGIN_NAME, '               + @CRLF  
       + '        TKC_NT_DOMAIN, TKC_NT_USER, '                                            + @CRLF  
       + '        /* fin de la partie spécifique à MS SQL Server */'                       + @CRLF  
       + '        TKC_XML_DATA)'                                                           + @CRLF  
       + 'SELECT TBL_ID, SYSTEM_USER, USER, CURRENT_TIMESTAMP, 0, '                        + @CRLF  
       + '       client_net_address, local_net_address, host_name, '                       + @CRLF  
       + '       program_name, login_name, original_login_name, '                          + @CRLF  
       + '       nt_domain, nt_user_name, '                                                + @CRLF  
       + '       CAST((SELECT *'                                                           + @CRLF  
       + '             FROM (SELECT ''AVANT'' AS MOMENT, '  
                            + dbo.F_GET_ALL_COLUMNS (S, T, 1, 1, 'TRK?_%', '?', NULL)      + @CRLF    
       + '                   FROM   deleted'                                               + @CRLF  
       + '                   UNION ALL'                                                    + @CRLF  
       + '                   SELECT ''APRES'' AS MOMENT, '  
                            + dbo.F_GET_ALL_COLUMNS (S, T, 1, 1, 'TRK?_%', '?', NULL)      + @CRLF    
       + '                   FROM   inserted) AS T'                                        + @CRLF  
       + '             FOR XML AUTO, ELEMENTS, ROOT(''SQL_DATASET'')) AS XML) '            + @CRLF  
       + 'FROM   S_TRK.T_TRACKED_TABLES_TBL '                                              + @CRLF  
       + '       CROSS JOIN sys.dm_exec_connections  AS EC '                               + @CRLF  
       + '             INNER JOIN sys.dm_exec_sessions AS ES '                             + @CRLF  
       + '                   ON EC.session_id = ES.session_id '                            + @CRLF  
       + 'WHERE  TBL_SCHEMA = ''' + S + ''' '                                              + @CRLF  
       + '  AND  TBL_NAME = ''' + T + ''' '                                                + @CRLF  
       + '  AND  EC.session_id = @@SPID; '                                                 + @CRLF    
FROM   T1
UNION  ALL                
SELECT 'CREATE TRIGGER E_D_' + TAG +'_TRACK_OFFLINE '                                      + @CRLF
       + 'ON ' + S + '.' + T                                                               + @CRLF
       + 'FOR DELETE'                                                                      + @CRLF  
       + 'AS'                                                                              + @CRLF  
       + '/******************************************************************************' + @CRLF  
       + '* Trigger d''historisation des données à la suppression (métadonnées + XML)   *' + @CRLF  
       + '*******************************************************************************' + @CRLF  
       + '* Fred. Brouard - http://sqlpro.developpez.com - www.sqlspot.com - 2009-12-12 *' + @CRLF  
       + '*******************************************************************************' + @CRLF  
       + '* Déclencheur FOR DELETE créé automatiquement                                 *' + @CRLF  
       + '******************************************************************************/' + @CRLF  
       + 'INSERT INTO S_TRK.T_TRACKED_DATACHANGES_TKC '                                    + @CRLF  
       + '       (TBL_ID, TKC_LOGIN_NAME, TKC_USER_NAME, TKC_DATETIME, TKC_DELETE, '       + @CRLF  
       + '        /* partie spécifique à MS SQL Server : */'                               + @CRLF  
       + '        TKC_CLIENT_NET_ADDRESS, TKC_LOCAL_NET_ADDRESS, TKC_HOST_NAME, '          + @CRLF  
       + '        TKC_PROGRAM_NAME, TKC_LOG_NAME, TKC_ORIGINAL_LOGIN_NAME, '               + @CRLF  
       + '        TKC_NT_DOMAIN, TKC_NT_USER, '                                            + @CRLF  
       + '        /* fin de la partie spécifique à MS SQL Server */'                       + @CRLF  
       + '        TKC_XML_DATA)'                                                           + @CRLF  
       + 'SELECT TBL_ID, SYSTEM_USER, USER, CURRENT_TIMESTAMP, 1, '                        + @CRLF  
       + '       client_net_address, local_net_address, host_name, '                       + @CRLF  
       + '       program_name, login_name, original_login_name, '                          + @CRLF  
       + '       nt_domain, nt_user_name, '                                                + @CRLF  
       + '       CAST((SELECT *'                                                           + @CRLF  
       + '             FROM (SELECT ''AVANT'' AS MOMENT, '  
                            + dbo.F_GET_ALL_COLUMNS (S, T, 1, 1, 'TRK?_%', '?', NULL)      + @CRLF    
       + '                   FROM   deleted) AS T'                                         + @CRLF  
       + '             FOR XML AUTO, ELEMENTS, ROOT(''SQL_DATASET'')) AS XML)'             + @CRLF  
       + 'FROM   S_TRK.T_TRACKED_TABLES_TBL '                                              + @CRLF  
       + '       CROSS JOIN sys.dm_exec_connections  AS EC '                               + @CRLF  
       + '             INNER JOIN sys.dm_exec_sessions AS ES '                             + @CRLF  
       + '                   ON EC.session_id = ES.session_id '                            + @CRLF  
       + 'WHERE  TBL_SCHEMA = ''' + S + ''' '                                              + @CRLF  
       + '  AND  TBL_NAME = ''' + T + ''' '                                                + @CRLF  
       + '  AND  EC.session_id = @@SPID; '                                                 + @CRLF    
FROM   T1;
GO
 
-- le résultat de ce script doit être exécuté.

*.*.*.*.*.*.*.*.*.*.*.*.*.*.*.*.*.*.*.*.*.*.*.*.*.*.*.*.*.*

TEST : 2e partie

Quelques mises à jour pour s’amuser avec l’historisation des données (quelques insertions, une mise à jour et des suppressions, le tout sous différents utilisateurs SQL)


-- création d'utilisateurs SQL et leurs privilèges pour test :
CREATE LOGIN L_123 WITH PASSWORD = '123'
CREATE LOGIN L_456 WITH PASSWORD = '456'
CREATE USER U_123 FOR LOGIN L_123;
CREATE USER U_456 FOR LOGIN L_456;
EXEC sp_addsrvrolemember 'L_123', 'sysadmin';
EXEC sp_addsrvrolemember 'L_456', 'sysadmin';
EXEC sp_addrolemember 'db_owner', 'U_123';
EXEC sp_addrolemember 'db_owner', 'U_456';
 
-- <strong>SE CONNECTER AVEC LE COMPTE L_123</strong> :
 
INSERT INTO T_CLIENT_CLI (CLI_NOM, CLI_OBSERVATIONS)
VALUES ('DUPONT', 'peu aimable'),
       ('SCHMIDT', 'Fort potentiel'),
       ('JOHNSON', NULL);
INSERT INTO T_COMMANDE_CMD (CLI_ID, CMD_DATE, CMD_OBSERVATIONS)
VALUES (1, '2009-12-08', 'Prix serrés !'),
       (1, '2009-12-12', 'Soldes à prévoir'),
       (2, '2009-12-12', NULL);
UPDATE T_COMMANDE_CMD  
SET    CMD_DATE = '2009-12-10'
WHERE  CMD_DATE = '2009-12-08';  
 
-- SE CONNECTER AVEC LE COMPTE L_456 :
 
DELETE FROM T_COMMANDE_CMD
WHERE  CMD_DATE = '2009-12-12';

Allons voir ce qui s’est passé :


-- dans les clients :
 
SELECT *  
FROM   T_CLIENT_CLI;
 
CLI_ID      CLI_NOM          CLI_OBSERVATIONS   TRK_DH_INSERT           TRK_DH_UPDATE   TRK_ID_INSERT  TRK_ID_UPDATE
----------- ---------------- ------------------ ----------------------- --------------- -------------- ----------------
1           DUPONT           peu aimable        2009-12-13 12:18:53.543 NULL            U_123          NULL
2           SCHMIDT          Fort potentiel     2009-12-13 12:18:53.543 NULL            U_123          NULL
3           JOHNSON          NULL               2009-12-13 12:18:53.543 NULL            U_123          NULL
 
--dans les commandes :
 
SELECT *  
FROM   T_COMMANDE_CMD
 
CMD_ID      CLI_ID      CMD_DATE   CMD_OBSERVATIONS    TRK_DH_INSERT           TRK_DH_UPDATE           TRK_ID_INSERT   TRK_ID_UPDATE
----------- ----------- ---------- ------------------- ----------------------- ----------------------- --------------- ----------------
1           1           2009-12-10 Prix serrés !       2009-12-13 12:18:53.560 2009-12-13 12:18:53.560 U_123           U_123
 
 
-- dans les changements (table S_TRK.T_TRACKED_DATACHANGES_TKC) :
 
SELECT *
FROM   S_TRK.T_TRACKED_DATACHANGES_TKC
 
TKC_ID    TBL_ID      TKC_LOGIN_NAME   TKC_USER_NAME    TKC_DATETIME            ... TKC_XML_DATA
--------- ----------- ---------------- ---------------- ----------------------- ... ------------------------------------------
1         1           L_123            U_123            2009-12-13 12:18:53.577 ... <SQL_DATASET>
                                                                                      <T>
                                                                                        <MOMENT>AVANT</MOMENT>
                                                                                        <CMD_ID>1</CMD_ID>
                                                                                        <CLI_ID>1</CLI_ID>
                                                                                        <CMD_DATE>2009-12-08</CMD_DATE>
                                                                                      </T>
                                                                                      <T>
                                                                                        <MOMENT>APRES</MOMENT>
                                                                                        <CMD_ID>1</CMD_ID>
                                                                                        <CLI_ID>1</CLI_ID>
                                                                                        <CMD_DATE>2009-12-10</CMD_DATE>
                                                                                      </T>
                                                                                    </SQL_DATASET>
2         1           L_456            U_456            2009-12-13 12:19:53.560 ... <SQL_DATASET>
                                                                                      <T>
                                                                                        <MOMENT>AVANT</MOMENT>
                                                                                        <CMD_ID>3</CMD_ID>
                                                                                        <CLI_ID>2</CLI_ID>
                                                                                        <CMD_DATE>2009-12-12</CMD_DATE>
                                                                                      </T>
                                                                                      <T>
                                                                                        <MOMENT>AVANT</MOMENT>
                                                                                        <CMD_ID>2</CMD_ID>
                                                                                        <CLI_ID>1</CLI_ID>
                                                                                        <CMD_DATE>2009-12-12</CMD_DATE>
                                                                                      </T>
                                                                                    </SQL_DATASET>

NOTA : nous n’avons pas ici reproduit les données des colonnes qpécifiques à SQL Server.

*.*.*.*.*.*.*.*.*.*.*.*.*.*.*.*.*.*.*.*.*.*.*.*.*.*.*.*.*.*

3 – REQUÊTES GÉNÉRÉES PAR LES SCRIPTS

À titre d’exemple, voici l’ensemble des requêtes générées par ces scripts :

3.1 – Nouvelles colonnes :

ALTER TABLE dbo.T_CLIENT_CLI ADD TRK_DH_INSERT DATETIME DEFAULT GETDATE();
ALTER TABLE dbo.T_COMMANDE_CMD ADD TRK_DH_INSERT DATETIME DEFAULT GETDATE();
ALTER TABLE dbo.T_CLIENT_CLI ADD TRK_DH_UPDATE DATETIME;
ALTER TABLE dbo.T_COMMANDE_CMD ADD TRK_DH_UPDATE DATETIME;
ALTER TABLE dbo.T_CLIENT_CLI ADD TRK_ID_INSERT NVARCHAR(128);
ALTER TABLE dbo.T_COMMANDE_CMD ADD TRK_ID_INSERT NVARCHAR(128);
ALTER TABLE dbo.T_CLIENT_CLI ADD TRK_ID_UPDATE NVARCHAR(128);
ALTER TABLE dbo.T_COMMANDE_CMD ADD TRK_ID_UPDATE NVARCHAR(128);

3.2 – Déclencheurs « IN LINE » :


CREATE TRIGGER E_I_dbo_T_CLIENT_CLI_TRACK_INLINE  
ON dbo.T_CLIENT_CLI
FOR INSERT
AS
/******************************************************************************
* Trigger de gestion des méta données d'insertion (utilisateur + dateheure)  *
*******************************************************************************
* Fred. Brouard - http://sqlpro.developpez.com - www.sqlspot.com - 2009-12-12 *
*******************************************************************************
* Déclencheur FOR INSERT créé automatiquement                                 *
******************************************************************************/
UPDATE dbo.T_CLIENT_CLI
SET    TRK_ID_INSERT = USER,
       TRK_DH_INSERT = CURRENT_TIMESTAMP
FROM   dbo.T_CLIENT_CLI AS T  
       INNER JOIN inserted as I  
             ON T.CLI_ID = I.CLI_ID;
             
             
CREATE TRIGGER E_I_dbo_T_COMMANDE_CMD_TRACK_INLINE  
ON dbo.T_COMMANDE_CMD
FOR INSERT
AS
/******************************************************************************
* Trigger de gestion des méta données d'insertion (utilisateur + dateheure)  *
*******************************************************************************
* Fred. Brouard - http://sqlpro.developpez.com - www.sqlspot.com - 2009-12-12 *
*******************************************************************************
* Déclencheur FOR INSERT créé automatiquement                                 *
******************************************************************************/
UPDATE dbo.T_COMMANDE_CMD
SET    TRK_ID_INSERT = USER,
       TRK_DH_INSERT = CURRENT_TIMESTAMP
FROM   dbo.T_COMMANDE_CMD AS T  
       INNER JOIN inserted as I  
             ON T.CMD_ID = I.CMD_ID;
             
             
CREATE TRIGGER E_U_dbo_T_CLIENT_CLI_TRACK_INLINE  
ON dbo.T_CLIENT_CLI
FOR UPDATE
AS
/******************************************************************************
* Trigger de gestion des méta données de modification (utilisateur+dateheure) *
*******************************************************************************
* Fred. Brouard - http://sqlpro.developpez.com - www.sqlspot.com - 2009-12-12 *
*******************************************************************************
* Déclencheur FOR UPDATE créé automatiquement                                 *
******************************************************************************/
UPDATE dbo.T_CLIENT_CLI
SET    TRK_ID_UPDATE = USER,
       TRK_DH_UPDATE = CURRENT_TIMESTAMP
FROM   dbo.T_CLIENT_CLI AS T  
       INNER JOIN inserted as I  
             ON T.CLI_ID = I.CLI_ID;
             
             
CREATE TRIGGER E_U_dbo_T_COMMANDE_CMD_TRACK_INLINE  
ON dbo.T_COMMANDE_CMD
FOR UPDATE
AS
/******************************************************************************
* Trigger de gestion des méta données de modification (utilisateur+dateheure) *
*******************************************************************************
* Fred. Brouard - http://sqlpro.developpez.com - www.sqlspot.com - 2009-12-12 *
*******************************************************************************
* Déclencheur FOR UPDATE créé automatiquement                                 *
******************************************************************************/
UPDATE dbo.T_COMMANDE_CMD
SET    TRK_ID_UPDATE = USER,
       TRK_DH_UPDATE = CURRENT_TIMESTAMP
FROM   dbo.T_COMMANDE_CMD AS T  
       INNER JOIN inserted as I  
             ON T.CMD_ID = I.CMD_ID;

3.3 – Déclencheurs « OFF LINE » :


CREATE TRIGGER E_U_dbo_T_CLIENT_CLI_TRACK_OFFLINE  
ON dbo.T_CLIENT_CLI
FOR UPDATE  
AS  
/******************************************************************************
* Trigger d'historisation des données à l'insertion. (métadonnées + XML)    *
*******************************************************************************
* Fred. Brouard - http://sqlpro.developpez.com - www.sqlspot.com - 2009-12-12 *
*******************************************************************************
* Déclencheur FOR DELETE créé automatiquement                                 *
******************************************************************************/
INSERT INTO S_TRK.T_TRACKED_DATACHANGES_TKC  
       (TBL_ID, TKC_LOGIN_NAME, TKC_USER_NAME, TKC_DATETIME, TKC_DELETE,  
        /* partie spécifique à MS SQL Server : */
        TKC_CLIENT_NET_ADDRESS, TKC_LOCAL_NET_ADDRESS, TKC_HOST_NAME,  
        TKC_PROGRAM_NAME, TKC_LOG_NAME, TKC_ORIGINAL_LOGIN_NAME,  
        TKC_NT_DOMAIN, TKC_NT_USER,  
        /* fin de la partie spécifique à MS SQL Server */
        TKC_XML_DATA)
SELECT TBL_ID, SYSTEM_USER, USER, CURRENT_TIMESTAMP, 0,  
       client_net_address, local_net_address, host_name,  
       program_name, login_name, original_login_name,  
       nt_domain, nt_user_name,  
       CAST((SELECT *
             FROM (SELECT 'AVANT' AS MOMENT, CLI_ID, CLI_NOM, CLI_OBSERVATIONS
                   FROM   deleted
                   UNION ALL
                   SELECT 'APRES' AS MOMENT, CLI_ID, CLI_NOM, CLI_OBSERVATIONS
                   FROM   inserted) AS T
             FOR XML AUTO, ELEMENTS, ROOT('SQL_DATASET')) AS XML)  
FROM   S_TRK.T_TRACKED_TABLES_TBL  
       CROSS JOIN sys.dm_exec_connections  AS EC  
             INNER JOIN sys.dm_exec_sessions AS ES  
                   ON EC.session_id = ES.session_id  
WHERE  TBL_SCHEMA = 'dbo'  
  AND  TBL_NAME = 'T_CLIENT_CLI'  
  AND  EC.session_id = @@SPID;
   
   
CREATE TRIGGER E_U_dbo_T_COMMANDE_CMD_TRACK_OFFLINE  
ON dbo.T_COMMANDE_CMD
FOR UPDATE  
AS  
/******************************************************************************
* Trigger d'historisation des données à l'insertion. (métadonnées + XML)    *
*******************************************************************************
* Fred. Brouard - http://sqlpro.developpez.com - www.sqlspot.com - 2009-12-12 *
*******************************************************************************
* Déclencheur FOR DELETE créé automatiquement                                 *
******************************************************************************/
INSERT INTO S_TRK.T_TRACKED_DATACHANGES_TKC  
       (TBL_ID, TKC_LOGIN_NAME, TKC_USER_NAME, TKC_DATETIME, TKC_DELETE,  
        /* partie spécifique à MS SQL Server : */
        TKC_CLIENT_NET_ADDRESS, TKC_LOCAL_NET_ADDRESS, TKC_HOST_NAME,  
        TKC_PROGRAM_NAME, TKC_LOG_NAME, TKC_ORIGINAL_LOGIN_NAME,  
        TKC_NT_DOMAIN, TKC_NT_USER,  
        /* fin de la partie spécifique à MS SQL Server */
        TKC_XML_DATA)
SELECT TBL_ID, SYSTEM_USER, USER, CURRENT_TIMESTAMP, 0,  
       client_net_address, local_net_address, host_name,  
       program_name, login_name, original_login_name,  
       nt_domain, nt_user_name,  
       CAST((SELECT *
             FROM (SELECT 'AVANT' AS MOMENT, CMD_ID, CLI_ID, CMD_DATE
                   FROM   deleted
                   UNION ALL
                   SELECT 'APRES' AS MOMENT, CMD_ID, CLI_ID, CMD_DATE
                   FROM   inserted) AS T
             FOR XML AUTO, ELEMENTS, ROOT('SQL_DATASET')) AS XML)  
FROM   S_TRK.T_TRACKED_TABLES_TBL  
       CROSS JOIN sys.dm_exec_connections  AS EC  
             INNER JOIN sys.dm_exec_sessions AS ES  
                   ON EC.session_id = ES.session_id  
WHERE  TBL_SCHEMA = 'dbo'  
  AND  TBL_NAME = 'T_COMMANDE_CMD'  
  AND  EC.session_id = @@SPID;
   
   
CREATE TRIGGER E_D_dbo_T_CLIENT_CLI_TRACK_OFFLINE  
ON dbo.T_CLIENT_CLI
FOR DELETE
AS
/******************************************************************************
* Trigger d'historisation des données à la suppression (métadonnées + XML)   *
*******************************************************************************
* Fred. Brouard - http://sqlpro.developpez.com - www.sqlspot.com - 2009-12-12 *
*******************************************************************************
* Déclencheur FOR DELETE créé automatiquement                                 *
******************************************************************************/
INSERT INTO S_TRK.T_TRACKED_DATACHANGES_TKC  
       (TBL_ID, TKC_LOGIN_NAME, TKC_USER_NAME, TKC_DATETIME, TKC_DELETE,  
        /* partie spécifique à MS SQL Server : */
        TKC_CLIENT_NET_ADDRESS, TKC_LOCAL_NET_ADDRESS, TKC_HOST_NAME,  
        TKC_PROGRAM_NAME, TKC_LOG_NAME, TKC_ORIGINAL_LOGIN_NAME,  
        TKC_NT_DOMAIN, TKC_NT_USER,  
        /* fin de la partie spécifique à MS SQL Server */
        TKC_XML_DATA)
SELECT TBL_ID, SYSTEM_USER, USER, CURRENT_TIMESTAMP, 1,  
       client_net_address, local_net_address, host_name,  
       program_name, login_name, original_login_name,  
       nt_domain, nt_user_name,  
       CAST((SELECT *
             FROM (SELECT 'AVANT' AS MOMENT, CLI_ID, CLI_NOM, CLI_OBSERVATIONS
                   FROM   deleted) AS T
             FOR XML AUTO, ELEMENTS, ROOT('SQL_DATASET')) AS XML)
FROM   S_TRK.T_TRACKED_TABLES_TBL  
       CROSS JOIN sys.dm_exec_connections  AS EC  
             INNER JOIN sys.dm_exec_sessions AS ES  
                   ON EC.session_id = ES.session_id  
WHERE  TBL_SCHEMA = 'dbo'  
  AND  TBL_NAME = 'T_CLIENT_CLI'  
  AND  EC.session_id = @@SPID;
   
   
CREATE TRIGGER E_D_dbo_T_COMMANDE_CMD_TRACK_OFFLINE  
ON dbo.T_COMMANDE_CMD
FOR DELETE
AS
/******************************************************************************
* Trigger d'historisation des données à la suppression (métadonnées + XML)   *
*******************************************************************************
* Fred. Brouard - http://sqlpro.developpez.com - www.sqlspot.com - 2009-12-12 *
*******************************************************************************
* Déclencheur FOR DELETE créé automatiquement                                 *
******************************************************************************/
INSERT INTO S_TRK.T_TRACKED_DATACHANGES_TKC  
       (TBL_ID, TKC_LOGIN_NAME, TKC_USER_NAME, TKC_DATETIME, TKC_DELETE,  
        /* partie spécifique à MS SQL Server : */
        TKC_CLIENT_NET_ADDRESS, TKC_LOCAL_NET_ADDRESS, TKC_HOST_NAME,  
        TKC_PROGRAM_NAME, TKC_LOG_NAME, TKC_ORIGINAL_LOGIN_NAME,  
        TKC_NT_DOMAIN, TKC_NT_USER,  
        /* fin de la partie spécifique à MS SQL Server */
        TKC_XML_DATA)
SELECT TBL_ID, SYSTEM_USER, USER, CURRENT_TIMESTAMP, 1,  
       client_net_address, local_net_address, host_name,  
       program_name, login_name, original_login_name,  
       nt_domain, nt_user_name,  
       CAST((SELECT *
             FROM (SELECT 'AVANT' AS MOMENT, CMD_ID, CLI_ID, CMD_DATE
                   FROM   deleted) AS T
             FOR XML AUTO, ELEMENTS, ROOT('SQL_DATASET')) AS XML)
FROM   S_TRK.T_TRACKED_TABLES_TBL  
       CROSS JOIN sys.dm_exec_connections  AS EC  
             INNER JOIN sys.dm_exec_sessions AS ES  
                   ON EC.session_id = ES.session_id  
WHERE  TBL_SCHEMA = 'dbo'  
  AND  TBL_NAME = 'T_COMMANDE_CMD'  
  AND  EC.session_id = @@SPID;

4 – AMÉLIORATIONS DU SYSTÈME :

4.1 – évolution de la structure des tables :

Il faut penser que les tables peuvent évoluer, par exemple l’ajout de colonne ou le retrait est toujours possible dans la vie d’une base de données. C’est pourquoi, si votre SGBDR supporte les déclencheurs DDL comme SQL Server par exemple, alors il est facile d’implanter des triggers se déclenchant sur ALTER TABLE ou DROP TABLE, qui supprimment et replace les colonnes et déclencheur d’hitorisation.

4.2 – meilleure présentation des données de l’UPDATE :

Une meilleure présentation du XML montrant les différences dans la modification (UPDATE) serait de grouper les données avant et après en deux items à même niveau de l’arborescen XML.
Ceci peut être fait en considérant que chacune des requêtes sur les pseudo table INSERTED et DELETE produisent leur propre XML dont les racines serait AVANT et APRES, et que l’on peut concaténer comme suit :
@XMLAVANT.modify(' insert sql:variable("@XMLAPRES") into (/)[1]')
Mais il faut penser y rajouter la racine avec :

DECLARE @XMLROOT XML
SET @XMLROOT = '<DATASET></DATASET>'
SET @XMLROOT.modify(' insert sql:variable("@XMLAVANT")  into (/DATASET)[1]')
SELECT @XMLROOT

5 – LIMITES DU SYSTÈME

Le tracking reposant sur l’utilisation des utilisateurs SQL, il faut créer un utilisateur SQL et sa connexion par personnes susceptible d’utiliser l’application.
Toutes les tables auditées doivent avoir une clef primaire.

6 – CORRECTIONS À APPORTER POUR DES BASES NON MS SQL Server

6.1 : les pseudos tables inserted et deleted sont spécifiques à MS SQL Server. Veuillez utiliser un alias spécifique, ou aliassez les pseudo tables des trigger « PER STATEMENT » inserted et delete.
6.2 : les SGBDR comme MySQL qui ne supportent pas les CTE doivent utiliser des vues ou des sous requêtes en tables dérivées.
6.3 : retirer les éléments de tracking de méta données spécifique à SQL server (colonnes de la table de suivi, jointure dans la requête produisant le trigger OFF LINE, filtre sur @SPID…).

7 – BONUS


--===========================================================================--
<strong>-- 0 : script de création des objets technique nécessaires à l'historisation --</strong>
--===========================================================================--
 
USE master;
GO
 
IF EXISTS(SELECT *
          FROM   sys.databases  
          WHERE  name = 'DB_TEST_TRACK')
BEGIN
   ALTER DATABASE DB_TEST_TRACK
   SET SINGLE_USER WITH ROLLBACK IMMEDIATE;          
   DROP DATABASE DB_TEST_TRACK;
END;
GO      
 
CREATE DATABASE DB_TEST_TRACK;
GO
 
USE DB_TEST_TRACK;
GO

PS : Santé Service est la plus importante association à but non lucratif d’HAD (Hospitalisation À Domicile). Elle soigne en moyenne 1200 patients en Île de France. C’est aussi une des plus ancienne : elle a été fondée en 1958 sur l’initiative de médecins de l’Institut Gustave Roussy spécialisé dans les soins du cancer.


Frédéric BROUARD, Spécialiste modélisation, bases de données, optimisation, langage SQL.
Le site sur le langage SQL et les S.G.B.D. relationnels : http://sqlpro.developpez.com/
Expert SQL Server http://www.sqlspot.com : audit, optimisation, tuning, formation
* * * * * Enseignant au CNAM PACA et à l’ISEN à Toulon * * * * *

3 réflexions au sujet de « Historisation de l’évolution des données »

  1. Avatar de nicopulsenicopulse

    Bonjour,

    J’ai reproduit vos instructions dans une base de test sur SQL Serveur 2008, mais à la partie TEST après les insertions et les updates je n’ai aucune donnée dans T_TRACKED_DATACHANGES_TKC.

    Faut il se connecter avec un utilisateur spécifique pour voir les données de cette table ?

Laisser un commentaire