Question

Nous avons l'obligation dans le projet de stocker toutes les révisions (historique des modifications) des entités dans la base de données.Actuellement, nous avons 2 propositions conçues pour cela :

par exemple.pour l'entité "Employé"

Conception 1 :

-- Holds Employee Entity
"Employees (EmployeeId, FirstName, LastName, DepartmentId, .., ..)"

-- Holds the Employee Revisions in Xml. The RevisionXML will contain
-- all data of that particular EmployeeId
"EmployeeHistories (EmployeeId, DateModified, RevisionXML)"

Conception 2 :

-- Holds Employee Entity
"Employees (EmployeeId, FirstName, LastName, DepartmentId, .., ..)"

-- In this approach we have basically duplicated all the fields on Employees 
-- in the EmployeeHistories and storing the revision data.
"EmployeeHistories (EmployeeId, RevisionId, DateModified, FirstName, 
      LastName, DepartmentId, .., ..)"

Existe-t-il une autre façon de procéder ?

Le problème avec le "Design 1" est que nous devons analyser XML à chaque fois que nous avons besoin d'accéder à des données.Cela ralentira le processus et ajoutera également certaines limitations telles que nous ne pouvons pas ajouter de jointures sur les champs de données de révision.

Et le problème avec le "Design 2" est que nous devons dupliquer chaque champ sur toutes les entités (nous avons environ 70 à 80 entités pour lesquelles nous souhaitons conserver les révisions).

Était-ce utile?

La solution

  1. Faire pas mettez le tout dans une seule table avec un attribut discriminateur IsCurrent.Cela ne fait que causer des problèmes sur toute la ligne, nécessite des clés de substitution et toutes sortes d'autres problèmes.
  2. La conception 2 a des problèmes avec les changements de schéma.Si vous modifiez la table Employees, vous devez modifier la table EmployeeHistories et toutes les procédures associées qui l'accompagnent.Double potentiellement votre effort de changement de schéma.
  3. La conception 1 fonctionne bien et si elle est effectuée correctement, elle ne coûte pas cher en termes de performances.Vous pouvez utiliser un schéma XML et même des index pour résoudre d'éventuels problèmes de performances.Votre commentaire sur l'analyse du XML est valide, mais vous pouvez facilement créer une vue à l'aide de xquery - que vous pouvez inclure dans les requêtes et y rejoindre.Quelque chose comme ça...
CREATE VIEW EmployeeHistory
AS
, FirstName, , DepartmentId

SELECT EmployeeId, RevisionXML.value('(/employee/FirstName)[1]', 'varchar(50)') AS FirstName,

  RevisionXML.value('(/employee/LastName)[1]', 'varchar(100)') AS LastName,

  RevisionXML.value('(/employee/DepartmentId)[1]', 'integer') AS DepartmentId,

FROM EmployeeHistories 

Autres conseils

Je pense que la question clé à poser ici est « Qui/Qu'est-ce qui va utiliser l'historique » ?

S'il s'agit principalement de rapports/d'un historique lisible par l'homme, nous avons mis en œuvre ce système dans le passé...

Créez une table appelée « AuditTrail » ou quelque chose qui contient les champs suivants...

[ID] [int] IDENTITY(1,1) NOT NULL,
[UserID] [int] NULL,
[EventDate] [datetime] NOT NULL,
[TableName] [varchar](50) NOT NULL,
[RecordID] [varchar](20) NOT NULL,
[FieldName] [varchar](50) NULL,
[OldValue] [varchar](5000) NULL,
[NewValue] [varchar](5000) NULL

Vous pouvez ensuite ajouter une colonne 'LastUpdatedByUserID' à toutes vos tables qui doit être définie à chaque fois que vous effectuez une mise à jour/insertion sur la table.

Vous pouvez ensuite ajouter un déclencheur à chaque table pour détecter toute insertion/mise à jour qui se produit et créer une entrée dans cette table pour chaque champ modifié.Étant donné que la table est également fournie avec le « LastUpdateByUserID » pour chaque mise à jour/insertion, vous pouvez accéder à cette valeur dans le déclencheur et l'utiliser lors de l'ajout à la table d'audit.

Nous utilisons le champ RecordID pour stocker la valeur du champ clé de la table en cours de mise à jour.S'il s'agit d'une clé combinée, nous effectuons simplement une concaténation de chaînes avec un '~' entre les champs.

