171 votes

MongoDB/NoSQL: Garder l'historique des modifications de documents

Une exigence assez courante dans les applications de base de données est de suivre les changements apportés à une ou plusieurs entités spécifiques dans une base de données. J'ai entendu cela appelé versionnement de ligne, une table de journal ou une table d'historique (je suis sûr qu'il existe d'autres noms pour cela). Il existe plusieurs façons de l'aborder dans un SGBDR - vous pouvez écrire tous les changements de toutes les tables source dans une seule table (plutôt un journal) ou avoir une table d'historique distincte pour chaque table source. Vous avez également la possibilité de gérer la journalisation dans le code d'application ou via des déclencheurs de base de données.

J'essaie de réfléchir à ce à quoi pourrait ressembler une solution au même problème dans une base de données NoSQL/document (en particulier MongoDB) et comment cela pourrait être résolu de manière uniforme. Serait-il aussi simple de créer des numéros de version pour les documents et de ne jamais les écraser? Créer des collections distinctes pour les documents "réels" vs "journalisés"? Comment cela affecterait-il les requêtes et les performances?

En tout cas, est-ce un scénario courant avec les bases de données NoSQL, et le cas échéant, y a-t-il une solution commune?

0 votes

Quel pilote de langue utilisez-vous ?

0 votes

Pas encore décidé - je peaufine encore et n'ai même pas encore finalisé le choix des back-ends (bien que MongoDB semble extrêmement probable). J'ai bricolé avec NoRM (C#), et j'aime certains des noms associés à ce projet, il semble donc très probable que ce soit le choix final.

2 votes

Je sais que cette question est ancienne, mais pour ceux qui recherchent le versionnage avec MongoDB, cette question SO est pertinente et, à mon avis, dispose de meilleures réponses.

140voto

Niels van der Rest Points 11802

Bonne question, j'y regardais aussi.

Créez une nouvelle version à chaque changement

J'ai découvert le module de versioning du pilote Mongoid pour Ruby. Je ne l'ai pas encore utilisé, mais d'après ce que j'ai pu trouver, il ajoute un numéro de version à chaque document. Les anciennes versions sont intégrées dans le document lui-même. Le principal inconvénient est que le document entier est dupliqué à chaque changement, ce qui entraîne beaucoup de contenu en double étant stocké lorsque vous traitez avec de grands documents. Cette approche est cependant valable lorsque vous traitez avec des documents de petite taille et/ou que vous ne mettez pas souvent à jour les documents.

Stocker uniquement les changements dans une nouvelle version

Une autre approche serait de stocker seulement les champs modifiés dans une nouvelle version. Ensuite, vous pouvez 'aplanir' votre historique pour reconstruire n'importe quelle version du document. Cependant, c'est assez complexe, car vous devez suivre les changements dans votre modèle et stocker les mises à jour et les suppressions d'une manière telle que votre application puisse reconstruire le document à jour. Cela peut être délicat, car vous travaillez avec des documents structurés plutôt que des tables SQL plates.

Stockez les changements à l'intérieur du document

Chaque champ peut également avoir une historique individuelle. Reconstruire des documents vers une version donnée est beaucoup plus facile de cette manière. Dans votre application, vous n'avez pas à suivre explicitement les changements, mais simplement créer une nouvelle version de la propriété lorsque vous en changez la valeur. Un document pourrait ressembler à ceci:

{
  _id: "4c6b9456f61f000000007ba6"
  title: [
    { version: 1, value: "Bonjour tout le monde" },
    { version: 6, value: "Foo" }
  ],
  body: [
    { version: 1, value: "Ce truc fonctionne-t-il ?" },
    { version: 2, value: "Que devrais-je écrire ?" },
    { version: 6, value: "Voici le nouveau contenu" }
  ],
  tags: [
    { version: 1, value: [ "test", "trivial" ] },
    { version: 6, value: [ "foo", "test" ] }
  ],
  comments: [
    {
      author: "joe", // Champ sans version
      body: [
        { version: 3, value: "Quelque chose de cool" }
      ]
    },
    {
      author: "xxx",
      body: [
        { version: 4, value: "Spam" },
        { version: 5, deleted: true }
      ]
    },
    {
      author: "jim",
      body: [
        { version: 7, value: "Pas mal" },
        { version: 8, value: "Pas mal du tout" }
      ]
    }
  ]
}

Marquer une partie du document comme supprimée dans une version est encore quelque peu maladroit. Vous pourriez introduire un champ state pour les parties qui peuvent être supprimées/restaurées depuis votre application:

{
  author: "xxx",
  body: [
    { version: 4, value: "Spam" }
  ],
  state: [
    { version: 4, deleted: false },
    { version: 5, deleted: true }
  ]
}

Avec chacune de ces approches, vous pouvez stocker une version à jour et aplatie dans une collection et les données historiques dans une collection séparée. Cela devrait améliorer les temps de requête si vous vous intéressez seulement à la dernière version d'un document. Mais lorsque vous avez besoin à la fois de la dernière version et des données historiques, vous devrez effectuer deux requêtes, au lieu d'une seule. Ainsi, le choix entre utiliser une seule collection ou deux collections séparées devrait dépendre de la fréquence à laquelle votre application a besoin des versions historiques.

La plupart de cette réponse est juste un déversement de mes pensées, je n'ai pas encore essayé tout cela. En regardant en arrière, la première option est probablement la solution la plus facile et la meilleure, à moins que le surcroît de données dupliquées ne soit très significatif pour votre application. La deuxième option est assez complexe et probablement ne vaut pas l'effort. La troisième option est essentiellement une optimisation de la deuxième option et devrait être plus facile à mettre en œuvre, mais probablement ne mérite pas l'effort de mise en œuvre, à moins que vous ne puissiez vraiment pas opter pour la première option.

En attendant vos retours sur ce sujet et les solutions des autres personnes au problème :)

