154 votes

Existe-t-il une option/caractéristique MySQL permettant de suivre l'historique des modifications apportées aux enregistrements ?

On m'a demandé si je pouvais garder la trace des modifications apportées aux enregistrements dans une base de données MySQL. Ainsi, lorsqu'un champ a été modifié, l'ancien et le nouveau sont disponibles, ainsi que la date à laquelle cela s'est produit. Existe-t-il une fonctionnalité ou une technique commune pour faire cela ?

Si c'est le cas, je pensais faire quelque chose comme ça. Créer une table appelée changes . Il contiendrait les mêmes champs que le maître mais préfixée par les termes "ancien" et "nouveau", mais uniquement pour les champs qui ont été effectivement modifiés et un TIMESTAMP pour ça. Il serait indexé avec un ID . De cette façon, un SELECT pourrait être exécuté pour montrer l'historique de chaque enregistrement. Est-ce une bonne méthode ? Merci !

221voto

transient closure Points 2331

Voici un moyen simple de le faire :

Tout d'abord, créez un tableau d'historique pour chaque tableau de données que vous souhaitez suivre (exemple de requête ci-dessous). Ce tableau contiendra une entrée pour chaque requête d'insertion, de mise à jour et de suppression effectuée sur chaque ligne de la table de données.

La structure de la table d'historique sera la même que celle de la table de données qu'elle suit, à l'exception de trois colonnes supplémentaires : une colonne pour stocker l'opération qui s'est produite (appelons-la "action"), la date et l'heure de l'opération, et une colonne pour stocker un numéro de séquence ("révision"), qui s'incrémente pour chaque opération et est groupé par la colonne clé primaire de la table de données.

Pour réaliser ce comportement de séquençage, un index (composite) à deux colonnes est créé sur la colonne de clé primaire et la colonne de révision. Notez que vous ne pouvez faire le séquençage de cette manière que si le moteur utilisé par la table historique est MyISAM ( Voir la section "Notes sur MyISAM" sur cette page.)

La table d'historique est assez facile à créer. Dans la requête ALTER TABLE ci-dessous (et dans les requêtes trigger qui suivent), remplacez 'primary_key_column' par le nom réel de cette colonne dans votre table de données.

CREATE TABLE MyDB.data_history LIKE MyDB.data;

ALTER TABLE MyDB.data_history MODIFY COLUMN primary_key_column int(11) NOT NULL, 
   DROP PRIMARY KEY, ENGINE = MyISAM, ADD action VARCHAR(8) DEFAULT 'insert' FIRST, 
   ADD revision INT(6) NOT NULL AUTO_INCREMENT AFTER action,
   ADD dt_datetime DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP AFTER revision,
   ADD PRIMARY KEY (primary_key_column, revision);

Et ensuite vous créez les déclencheurs :

DROP TRIGGER IF EXISTS MyDB.data__ai;
DROP TRIGGER IF EXISTS MyDB.data__au;
DROP TRIGGER IF EXISTS MyDB.data__bd;

CREATE TRIGGER MyDB.data__ai AFTER INSERT ON MyDB.data FOR EACH ROW
    INSERT INTO MyDB.data_history SELECT 'insert', NULL, NOW(), d.* 
    FROM MyDB.data AS d WHERE d.primary_key_column = NEW.primary_key_column;

CREATE TRIGGER MyDB.data__au AFTER UPDATE ON MyDB.data FOR EACH ROW
    INSERT INTO MyDB.data_history SELECT 'update', NULL, NOW(), d.*
    FROM MyDB.data AS d WHERE d.primary_key_column = NEW.primary_key_column;

CREATE TRIGGER MyDB.data__bd BEFORE DELETE ON MyDB.data FOR EACH ROW
    INSERT INTO MyDB.data_history SELECT 'delete', NULL, NOW(), d.* 
    FROM MyDB.data AS d WHERE d.primary_key_column = OLD.primary_key_column;

