Version contrôlant le contenu d'une base de données

16

Je travaille sur un projet Web qui implique un contenu modifiable par l'utilisateur, et j'aimerais pouvoir faire un suivi de version du contenu réel, qui vit dans une base de données. Fondamentalement, je veux implémenter des historiques de changements de style wiki.

En faisant des recherches de base, je vois beaucoup de documentation sur la façon de mettre à jour votre schéma de base de données (le mien est en fait déjà contrôlé), mais toutes les stratégies existantes sur la façon de suivre les modifications du contenu de votre base de données sont perdues dans l'avalanche de trucs de version de schéma, au moins dans mes recherches.

Je peux penser à quelques façons d'implémenter mon propre suivi des modifications, mais elles semblent toutes plutôt grossières:

  • Enregistrez la ligne entière à chaque changement, reliez la ligne à l'ID source avec une clé primaire (ce vers quoi je me penche actuellement, c'est la plus simple). Beaucoup de petits changements pourraient cependant produire beaucoup de ballonnement.
  • enregistrer avant / après / utilisateur / horodatage pour chaque modification, avec un nom de colonne pour relier la modification à la colonne appropriée.
  • enregistrer avant / après / utilisateur / horodatage avec une table pour chaque colonne (entraînerait trop de tables).
  • enregistrer les différences / utilisateur / horodatage pour chaque modification avec une colonne (cela signifierait que vous devrez parcourir l'intégralité de l'historique des modifications pour revenir à une certaine date).

Quelle est la meilleure approche ici? Rouler le mien semble que je réinvente probablement la (meilleure) base de code de quelqu'un d'autre.


Points bonus pour PostgreSQL.

Faux nom
la source
Cette question a déjà été discutée sur SO: stackoverflow.com/questions/3874199/… . Google pour "l'historique des enregistrements de base de données", et vous trouverez d'autres articles.
Doc Brown
1
Cela ressemble à un candidat idéal pour le sourçage d'événements
James
Pourquoi ne pas utiliser le journal des transactions du SQL-Server pour faire l'affaire?
Thomas Junk

Réponses:

11

La technique que j'ai normalement utilisée est de sauvegarder l'enregistrement complet, avec un champ end_timestamp. Il existe une règle commerciale selon laquelle une seule ligne peut avoir un horodatage final nul, et c'est bien sûr le contenu actuellement actif.

Si vous adoptez ce système, je vous recommande fortement d'ajouter un index ou une contrainte pour appliquer la règle. C'est facile avec Oracle, car un index unique peut contenir un et un seul null. D'autres bases de données peuvent être plus problématiques. Faire appliquer la règle par la base de données gardera votre code honnête.

Vous avez tout à fait raison de dire que de nombreux petits changements créeront des ballonnements, mais vous devez les échanger contre du code et de la simplicité des rapports.

kiwiron
la source
Notez que d'autres moteurs de base de données peuvent se comporter différemment, par exemple MySQL autorise plusieurs valeurs NULL dans une colonne avec un index unique. Cela rend cette contrainte beaucoup plus difficile à appliquer.
qbd
L'utilisation d'un horodatage réel n'est pas sûre, mais certaines bases de données MVCC fonctionnent en interne en stockant des numéros de série de transactions minimum et maximum avec des tuples.
user2313838
"C'est facile avec Oracle, car un index unique peut contenir un et un seul null". Faux. Oracle n'inclut pas du tout les valeurs nulles dans les index. Il n'y a pas de limite au nombre de null dans une colonne avec un index unique.
Gerrat
@Gerrat Cela fait plusieurs années que j'ai conçu une base de données qui avait cette exigence, et je n'ai plus accès à cette base de données. Vous avez raison de dire qu'un index unique standard peut prendre en charge plusieurs valeurs Null, mais je pense que nous avons utilisé soit une contrainte unique, soit éventuellement un index fonctionnel.
kiwiron
8

Notez que si vous utilisez Microsoft SQL Server, il existe déjà une fonctionnalité appelée Change Data Capture . Vous devrez toujours écrire du code pour accéder aux révisions précédentes plus tard (CDC crée des vues spécifiques pour cela), mais au moins vous n'avez pas à modifier le schéma de vos tables, ni implémenter le suivi des modifications lui-même.

Sous le capot , ce qui se passe, c'est que:

  • CDC crée un tableau supplémentaire contenant les révisions,

  • Votre tableau d'origine est utilisé tel qu'il était auparavant, c'est-à-dire que toute mise à jour est reflétée directement dans ce tableau,

  • La table CDC ne stocke que les valeurs modifiées, ce qui signifie que la duplication des données est réduite au minimum.

