Comment contrôler la version d'un enregistrement dans une base de données

177

Disons que j'ai un enregistrement dans la base de données et que les administrateurs et les utilisateurs normaux peuvent faire des mises à jour.

Quelqu'un peut-il suggérer une bonne approche / architecture pour contrôler la version de chaque changement dans ce tableau afin qu'il soit possible de restaurer un enregistrement à une révision précédente.

Niels Bosma
la source

Réponses:

164

Supposons que vous ayez une FOOtable que les administrateurs et les utilisateurs peuvent mettre à jour. La plupart du temps, vous pouvez écrire des requêtes sur la table FOO. Jours heureux.

Ensuite, je créerais une FOO_HISTORYtable. Cela a toutes les colonnes du FOOtableau. La clé primaire est la même que FOO plus une colonne RevisionNumber. Il existe une clé étrangère de FOO_HISTORYà FOO. Vous pouvez également ajouter des colonnes liées à la révision, telles que UserId et RevisionDate. Remplissez les RevisionNumbers de manière toujours croissante dans toutes les *_HISTORYtables (c'est-à-dire à partir d'une séquence Oracle ou équivalent). Ne vous fiez pas au fait qu'il n'y ait qu'un seul changement en une seconde (c'est-à-dire ne pas mettre RevisionDatedans la clé primaire).

Maintenant, chaque fois que vous mettez à jour FOO, juste avant de faire la mise à jour, vous insérez les anciennes valeurs dans FOO_HISTORY. Vous faites cela à un niveau fondamental de votre conception afin que les programmeurs ne puissent pas accidentellement manquer cette étape.

Si vous souhaitez supprimer une ligne, FOOvous avez le choix. Soit mettre en cascade et supprimer tout l'historique, soit effectuer une suppression logique en marquant FOOcomme supprimé.

Cette solution est bonne lorsque vous vous intéressez largement aux valeurs actuelles et seulement occasionnellement à l'histoire. Si vous avez toujours besoin de l'historique, vous pouvez définir des dates de début et de fin effectives et conserver tous les enregistrements en FOOsoi. Chaque requête doit ensuite vérifier ces dates.

WW.
la source
1
Vous pouvez effectuer la mise à jour de la table d'audit avec des déclencheurs de base de données si votre couche d'accès aux données ne la prend pas directement en charge. En outre, il n'est pas difficile de créer un générateur de code pour créer les déclencheurs qui utilisent l'introspection à partir du dictionnaire de données système.
ConcernedOfTunbridgeWells
44
Je vous recommande d'insérer réellement les nouvelles données, pas les précédentes, afin que la table d'historique contienne toutes les données. Bien qu'il stocke des données redyondantes, il élimine les cas particuliers nécessaires pour traiter la recherche dans les deux tables lorsque des données historiques sont nécessaires.
Nerdfest
6
Personnellement, je recommanderais de ne rien supprimer (reporter cela à une activité de ménage spécifique) et d'avoir une colonne "type d'action" pour spécifier s'il s'agit d'insérer / mettre à jour / supprimer. Pour une suppression, copiez la ligne normalement, mais mettez «supprimer» dans la colonne du type d'action.
Neil Barnwell
3
@Hydrargyrum Une table contenant les valeurs actuelles fonctionnera mieux qu'une vue de la table historique. Vous souhaiterez peut-être également définir des clés étrangères référençant les valeurs actuelles.
WW.
2
There is a foreign key from FOO_HISTORY to FOO': mauvaise idée, je voudrais supprimer des enregistrements de foo sans changer l'historique. la table d'historique doit être insérée uniquement en utilisation normale.
Jasen
46

