MongoDB / NoSQL: conserver l'historique des modifications de document

134

Une exigence assez courante dans les applications de base de données est de suivre les modifications apportées à une ou plusieurs entités spécifiques dans une base de données. J'ai entendu cela appelé gestion des versions de ligne, une table de journal ou une table d'historique (je suis sûr qu'il y a d'autres noms pour cela). Il existe un certain nombre de façons de l'aborder dans un SGBDR - vous pouvez écrire toutes les modifications de toutes les tables source dans une seule table (plus d'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 de l'application ou via des déclencheurs de base de données.

J'essaie de réfléchir à ce à quoi ressemblerait une solution au même problème dans une base de données NoSQL / document (en particulier MongoDB), et comment elle serait résolue de manière uniforme. Serait-ce aussi simple que de créer des numéros de version pour les documents et de ne jamais les écraser? Créer des collections séparées pour les documents «réels» et «enregistrés»? Comment cela affecterait-il les requêtes et les performances?

Quoi qu'il en soit, est-ce un scénario courant avec les bases de données NoSQL, et si oui, existe-t-il une solution commune?

Phil Sandler
la source
Quel pilote de langue utilisez-vous?
Joshua Partogi
Pas encore décidé - toujours bricoler et n'ont même pas encore finalisé le choix des back-ends (bien que MongoDB semble extrêmement probable). J'ai bricolé NoRM (C #), et j'aime certains des noms associés à ce projet, il semble donc très probable que ce soit le choix.
Phil Sandler
2
Je sais que c'est une vieille question, mais pour tous ceux qui recherchent des versions avec MongoDB, cette question SO est liée et à mon avis avec de meilleures réponses.
AWolf

Réponses:

107

Bonne question, je me suis aussi penché sur cette question.

Créer une nouvelle version à chaque changement

Je suis tombé sur le module de gestion des versions du pilote Mongoid pour Ruby. Je ne l'ai pas utilisé moi-même, mais d'après ce que j'ai pu trouver , il ajoute un numéro de version à chaque document. Les versions plus anciennes sont intégrées dans le document lui-même. L'inconvénient majeur est que le document entier est dupliqué à chaque modification , ce qui entraînera le stockage d'un grand nombre de contenu dupliqué lorsque vous traitez de gros documents. Cette approche convient cependant lorsque vous traitez avec des documents de petite taille et / ou que vous ne mettez pas à jour des documents très souvent.

Stocker uniquement les modifications dans une nouvelle version

Une autre approche consisterait à stocker uniquement les champs modifiés dans une nouvelle version . Ensuite, vous pouvez «aplatir» votre historique pour reconstruire n'importe quelle version du document. Ceci est cependant assez complexe, car vous devez suivre les modifications de votre modèle et stocker les mises à jour et les suppressions de manière à ce que votre application puisse reconstruire le document à jour. Cela peut être délicat, car vous avez affaire à des documents structurés plutôt qu'à des tables SQL plates.

Stocker les modifications dans le document

Chaque champ peut également avoir une histoire individuelle. Reconstruire des documents dans une version donnée est beaucoup plus facile de cette façon. Dans votre application, vous n'avez pas à suivre explicitement les modifications, mais créez simplement une nouvelle version de la propriété lorsque vous modifiez sa valeur. Un document pourrait ressembler à ceci:

{
  _id: "4c6b9456f61f000000007ba6"
  title: [
    { version: 1, value: "Hello world" },
    { version: 6, value: "Foo" }
  ],
  body: [
    { version: 1, value: "Is this thing on?" },
    { version: 2, value: "What should I write?" },
    { version: 6, value: "This is the new body" }
  ],
  tags: [
    { version: 1, value: [ "test", "trivial" ] },
    { version: 6, value: [ "foo", "test" ] }
  ],
  comments: [
    {
      author: "joe", // Unversioned field
      body: [
        { version: 3, value: "Something cool" }
      ]
    },
    {
      author: "xxx",
      body: [
        { version: 4, value: "Spam" },
        { version: 5, deleted: true }
      ]
    },
    {
      author: "jim",
      body: [
        { version: 7, value: "Not bad" },
        { version: 8, value: "Not bad at all" }
      ]
    }
  ]
}

Marquer une partie du document comme supprimée dans une version est cependant un peu gênant. Vous pouvez introduire un statechamp pour les pièces qui peuvent être supprimées / restaurées à partir de 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 d'historique dans une collection distincte. Cela devrait améliorer les temps de requête si vous n'êtes intéressé que par 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. Ainsi, le choix d'utiliser une seule collection plutôt que deux collections distinctes devrait dépendre de la fréquence à laquelle votre application a besoin des versions historiques .

La plupart de cette réponse n'est qu'une décharge cérébrale de mes pensées, je n'ai encore rien essayé de cela. En y repensant, la première option est probablement la solution la plus simple et la meilleure, à moins que la surcharge des données en double ne soit très importante pour votre application. La deuxième option est assez complexe et ne vaut probablement pas la peine. La troisième option est essentiellement une optimisation de l'option deux et devrait être plus facile à implémenter, mais ne vaut probablement pas l'effort d'implémentation à moins que vous ne puissiez vraiment pas opter pour l'option un.

Dans l'attente des commentaires à ce sujet et des solutions d'autres personnes au problème :)