3 votes

Que diriez-vous de stocker les deltas quelque part, de sorte que vous deviez les aplatir pour obtenir un document historique et avoir toujours le document actuel disponible ?

0 votes

@jpmc26 C'est similaire à la deuxième approche, mais au lieu de sauvegarder les écarts pour atteindre les dernières versions, vous sauvegardez des écarts pour atteindre les versions historiques. Le choix de l'approche à utiliser dépend de la fréquence à laquelle vous aurez besoin des versions historiques.

0 votes

Vous pourriez ajouter un paragraphe sur l'utilisation du document comme une vue de l'état actuel des choses et avoir un deuxième document comme un journal des modifications qui suivra chaque changement, y compris un horodatage (les valeurs initiales doivent apparaître dans ce journal) - vous pouvez ensuite 'rejouer' à n'importe quel moment donné et par exemple corréler ce qui se passait lorsque votre algorithme l'a touché ou voir comment un élément était affiché lorsque l'utilisateur a cliqué dessus.

18voto

Paul Taylor Points 41

Pourquoi ne pas une variation sur Conserver les modifications dans le document ?

Au lieu de stocker des versions pour chaque paire de clés, les paires de clés actuelles dans le document représentent toujours l'état le plus récent et un 'journal' des modifications est stocké au sein d'un tableau d'historique. Seules les clés qui ont changé depuis la création auront une entrée dans le journal.

{
  _id: "4c6b9456f61f000000007ba6"
  title: "Bar",
  body: "Est-ce que ça marche ?",
  tags: [ "test", "trivial" ],
  comments: [
    { key: 1, author: "joe", body: "Quelque chose de cool" },
    { key: 2, author: "xxx", body: "Spam", deleted: true },
    { key: 3, author: "jim", body: "Pas mal du tout" }
  ],
  history: [
    { 
      who: "joe",
      when: 20160101,
      what: { title: "Foo", body: "Que devrais-je écrire ?" }
    },
    { 
      who: "jim",
      when: 20160105,
      what: { tags: ["test", "test2"], comments: { key: 3, body: "Pas mal du touuut" }
    }
  ]
}

10voto

Amala Points 1115

Nous avons partiellement implémenté ceci sur notre site et nous utilisons la fonction 'Stocker les révisions dans un document séparé" (et une base de données séparée). Nous avons écrit une fonction personnalisée pour retourner les différences et nous les stockons. Pas si difficile et peut permettre une récupération automatisée.

2 votes

Pouvez-vous s'il vous plaît partager un peu de code autour de la même chose ? Cette approche semble prometteuse

1 votes

@smilyface - L'intégration de Spring Boot avec Javers est la meilleure pour y parvenir

1 votes

@PAA - J'ai posé une question (presque le même concept). stackoverflow.com/questions/56683389/… Avez-vous un avis sur cela ?

5voto

Paul Kar. Points 739

On peut avoir une base de données NoSQL actuelle et une base de données NoSQL historique. Il y aura un ETL nocturne exécuté tous les jours. Cet ETL enregistrera chaque valeur avec un horodatage, donc au lieu de valeurs, ce seront toujours des tuples (champs versionnés). Il enregistrera uniquement une nouvelle valeur s'il y a eu un changement apporté à la valeur actuelle, ce qui permet d'économiser de l'espace dans le processus. Par exemple, ce fichier json de base de données historique NoSQL peut ressembler à ceci :

{
  _id: "4c6b9456f61f000000007ba6"
  title: [
    { date: 20160101, value: "Bonjour le monde" },
    { date: 20160202, value: "Foo" }
  ],
  body: [
    { date: 20160101, value: "Est-ce que ça fonctionne ?" },
    { date: 20160102, value: "Que devrais-je écrire ?" },
    { date: 20160202, value: "Ceci est le nouveau corps" }
  ],
  tags: [
    { date: 20160101, value: [ "test", "trivial" ] },
    { date: 20160102, value: [ "foo", "test" ] }
  ],
  comments: [
    {
      author: "joe", // Champ non versionné
      body: [
        { date: 20160301, value: "Quelque chose de cool" }
      ]
    },
    {
      author: "xxx",
      body: [
        { date: 20160101, value: "Spam" },
        { date: 20160102, deleted: true }
      ]
    },
    {
      author: "jim",
      body: [
        { date: 20160101, value: "Pas mal" },
        { date: 20160102, value: "Pas mal du tout" }
      ]
    }
  ]
}

2voto

Dash2TheDot Points 16

Pour les utilisateurs de Python (python 3+ et plus bien sûr), il y a HistoricalCollection qui est une extension de l'objet Collection de pymongo.

Exemple à partir de la documentation:

from historical_collection.historical import HistoricalCollection
from pymongo import MongoClient
class Users(HistoricalCollection):
    PK_FIELDS = ['username', ]  # <<= Ceci est le seul prérequis

# ...

users = Users(database=db)

users.patch_one({"username": "darth_later", "email": "darthlater@example.com"})
users.patch_one({"username": "darth_later", "email": "darthlater@example.com", "laser_sword_color": "red"})

list(users.revisions({"username": "darth_later"}))

# [{'_id': ObjectId('5d98c3385d8edadaf0bb845b'),
#   'username': 'darth_later',
#   'email': 'darthlater@example.com',
#   '_revision_metadata': None},
#  {'_id': ObjectId('5d98c3385d8edadaf0bb845b'),
#   'username': 'darth_later',
#   'email': 'darthlater@example.com',
#   '_revision_metadata': None,
#   'laser_sword_color': 'red'}]

Divulgation complète, je suis l'auteur du package. :)

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