Je suis sûr que ce système peut présenter des inconvénients : pour les bases de données fortement mises à jour, les performances peuvent être affectées, mais pour mon application Web, nous obtenons beaucoup plus de lectures que d'écritures et cela semble plutôt bien fonctionner.Nous avons même écrit un petit utilitaire VB.NET pour écrire automatiquement les déclencheurs en fonction des définitions des tables.

Juste une pensée!

Le Tableaux d'historique article dans le Programmeur de base de données le blog pourrait être utile - couvre certains des points soulevés ici et discute du stockage des deltas.

Modifier

Dans le Tableaux d'historique essai, l'auteur (Kenneth Downs), recommande de conserver une table d'historique d'au moins sept colonnes :

  1. Horodatage du changement,
  2. Utilisateur qui a effectué la modification,
  3. Un jeton pour identifier l'enregistrement qui a été modifié (où l'historique est conservé séparément de l'état actuel),
  4. Que la modification soit une insertion, une mise à jour ou une suppression,
  5. L'ancienne valeur,
  6. La nouvelle valeur,
  7. Le delta (pour les modifications des valeurs numériques).

Les colonnes qui ne changent jamais ou dont l'historique n'est pas requis ne doivent pas être suivies dans la table d'historique pour éviter toute surcharge.Le stockage du delta pour les valeurs numériques peut faciliter les requêtes ultérieures, même s'il peut être dérivé des anciennes et des nouvelles valeurs.

La table d'historique doit être sécurisée, les utilisateurs non-système ne pouvant pas insérer, mettre à jour ou supprimer des lignes.Seule une purge périodique doit être prise en charge pour réduire la taille globale (et si le cas d'utilisation le permet).

Nous avons mis en œuvre une solution très similaire à celle suggérée par Chris Roberts, et qui fonctionne plutôt bien pour nous.

La seule différence est que nous stockons uniquement la nouvelle valeur.L'ancienne valeur est après tout stockée dans la ligne précédente de l'historique

[ID] [int] IDENTITY(1,1) NOT NULL,
[UserID] [int] NULL,
[EventDate] [datetime] NOT NULL,
[TableName] [varchar](50) NOT NULL,
[RecordID] [varchar](20) NOT NULL,
[FieldName] [varchar](50) NULL,
[NewValue] [varchar](5000) NULL

Disons que vous avez un tableau de 20 colonnes.De cette façon, vous n'avez qu'à stocker la colonne exacte qui a changé au lieu de devoir stocker la ligne entière.

Évitez la conception 1 ;ce n'est pas très pratique une fois que vous devrez par exemple restaurer les anciennes versions des enregistrements - soit automatiquement, soit "manuellement" à l'aide de la console des administrateurs.

Je ne vois pas vraiment les inconvénients du Design 2.Je pense que la seconde table Historique devrait contenir toutes les colonnes présentes dans la première table Records.Par exemple.dans MySQL, vous pouvez facilement créer une table avec la même structure qu'une autre table (create table X like Y).Et, lorsque vous êtes sur le point de modifier la structure de la table Records dans votre base de données en direct, vous devez utiliser alter table commandes de toute façon - et il n'y a pas de gros effort pour exécuter ces commandes également pour votre table d'historique.

Remarques

  • Le tableau des enregistrements contient uniquement la dernière révision ;
  • La table Historique contient toutes les révisions précédentes des enregistrements dans la table Enregistrements ;
  • La clé primaire de la table Historique est une clé primaire de la table Records avec RevisionId colonne;
  • Pensez aux champs auxiliaires supplémentaires comme ModifiedBy - l'utilisateur qui a créé une révision particulière.Vous souhaiterez peut-être également avoir un champ DeletedBy pour savoir qui a supprimé une révision particulière.
  • Penser à quoi DateModified devrait signifier - soit cela signifie où cette révision particulière a été créée, soit cela signifiera quand cette révision particulière a été remplacée par une autre.Le premier nécessite que le champ soit dans la table Records et semble plus intuitif à première vue ;la deuxième solution semble cependant plus pratique pour les enregistrements supprimés (date à laquelle cette révision particulière a été supprimée).Si vous optez pour la première solution, vous aurez probablement besoin d'un deuxième champ DateDeleted (uniquement si vous en avez besoin bien sûr).Cela dépend de vous et de ce que vous souhaitez réellement enregistrer.

Les opérations dans Design 2 sont très triviales :

Modifier
  • copiez l'enregistrement de la table Records vers la table History, donnez-lui un nouveau RevisionId (s'il n'est pas déjà présent dans la table Records), gérez DateModified (dépend de la façon dont vous l'interprétez, voir les notes ci-dessus)
  • poursuivre la mise à jour normale de l'enregistrement dans la table Records