Niels van der Rest
la source
Qu'en est-il de stocker des deltas quelque part, de sorte que vous deviez aplatir pour obtenir un document historique et toujours avoir le courant disponible?
jpmc26
@ jpmc26 C'est similaire à la deuxième approche, mais au lieu d'enregistrer les deltas pour accéder aux dernières versions, vous enregistrez des deltas pour accéder aux versions historiques. L'approche à utiliser dépend de la fréquence à laquelle vous aurez besoin des versions historiques.
Niels van der Rest du
Vous pouvez ajouter un paragraphe sur l'utilisation du document comme vue de l'état actuel des choses et avoir un deuxième document comme journal des modifications qui suivra chaque changement, y compris un horodatage (les valeurs initiales doivent apparaître dans ce journal) - vous pouvez alors 'rejouer 'à un 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.
Manuel Arwed Schmidt
Cela affectera-t-il les performances si les champs indexés sont représentés sous forme de tableaux?
DmitriD
@All - Pourriez-vous s'il vous plaît partager du code pour y parvenir?
Pra_A
8

Nous l'avons partiellement implémenté sur notre site et nous utilisons le "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 renvoyer les diffs et nous stockons cela. Pas si difficile et pouvons permettre une récupération automatisée.

Amala
la source
2
Pourriez-vous s'il vous plaît partager du code autour du même? Cette approche semble prometteuse
Pra_A
1
@smilyface - L'intégration de Spring Boot Javers est la meilleure pour y parvenir
Pra_A
@PAA - J'ai posé une question (presque le même concept). stackoverflow.com/questions/56683389/... Avez-vous des suggestions pour cela?
smilyface
6

Pourquoi pas une variante des modifications du magasin dans le document ?

Au lieu de stocker des versions par rapport à 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é dans 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: "Is this thing on?",
  tags: [ "test", "trivial" ],
  comments: [
    { key: 1, author: "joe", body: "Something cool" },
    { key: 2, author: "xxx", body: "Spam", deleted: true },
    { key: 3, author: "jim", body: "Not bad at all" }
  ],
  history: [
    { 
      who: "joe",
      when: 20160101,
      what: { title: "Foo", body: "What should I write?" }
    },
    { 
      who: "jim",
      when: 20160105,
      what: { tags: ["test", "test2"], comments: { key: 3, body: "Not baaad at all" }
    }
  ]
}
Paul Taylor
la source
2

On peut avoir une base de données NoSQL actuelle et une base de données NoSQL historique. Il y aura un ETL tous les soirs. Cet ETL enregistrera chaque valeur avec un horodatage, donc au lieu de valeurs, ce sera toujours des tuples (champs versionnés). Il n'enregistrera une nouvelle valeur que si une modification a été apportée à la valeur actuelle, ce qui économise de l'espace dans le processus. Par exemple, ce fichier json de base de données NoSQL historique peut ressembler à ceci:

{
  _id: "4c6b9456f61f000000007ba6"
  title: [
    { date: 20160101, value: "Hello world" },
    { date: 20160202, value: "Foo" }
  ],
  body: [
    { date: 20160101, value: "Is this thing on?" },
    { date: 20160102, value: "What should I write?" },
    { date: 20160202, value: "This is the new body" }
  ],
  tags: [
    { date: 20160101, value: [ "test", "trivial" ] },
    { date: 20160102, value: [ "foo", "test" ] }
  ],
  comments: [
    {
      author: "joe", // Unversioned field
      body: [
        { date: 20160301, value: "Something cool" }
      ]
    },
    {
      author: "xxx",
      body: [
        { date: 20160101, value: "Spam" },
        { date: 20160102, deleted: true }
      ]
    },
    {
      author: "jim",
      body: [
        { date: 20160101, value: "Not bad" },
        { date: 20160102, value: "Not bad at all" }
      ]
    }
  ]
}
Paul Kar.
la source
0

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

Exemple tiré de la documentation:

from historical_collection.historical import HistoricalCollection
from pymongo import MongoClient
class Users(HistoricalCollection):
    PK_FIELDS = ['username', ]  # <<= This is the only requirement

# ...

users = Users(database=db)

users.patch_one({"username": "darth_later", "email": "[email protected]"})
users.patch_one({"username": "darth_later", "email": "[email protected]", "laser_sword_color": "red"})

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

# [{'_id': ObjectId('5d98c3385d8edadaf0bb845b'),
#   'username': 'darth_later',
#   'email': '[email protected]',
#   '_revision_metadata': None},
#  {'_id': ObjectId('5d98c3385d8edadaf0bb845b'),
#   'username': 'darth_later',
#   'email': '[email protected]',
#   '_revision_metadata': None,
#   'laser_sword_color': 'red'}]

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

Dash2TheDot
la source