Je pense que vous recherchez un versionnage du contenu des enregistrements de base de données (comme le fait StackOverflow lorsque quelqu'un modifie une question / réponse). Un bon point de départ pourrait être d'examiner un modèle de base de données qui utilise le suivi des révisions .

Le meilleur exemple qui me vient à l'esprit est MediaWiki, le moteur de Wikipédia. Comparez le diagramme de la base de données ici , en particulier la table de révision .

En fonction des technologies que vous utilisez, vous devrez trouver de bons algorithmes de comparaison / fusion.

Vérifiez cette question si c'est pour .NET.

CMS
la source
30

Dans le monde de la BI, vous pouvez accomplir cela en ajoutant une date de début et une date de fin à la table dont vous souhaitez effectuer la version. Lorsque vous insérez le premier enregistrement dans la table, startDate est renseigné, mais endDate est nul. Lorsque vous insérez le deuxième enregistrement, vous mettez également à jour la date de fin du premier enregistrement avec la date de début du deuxième enregistrement.

Lorsque vous souhaitez afficher l'enregistrement actuel, vous sélectionnez celui où endDate est nul.

Ceci est parfois appelé une dimension à changement lent de type 2 . Voir aussi TupleVersioning

Dave Neeley
la source
Ma table ne deviendra-t-elle pas assez grande en utilisant cette approche?
Niels Bosma
1
Oui, mais vous pouvez gérer cela en indexant et / ou en partitionnant la table. De plus, il n'y aura qu'une petite poignée de grandes tables. La plupart seront beaucoup plus petits.
ConcernedOfTunbridgeWells
Si je ne me trompe pas, le seul inconvénient est qu'il limite les changements à une fois par seconde, n'est-ce pas?
pimbrouwers
@pimbrouwers oui, cela dépend finalement de la précision des champs et de la fonction qui les remplit.
Dave Neeley
9

Mettez à niveau vers SQL 2008.

Essayez d'utiliser le suivi des modifications SQL, dans SQL 2008. Au lieu des hacks d'horodatage et de colonne tombstone, vous pouvez utiliser cette nouvelle fonctionnalité pour suivre les modifications sur les données de votre base de données.

Suivi des modifications MSDN SQL 2008

D3vtr0n
la source
7

Je voulais juste ajouter qu'une bonne solution à ce problème est d'utiliser une base de données temporelle . De nombreux fournisseurs de bases de données proposent cette fonctionnalité soit prête à l'emploi, soit via une extension. J'ai utilisé avec succès l' extension de table temporelle avec PostgreSQL mais d'autres l'ont aussi. Chaque fois que vous mettez à jour un enregistrement dans la base de données, la base de données conserve également la version précédente de cet enregistrement.

wuher
la source
6

Deux options:

  1. Avoir une table d'historique - insérez les anciennes données dans cette table d'historique chaque fois que l'original est mis à jour.
  2. Table d'audit - stockez les valeurs avant et après - uniquement pour les colonnes modifiées dans une table d'audit avec d'autres informations telles que qui a mis à jour et quand.
alok
la source
5

Vous pouvez effectuer un audit sur une table SQL via des déclencheurs SQL. À partir d'un déclencheur, vous pouvez accéder à 2 tables spéciales ( insérées et supprimées ). Ces tables contiennent les lignes exactes qui ont été insérées ou supprimées chaque fois que la table est mise à jour. Dans le déclencheur SQL, vous pouvez prendre ces lignes modifiées et les insérer dans la table d'audit. Cette approche signifie que votre audit est transparent pour le programmeur; ne nécessitant aucun effort de leur part ni aucune connaissance de mise en œuvre.

L'avantage supplémentaire de cette approche est que l'audit se produira indépendamment du fait que l'opération SQL ait eu lieu via vos DLL d'accès aux données ou via une requête SQL manuelle; (car l'audit est effectué sur le serveur lui-même).

Docteur Jones
la source
3

Vous ne dites pas quelle base de données, et je ne la vois pas dans les balises de publication. Si c'est pour Oracle, je peux recommander l'approche intégrée à Designer: utiliser des tables de journal . Si c'est pour une autre base de données, eh bien, je recommande essentiellement la même manière aussi ...

La façon dont cela fonctionne, au cas où vous voudriez le répliquer dans une autre base de données, ou peut-être si vous voulez simplement le comprendre, est que pour une table, il y a aussi une table d'ombre créée, juste une table de base de données normale, avec les mêmes spécifications de champ , plus quelques champs supplémentaires: comme quelle action a été effectuée pour la dernière fois (chaîne, valeurs typiques «INS» pour l'insertion, «UPD» pour la mise à jour et «DEL» pour la suppression), date / heure pour laquelle l'action a eu lieu et identifiant de l'utilisateur pour qui l'a fait il.

Grâce aux déclencheurs, chaque action sur une ligne de la table insère une nouvelle ligne dans la table de journal avec les nouvelles valeurs, quelle action a été entreprise, quand et par quel utilisateur. Vous ne supprimez jamais aucune ligne (du moins pas au cours des derniers mois). Oui, cela va grossir, facilement des millions de lignes, mais vous pouvez facilement suivre la valeur de n'importe quel enregistrement à tout moment depuis le début de la journalisation ou la dernière purge des anciennes lignes du journal, et qui a effectué la dernière modification.

Dans Oracle, tout ce dont vous avez besoin est généré automatiquement sous forme de code SQL, tout ce que vous avez à faire est de le compiler / l'exécuter; et il est livré avec une application CRUD de base (en fait seulement "R") pour l'inspecter.

bart
la source
2

Je fais aussi la même chose. Je crée une base de données pour les plans de cours. Ces plans nécessitent une flexibilité de gestion des versions de changement atomique. En d'autres termes, chaque changement, aussi petit soit-il, dans les plans de cours doit être autorisé, mais l'ancienne version doit également être conservée intacte. De cette façon, les créateurs de cours peuvent modifier les plans de cours pendant que les élèves les utilisent.

La façon dont cela fonctionnerait est qu'une fois qu'un élève a suivi une leçon, ses résultats sont attachés à la version qu'il a terminée. Si une modification est apportée, leurs résultats pointeront toujours vers leur version.

De cette façon, si un critère de cours est supprimé ou déplacé, ses résultats ne changeront pas.

La manière dont je fais actuellement ceci est en manipulant toutes les données dans une table. Normalement, j'aurais juste un champ id, mais avec ce système, j'utilise un id et un sub_id. Le sub_id reste toujours avec la ligne, à travers les mises à jour et les suppressions. L'identifiant est auto-incrémenté. Le logiciel du plan de cours sera lié au plus récent sous_id. Les résultats des élèves seront liés à l'ID. J'ai également inclus un horodatage pour le suivi des changements, mais il n'est pas nécessaire de gérer le contrôle de version.

Une chose que je pourrais changer, une fois que je l'ai testé, est que je pourrais utiliser l'idée nulle endDate mentionnée précédemment. Dans mon système, pour trouver la dernière version, je devrais trouver le max (id). L'autre système recherche simplement endDate = null. Je ne sais pas si les avantages ont un autre champ de date.

Mes deux centimes.

Jordan
la source
2

Alors que @WW. La réponse est une bonne réponse, une autre manière est de créer une colonne de version et de conserver toutes vos versions dans le même tableau.

Pour une approche de table, vous pouvez soit:

  • Utilisez un drapeau pour indiquer le dernier ala Word Press
  • OU faire une version méchante supérieure à outer join.

Un exemple de SQL de la outer joinméthode utilisant des numéros de révision est:

SELECT tc.*
FROM text_content tc
LEFT OUTER JOIN text_content mc ON tc.path = mc.path
AND mc.revision > tc.revision
WHERE mc.revision is NULL 
AND tc.path = '/stuff' -- path in this case is our natural id.

La mauvaise nouvelle est que ce qui précède nécessite un outer joinet les jointures externes peuvent être lentes. La bonne nouvelle est que la création de nouvelles entrées est théoriquement moins chère car vous pouvez le faire en une seule opération d'écriture sans transactions (en supposant que votre base de données soit atomique).

Un exemple de création d'une nouvelle révision pour '/stuff'pourrait être:

INSERT INTO text_content (id, path, data, revision, revision_comment, enabled, create_time, update_time)
(
SELECT
(md5(random()::text)) -- {id}
, tc.path
, 'NEW' -- {data}
, (tc.revision + 1)
, 'UPDATE' -- {comment}
, 't' -- {enabled}
, tc.create_time
, now() 
FROM text_content tc
LEFT OUTER JOIN text_content mc ON tc.path = mc.path
AND mc.revision > tc.revision
WHERE mc.revision is NULL 
AND tc.path = '/stuff' -- {path}
)

Nous insérons en utilisant les anciennes données. Ceci est particulièrement utile si vous ne souhaitez mettre à jour qu'une seule colonne et éviter le verrouillage optimiste et / ou les transactions.

L'approche par indicateur et l'approche par table d'historique nécessitent l'insertion / la mise à jour de deux lignes.

L'autre avantage de l' outer joinapproche du numéro de révision est que vous pouvez toujours refactoriser ultérieurement l'approche à plusieurs tables avec des déclencheurs, car votre déclencheur doit essentiellement faire quelque chose comme ci-dessus.

Adam Gent
la source
2

Alok suggéré Audit tableci-dessus, je voudrais l'expliquer dans mon post.

J'ai adopté cette conception de table unique sans schéma sur mon projet.

Schéma:

  • id - INCREMENT AUTO ENTIER
  • nom d'utilisateur - STRING
  • nom de la table - STRING
  • oldvalue - TEXT / JSON
  • newvalue - TEXT / JSON
  • créé le - DATETIME

Cette table peut contenir des enregistrements historiques pour chaque table en un seul endroit, avec l'historique complet des objets dans un enregistrement. Cette table peut être remplie à l'aide de déclencheurs / hooks où les données changent, en stockant l'ancien et le nouveau instantané de valeur de la ligne cible.

Avantages avec cette conception:

  • Moins de tables à gérer pour la gestion de l'historique.
  • Stocke un instantané complet de chaque état ancien et nouveau de ligne.
  • Recherche facile sur chaque table.
  • Peut créer une partition par table.
  • Peut définir une politique de conservation des données par table.

Contre cette conception:

  • La taille des données peut être importante si le système a des changements fréquents.
Hassan Farid
la source