Supprimer
  • faites exactement la même chose que lors de la première étape de l’opération Modifier.Gérez DateModified/DateDeleted en conséquence, en fonction de l'interprétation que vous avez choisie.
Annuler la suppression (ou restauration)
  • prendre la révision la plus élevée (ou une révision particulière ?) de la table Historique et la copier dans la table Records
Répertorier l'historique des révisions pour un enregistrement particulier
  • sélectionner dans la table Historique et la table Enregistrements
  • pensez à ce que vous attendez exactement de cette opération ;cela déterminera probablement les informations dont vous avez besoin dans les champs DateModified/DateDeleted (voir les notes ci-dessus)

Si vous optez pour Design 2, toutes les commandes SQL nécessaires pour ce faire seront très très simples, ainsi que la maintenance !Peut-être que ce sera beaucoup plus facile si vous utilisez les colonnes auxiliaires (RevisionId, DateModified) également dans la table Records - pour conserver les deux tables exactement dans la même structure (sauf pour les clés uniques) !Cela permettra d'utiliser des commandes SQL simples, qui seront tolérantes à tout changement de structure de données :

insert into EmployeeHistory select * from Employe where ID = XX

N'oubliez pas d'utiliser les transactions !

Quant à la mise à l'échelle, cette solution est très efficace, puisque vous ne transformez aucune donnée XML d'avant en arrière, vous copiez simplement des lignes entières de table - requêtes très simples, utilisant des index - très efficace !

Si vous devez stocker l'historique, créez une table fantôme avec le même schéma que la table que vous suivez et une colonne « Date de révision » et « Type de révision » (par ex.« supprimer », « mettre à jour »).Écrivez (ou générez - voir ci-dessous) un ensemble de déclencheurs pour remplir la table d'audit.

Il est assez simple de créer un outil qui lira le dictionnaire de données système d'une table et générera un script qui crée la table fantôme et un ensemble de déclencheurs pour la remplir.

N'essayez pas d'utiliser XML pour cela, le stockage XML est beaucoup moins efficace que le stockage de table de base de données natif utilisé par ce type de déclencheur.

Ramesh, j'ai participé au développement d'un système basé sur la première approche.
Il s'est avéré que le stockage des révisions au format XML entraîne une énorme croissance de la base de données et ralentit considérablement les choses.
Mon approche serait d'avoir une table par entité :

Employee (Id, Name, ... , IsActive)  

C'est actif est un signe de la dernière version

Si vous souhaitez associer des informations supplémentaires à des révisions, vous pouvez créer un tableau séparé contenant ces informations et les lier à des tables entités à l'aide de la relation PK FK.

De cette façon, vous pouvez stocker toutes les versions des employés dans une seule table.Avantages de cette approche :

  • Structure de base de données simple
  • Aucun conflit puisque la table devient uniquement en ajout
  • Vous pouvez revenir à la version précédente en modifiant simplement l'indicateur IsActive
  • Pas besoin de jointures pour obtenir l'historique des objets

Notez que vous devez autoriser que la clé primaire ne soit pas unique.

La façon dont j'ai vu cela se faire dans le passé est de

Employees (EmployeeId, DateModified, < Employee Fields > , boolean isCurrent );

Vous ne "mettez jamais à jour" cette table (sauf pour modifier la validité de isCurrent), insérez simplement de nouvelles lignes.Pour tout EmployeeId donné, une seule ligne peut avoir isCurrent == 1.