Le fait que les modifications soient stockées dans une table différente a deux conséquences majeures:

  • Les sélections de la table d'origine sont aussi rapides que sans CDC. Si je me souviens bien, CDC se produit après la mise à jour, donc les mises à jour sont tout aussi rapides (bien que je ne me souvienne pas bien de la façon dont CDC gère la cohérence des données).

  • Certaines modifications apportées au schéma de la table d'origine entraînent la suppression du CDC. Par exemple, si vous ajoutez une colonne, CDC ne sait pas comment gérer cela. D'un autre côté, l'ajout d'un index ou d'une contrainte devrait convenir. Cela devient rapidement un problème si vous activez CDC sur une table qui est sujette à des changements fréquents. Il pourrait y avoir une solution permettant de changer le schéma sans perdre le CDC, mais je ne l'ai pas recherchée.

Arseni Mourzenko
la source
6

Résolvez le problème "philosophiquement" et dans le code en premier. Et puis "négocier" avec le code et la base de données pour y arriver.

Par exemple , si vous traitez des articles génériques, un concept initial pour un article pourrait ressembler à ceci:

class Article {
  public Int32 Id;
  public String Body;
}

Et au niveau suivant le plus basique, je veux garder une liste de révisions:

class Article {
  public Int32 Id;
  public String Body;
  public List<String> Revisions;
}

Et je pourrais peut-être comprendre que le corps actuel n'est que la dernière révision. Et cela signifie deux choses: j'ai besoin que chaque révision soit datée ou numérotée:

class Revision {
  public Int32 Id;
  public Article ParentArticle;
  public DateTime Created;
  public String Body;
}

Et ... et le corps actuel de l'article n'a pas besoin d'être distinct de la dernière révision:

class Article {
  public Int32 Id;
  public String Body {
    get {
      return (Revisions.OrderByDesc(r => r.Created))[0];
    }
    set {
      Revisions.Add(new Revision(value));
    }
  }
  public List<Revision> Revisions;
}

Quelques détails manquent; mais cela montre que vous voulez probablement deux entités . L'un représente l'article (ou un autre type d'en-tête), et l'autre est une liste de révisions (regrouper les champs qui ont un bon sens "philosophique" à grouper). Vous n'avez pas besoin de contraintes de base de données spéciales au départ, car votre code ne se soucie d'aucune des révisions en soi - ce sont les propriétés d'un article qui connaît les révisions.

Ainsi, vous n'avez pas à vous soucier de signaler les révisions d'une manière spéciale ou de vous appuyer sur une contrainte de base de données pour marquer l'article "actuel". Il vous suffit de les horodater (même un identifiant automatique serait OK), de les rendre liés à leur article parent et de laisser l'article en charge de savoir que le "dernier" est le plus pertinent.

Et vous laissez un ORM gérer les détails les moins philosophiques - ou vous les cachez dans une classe utilitaire personnalisée si vous n'utilisez pas un ORM prêt à l'emploi.

Beaucoup plus tard, après avoir effectué des tests de résistance, vous pouvez penser à faire de cette propriété de révision lazy-load, ou avoir votre attribut Body lazy-load uniquement la révision la plus haute. Mais, dans ce cas, votre structure de données ne devrait pas avoir à changer pour tenir compte de ces optimisations.

svidgen
la source
2

Il existe une page wiki PostgreSQL pour un déclencheur de suivi d'audit qui vous explique comment configurer un journal d'audit qui fera ce dont vous avez besoin.

Il suit les données d'origine complètes d'une modification, ainsi que la liste des nouvelles valeurs pour les mises à jour (pour les insertions et les suppressions, il n'y a qu'une seule valeur). Si vous souhaitez restaurer une ancienne version, vous pouvez récupérer la copie des données d'origine de l'enregistrement d'audit. Notez que si vos données impliquent des clés étrangères, ces enregistrements peuvent également devoir être annulés pour maintenir la cohérence.

De manière générale, si votre application de base de données passe la plupart de son temps uniquement aux données actuelles, je pense que vous feriez mieux de suivre les versions alternatives dans un tableau distinct des données actuelles. Cela gardera vos index de table actifs plus gérables.

Si les lignes que vous suivez sont très grandes et que l'espace est un problème grave, vous pouvez essayer de décomposer les modifications et de stocker des différences / correctifs minimaux, mais c'est certainement plus de travail pour couvrir tous vos types de types de données. J'ai déjà fait cela auparavant, et c'était difficile de reconstruire les anciennes versions des données en parcourant toutes les modifications en arrière, une à la fois.

Ben Turner
la source
1

Eh bien, je me suis contenté de l'option la plus simple, un déclencheur qui copie l'ancienne version d'une ligne dans un journal d'historique par table.

Si je me retrouve avec trop de ballonnement dans la base de données, je peux envisager de réduire éventuellement certains des changements mineurs de l'historique, si nécessaire.

La solution s'est avérée plutôt désordonnée, car je voulais générer automatiquement les fonctions de déclenchement. Je suis SQLAlchemy, j'ai donc pu produire la table d'historique en faisant des hijinks d'héritage, ce qui était bien, mais les fonctions de déclenchement réelles ont fini par nécessiter un couplage de chaînes pour générer correctement les fonctions PostgreSQL et mapper les colonnes d'une table à un autre correctement.

Quoi qu'il en soit, tout est sur github ici .

Faux nom
la source