Et vous avez terminé. Maintenant, tous les inserts, mises à jour et suppressions dans 'MyDb.data' seront enregistrés dans 'MyDb.data_history', ce qui vous donne un tableau historique comme celui-ci (sans la colonne 'data_columns')

ID    revision   action    data columns..
1     1         'insert'   ....          initial entry for row where ID = 1
1     2         'update'   ....          changes made to row where ID = 1
2     1         'insert'   ....          initial entry, ID = 2
3     1         'insert'   ....          initial entry, ID = 3 
1     3         'update'   ....          more changes made to row where ID = 1
3     2         'update'   ....          changes made to row where ID = 3
2     2         'delete'   ....          deletion of row where ID = 2 

Pour afficher les changements pour une ou plusieurs colonnes données d'une mise à jour à l'autre, vous devez joindre la table d'historique à elle-même sur la clé primaire et les colonnes de séquence. Vous pouvez créer une vue à cette fin, par exemple :

CREATE VIEW data_history_changes AS 
   SELECT t2.dt_datetime, t2.action, t1.primary_key_column as 'row id', 
   IF(t1.a_column = t2.a_column, t1.a_column, CONCAT(t1.a_column, " to ", t2.a_column)) as a_column
   FROM MyDB.data_history as t1 INNER join MyDB.data_history as t2 on t1.primary_key_column = t2.primary_key_column 
   WHERE (t1.revision = 1 AND t2.revision = 1) OR t2.revision = t1.revision+1
   ORDER BY t1.primary_key_column ASC, t2.revision ASC

Edit : Oh wow, les gens aiment mon tableau d'histoire d'il y a 6 ans :P

Ma mise en œuvre de ce système est toujours en cours, et devient de plus en plus lourde, je suppose. J'ai écrit des vues et une interface utilisateur assez sympa pour consulter l'historique de cette base de données, mais je ne pense pas qu'elle ait été beaucoup utilisée. C'est ainsi.

Pour répondre à certains commentaires, sans ordre particulier :

  • J'ai réalisé ma propre implémentation en PHP, un peu plus complexe, et j'ai évité certains des problèmes décrits dans les commentaires (le transfert d'index, de manière significative). Si vous transférez des index uniques à la table d'historique, les choses vont se casser. Il y a des solutions pour cela dans les commentaires). Suivre ce post à la lettre pourrait être une aventure, en fonction du degré d'établissement de votre base de données.

  • Si la relation entre la clé primaire et la colonne de révision ne semble pas exister, cela signifie généralement que la clé composite est défectueuse. En de rares occasions, cela s'est produit et je n'en connaissais pas la cause.

  • J'ai trouvé cette solution assez performante, en utilisant des triggers comme elle le fait. De plus, MyISAM est rapide pour les insertions, ce qui est tout ce que font les triggers. Vous pouvez encore améliorer cela avec une indexation intelligente (ou l'absence d'indexation...). L'insertion d'une seule ligne dans une table MyISAM avec une clé primaire ne devrait pas être une opération que vous devez optimiser, vraiment, à moins que vous ayez des problèmes importants ailleurs. Pendant toute la période où j'ai exploité la base de données MySQL, l'implémentation de la table d'historique n'a jamais été la cause d'un des (nombreux) problèmes de performance qui se sont présentés.

  • si vous obtenez des insertions répétées, vérifiez votre couche logicielle pour les requêtes de type INSERT IGNORE. Hrmm, je ne me souviens plus maintenant, mais je pense qu'il y a des problèmes avec ce schéma et les transactions qui échouent finalement après l'exécution de multiples actions DML. Il faut en être conscient, en tout cas.

  • Il est important que les champs de la table d'historique et de la table de données correspondent. Ou, plutôt, que votre table de données n'ait pas PLUS de colonnes que la table d'historique. Sinon, les requêtes d'insertion/mise à jour/suppression sur la table de données échoueront, lorsque les insertions dans les tables d'historique mettront des colonnes dans la requête qui n'existent pas (à cause de d.* dans les requêtes de déclenchement), et le déclenchement échouera. Ce serait génial si MySQL avait quelque chose comme les déclencheurs de schéma, où vous pourriez modifier la table d'historique si des colonnes sont ajoutées à la table de données. Est-ce que MySQL a cela maintenant ? Je fais du React ces jours-ci :P