La complexité de la maintenance peut être masquée par des vues et des déclencheurs "au lieu de" (dans Oracle, je présume des choses similaires dans d'autres SGBDR), vous pouvez même accéder à des vues matérialisées si les tables sont trop grandes et ne peuvent pas être gérées par des index) .

Cette méthode est correcte, mais vous pouvez vous retrouver avec des requêtes complexes.

Personnellement, j'aime beaucoup votre façon de procéder Design 2, et c'est également ainsi que je l'ai fait dans le passé.C’est simple à comprendre, simple à mettre en œuvre et simple à entretenir.

Cela crée également très peu de surcharge pour la base de données et l'application, en particulier lors de l'exécution de requêtes de lecture, ce que vous ferez probablement 99 % du temps.

Il serait également assez facile d'automatiser la création des tables d'historique et des déclencheurs à maintenir (en supposant que cela se fasse via des déclencheurs).

Je vais partager avec vous ma conception et elle est différente de vos deux conceptions en ce sens qu'elle nécessite une table pour chaque type d'entité.J'ai trouvé que la meilleure façon de décrire n'importe quelle conception de base de données est via ERD, voici la mienne :

enter image description here

Dans cet exemple, nous avons une entité nommée employé. utilisateur la table contient les enregistrements de vos utilisateurs et entité et entité_révision sont deux tableaux qui contiennent l'historique des révisions pour tous les types d'entités que vous aurez dans votre système.Voici comment fonctionne cette conception :

Les deux domaines de entité_id et révision_id

Chaque entité de votre système aura son propre identifiant d'entité unique.Votre entité peut subir des révisions mais son entités_id restera le même.Vous devez conserver cet identifiant d'entité dans votre table des employés (en tant que clé étrangère).Vous devez également stocker le type de votre entité dans le entité tableau (par ex.'employé').Quant au revision_id, comme son nom l'indique, il garde une trace des révisions de votre entité.La meilleure façon que j'ai trouvée pour cela est d'utiliser le id_employé comme votre revision_id.Cela signifie que vous aurez des identifiants de révision en double pour différents types d'entités, mais ce n'est pas un régal pour moi (je ne suis pas sûr de votre cas).La seule remarque importante à faire est que la combinaison d'entity_id et revision_id doit être unique.

Il y a aussi un État champ à l'intérieur entité_révision tableau qui indiquait l’état de révision.Il peut avoir l'un des trois états suivants : latest, obsolete ou deleted (ne pas se fier à la date des révisions vous aide beaucoup à booster vos requêtes).

Une dernière remarque sur revision_id, je n'ai pas créé de clé étrangère connectant Employee_id à revision_id car nous ne voulons pas modifier la table Entity_revision pour chaque type d'entité que nous pourrions ajouter à l'avenir.

INSERTION

Pour chaque employé que vous souhaitez insérer dans la base de données, vous ajouterez également un enregistrement à entité et entité_révision.Ces deux derniers enregistrements vous aideront à savoir par qui et quand un enregistrement a été inséré dans la base de données.

MISE À JOUR

Chaque mise à jour d'un enregistrement d'employé existant sera implémentée sous forme de deux insertions, une dans la table des employés et une dans Entity_revision.Le second vous aidera à savoir par qui et quand le dossier a été mis à jour.

EFFACEMENT

Pour supprimer un employé, un enregistrement est inséré dansentity_revision indiquant la suppression et terminé.

Comme vous pouvez le voir dans cette conception, aucune donnée n'est jamais modifiée ou supprimée de la base de données et, plus important encore, chaque type d'entité ne nécessite qu'une seule table.Personnellement, je trouve ce design vraiment flexible et facile à utiliser.Mais je ne suis pas sûr pour vous car vos besoins pourraient être différents.

[MISE À JOUR]

Ayant pris en charge les partitions dans les nouvelles versions de MySQL, je pense que ma conception offre également l'une des meilleures performances.On peut partitionner entity table utilisant type champ pendant la partition entity_revision en utilisant son state champ.Cela stimulera le SELECT requêtes de loin tout en gardant la conception simple et propre.

Si effectivement une piste d'audit est tout ce dont vous avez besoin, je pencherais pour la solution de table d'audit (avec des copies dénormalisées de la colonne importante sur d'autres tables, par exemple, UserName).Gardez toutefois à l’esprit que cette expérience amère indique qu’une seule table d’audit constituera un énorme goulot d’étranglement à l’avenir ;cela vaut probablement la peine de créer des tables d'audit individuelles pour toutes vos tables auditées.

Si vous devez suivre les versions historiques (et/ou futures) réelles, la solution standard consiste à suivre la même entité avec plusieurs lignes en utilisant une combinaison de valeurs de début, de fin et de durée.Vous pouvez utiliser une vue pour faciliter l’accès aux valeurs actuelles.Si telle est l'approche que vous adoptez, vous pouvez rencontrer des problèmes si vos données versionnées font référence à des données mutables mais non versionnées.

Les révisions des données sont un aspect du 'heure de validité' concept de base de données temporelle.De nombreuses recherches ont été consacrées à ce sujet et de nombreux modèles et lignes directrices ont émergé.J'ai écrit une longue réponse avec un tas de références à ce question pour ceux que ça intéresse.

Si vous souhaitez effectuer le premier, vous souhaiterez peut-être également utiliser XML pour la table Employés.La plupart des bases de données les plus récentes vous permettent d'interroger des champs XML, ce qui ne pose donc pas toujours un problème.Et il pourrait être plus simple de disposer d'un seul moyen d'accéder aux données des employés, qu'il s'agisse de la dernière version ou d'une version antérieure.

J'essaierais cependant la deuxième approche.Vous pouvez simplifier cela en n'ayant qu'une seule table Employees avec un champ DateModified.EmployeeId + DateModified serait la clé primaire et vous pouvez stocker une nouvelle révision en ajoutant simplement une ligne.De cette façon, l'archivage des anciennes versions et la restauration des versions à partir des archives sont également plus faciles.

Une autre façon de procéder pourrait être de modèle de coffre de données par Dan Linstedt.J'ai réalisé un projet pour le bureau néerlandais des statistiques qui utilisait ce modèle et cela fonctionne plutôt bien.Mais je ne pense pas que ce soit directement utile pour une utilisation quotidienne des bases de données.Cependant, vous pourriez avoir quelques idées en lisant ses articles.

Que diriez-vous:

  • ID employé
  • Date modifiée
    • et/ou numéro de révision, selon la façon dont vous souhaitez le suivre
  • ModifiéParUSerId
    • ainsi que toute autre information que vous souhaitez suivre
  • Champs des employés

Vous créez la clé primaire (EmployeeId, DateModified) et pour obtenir le ou les enregistrements "actuels", il vous suffit de sélectionner MAX (DateModified) pour chaque EmployeeID.Stocker un IsCurrent est une très mauvaise idée, car premièrement, il peut être calculé, et deuxièmement, il est beaucoup trop facile pour les données de se désynchroniser.

Vous pouvez également créer une vue répertoriant uniquement les derniers enregistrements et l'utiliser principalement lorsque vous travaillez dans votre application.L'avantage de cette approche est que vous n'avez pas de doublons de données et que vous n'avez pas besoin de collecter des données à partir de deux endroits différents (actuelles dans Employees et archivées dans EmployeesHistory) pour obtenir tout l'historique ou la restauration, etc.) .

Si vous souhaitez vous appuyer sur les données historiques (pour des raisons de reporting), vous devez utiliser une structure comme celle-ci :

// Holds Employee Entity
"Employees (EmployeeId, FirstName, LastName, DepartmentId, .., ..)"

// Holds the Employee revisions in rows.
"EmployeeHistories (HistoryId, EmployeeId, DateModified, OldValue, NewValue, FieldName)"

Ou solution globale d’application :

// Holds Employee Entity
"Employees (EmployeeId, FirstName, LastName, DepartmentId, .., ..)"

// Holds all entities revisions in rows.
"EntityChanges (EntityName, EntityId, DateModified, OldValue, NewValue, FieldName)"

Vous pouvez également enregistrer vos révisions au format XML, vous n'avez alors qu'un seul enregistrement pour une révision.Cela ressemblera à :

// Holds Employee Entity
"Employees (EmployeeId, FirstName, LastName, DepartmentId, .., ..)"

// Holds all entities revisions in rows.
"EntityChanges (EntityName, EntityId, DateModified, XMLChanges)"

Nous avons eu des exigences similaires et nous avons constaté que souvent, l'utilisateur souhaite simplement voir ce qui a été modifié, pas nécessairement annuler les modifications.

Je ne suis pas sûr de votre cas d'utilisation, mais nous avons créé une table d'audit qui est automatiquement mise à jour avec les modifications apportées à une entité commerciale, y compris le nom convivial de toutes les références et énumérations de clés étrangères.

Chaque fois que l'utilisateur enregistre ses modifications, nous rechargeons l'ancien objet, effectuons une comparaison, enregistrons les modifications et enregistrons l'entité (tout cela est effectué en une seule transaction de base de données en cas de problème).

Cela semble très bien fonctionner pour nos utilisateurs et nous évite le casse-tête d'avoir une table d'audit complètement séparée avec les mêmes champs que notre entité commerciale.

Il semble que vous souhaitiez suivre les modifications apportées à des entités spécifiques au fil du temps, par ex.ID 3, "bob", "123 main street", puis un autre ID 3, "bob" "234 elm st", et ainsi de suite, en substance, être capable de vomir un historique de révision montrant chaque adresse à laquelle "bob" a été .

La meilleure façon de procéder est d'avoir un champ "est actuel" sur chaque enregistrement et (probablement) un horodatage ou un FK pour une table date/heure.

Les insertions doivent ensuite définir le "est actuel" et également supprimer le "est actuel" sur l'enregistrement "est actuel" précédent.Les requêtes doivent spécifier "est actuel", sauf si vous souhaitez tout l'historique.

Il y a d'autres ajustements à cela s'il s'agit d'un très grand tableau ou si un grand nombre de révisions sont attendues, mais il s'agit d'une approche assez standard.

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top