96voto

Neville K Points 13666

C'est subtil.

Si le besoin de l'entreprise est "Je veux auditer les modifications apportées aux données - qui a fait quoi et quand ?", vous pouvez généralement utiliser des tables d'audit (comme dans l'exemple de trigger que Keethanjan a posté). Je ne suis pas un grand fan des triggers, mais cela a le grand avantage d'être relativement facile à mettre en œuvre - votre code existant n'a pas besoin de connaître les triggers et les trucs d'audit.

Si le besoin de l'entreprise est "montrez-moi quel était l'état des données à une date donnée dans le passé", cela signifie que l'aspect du changement dans le temps est entré dans votre solution. Bien que vous puissiez, à peu près, reconstruire l'état de la base de données simplement en regardant les tables d'audit, c'est difficile et source d'erreurs, et pour toute logique de base de données compliquée, cela devient lourd. Par exemple, si l'entreprise souhaite "trouver les adresses des lettres que nous aurions dû envoyer aux clients dont les factures étaient impayées le premier jour du mois", vous devrez probablement parcourir une demi-douzaine de tables d'audit.

Au lieu de cela, vous pouvez intégrer le concept de changement dans le temps dans la conception de votre schéma (c'est la deuxième option suggérée par Keethanjan). Il s'agit d'une modification de votre application, notamment au niveau de la logique métier et de la persistance, ce qui n'est pas une mince affaire.

Par exemple, si vous avez un tableau comme celui-ci :

CUSTOMER
---------
CUSTOMER_ID PK
CUSTOMER_NAME
CUSTOMER_ADDRESS

et que vous vouliez garder une trace dans le temps, vous le modifieriez comme suit :

CUSTOMER
------------
CUSTOMER_ID            PK
CUSTOMER_VALID_FROM    PK
CUSTOMER_VALID_UNTIL   PK
CUSTOMER_STATUS
CUSTOMER_USER
CUSTOMER_NAME
CUSTOMER_ADDRESS

Chaque fois que vous voulez modifier un enregistrement client, au lieu de mettre à jour l'enregistrement, vous définissez le VALID_UNTIL de l'enregistrement actuel sur NOW(), et vous insérez un nouvel enregistrement avec un VALID_FROM (now) et un VALID_UNTIL nul. Vous définissez le statut "CUSTOMER_USER" sur l'ID de connexion de l'utilisateur actuel (si vous devez le conserver). Si le client doit être supprimé, vous utilisez l'indicateur CUSTOMER_STATUS pour l'indiquer - vous ne pouvez jamais supprimer des enregistrements de cette table.

De cette façon, vous pouvez toujours trouver quel était le statut de la table des clients pour une date donnée - quelle était l'adresse ? Ont-ils changé de nom ? En vous joignant à d'autres tables ayant des dates de début et de fin de validité similaires, vous pouvez reconstituer l'ensemble du tableau historique. Pour trouver le statut actuel, vous recherchez les enregistrements dont la date VALID_UNTIL est nulle.

C'est lourd (à proprement parler, vous n'avez pas besoin de valid_from, mais cela facilite un peu les requêtes). Cela complique votre conception et l'accès à votre base de données. Mais cela rend la reconstruction du monde beaucoup plus facile.

17voto

Keethanjan Points 335

Vous pourriez créer des déclencheurs pour résoudre ce problème. Voici un tutoriel pour le faire (lien archivé).

Il est préférable de définir des contraintes et des règles dans la base de données plutôt que d'écrire des fichiers l'écriture d'un code spécial pour gérer la même tâche, car cela empêchera une autre développeur d'écrire une requête différente qui contourne tout le code code spécial et qui pourrait laisser votre base de données avec une mauvaise intégrité des données.

Pendant longtemps, j'ai copié des informations vers une autre table en utilisant un script. puisque MySQL ne prenait pas en charge les triggers à l'époque. J'ai maintenant trouvé que ce déclencheur pour être plus efficace pour garder une trace de tout.

Ce déclencheur copiera une ancienne valeur dans une table d'historique si elle est modifiée. lorsque quelqu'un édite une ligne. Editor ID y last mod sont stockées dans le d'origine à chaque fois que quelqu'un édite cette ligne ; l'heure correspond au moment au moment où elle a été modifiée dans sa forme actuelle.

DROP TRIGGER IF EXISTS history_trigger $$

CREATE TRIGGER history_trigger
BEFORE UPDATE ON clients
    FOR EACH ROW
    BEGIN
        IF OLD.first_name != NEW.first_name
        THEN
                INSERT INTO history_clients
                    (
                        client_id    ,
                        col          ,
                        value        ,
                        user_id      ,
                        edit_time
                    )
                    VALUES
                    (
                        NEW.client_id,
                        'first_name',
                        NEW.first_name,
                        NEW.editor_id,
                        NEW.last_mod
                    );
        END IF;

        IF OLD.last_name != NEW.last_name
        THEN
                INSERT INTO history_clients
                    (
                        client_id    ,
                        col          ,
                        value        ,
                        user_id      ,
                        edit_time
                    )
                    VALUES
                    (
                        NEW.client_id,
                        'last_name',
                        NEW.last_name,
                        NEW.editor_id,
                        NEW.last_mod
                    );
        END IF;

    END;
$$

Une autre solution serait de conserver un champ Révision et de mettre à jour ce champ lors de la sauvegarde. Vous pourriez décider que le maximum est la révision la plus récente, ou que 0 est la ligne la plus récente. C'est vous qui décidez.

11voto

Zenex Points 131

Voici comment nous l'avons résolu

un tableau des utilisateurs ressemblait à ceci

Users
-------------------------------------------------
id | name | address | phone | email | created_on | updated_on

Les besoins de l'entreprise ont changé et nous avons dû vérifier toutes les adresses et tous les numéros de téléphone précédents d'un utilisateur. Le nouveau schéma ressemble à ceci

Users (the data that won't change over time)
-------------
id | name

UserData (the data that can change over time and needs to be tracked)
-------------------------------------------------
id | id_user | revision | city | address | phone | email | created_on
 1 |   1     |    0     | NY   | lake st | 9809  | @long | 2015-10-24 10:24:20
 2 |   1     |    2     | Tokyo| lake st | 9809  | @long | 2015-10-24 10:24:20
 3 |   1     |    3     | Sdny | lake st | 9809  | @long | 2015-10-24 10:24:20
 4 |   2     |    0     | Ankr | lake st | 9809  | @long | 2015-10-24 10:24:20
 5 |   2     |    1     | Lond | lake st | 9809  | @long | 2015-10-24 10:24:20

Pour trouver l'adresse actuelle de n'importe quel utilisateur, nous recherchons UserData avec révision DESC et LIMIT 1

Pour obtenir l'adresse d'un utilisateur entre une certaine période de temps nous pouvons utiliser created_on bewteen (date1 , date 2)

9voto

midenok Points 761

MariaDB supporte le System Versioning depuis 10.3. Il s'agit d'une fonctionnalité SQL standard qui fait exactement ce que vous voulez : elle stocke l'historique des enregistrements de la table et permet d'y accéder par le biais de SELECT des requêtes. MariaDB est un développement ouvert de MySQL. Vous pouvez trouver plus d'informations sur son System Versioning via ce lien :

https://mariadb.com/kb/en/library/system-versioned-tables/

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X