web-dev-qa-db-fra.com

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

Une exigence assez courante dans les applications de base de données consiste à suivre les modifications apportées à une ou plusieurs entités spécifiques dans une base de données. J'ai entendu ce qu'on appelle le versionnage des lignes, 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 un journal) ou avoir une table d'historique distincte pour chaque table source. Vous avez également la possibilité de gérer la connexion dans le code d'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 cela serait résolu de manière uniforme. Serait-ce aussi simple que de créer des numéros de version pour des documents et de ne jamais les écraser? Créer des collections distinctes 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?

114
Phil Sandler

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

Créer une nouvelle version à chaque changement

Je suis tombé sur le module de versioning du pilote Mongoid pour Ruby. Je ne l'ai pas utilisé moi-même, mais à partir de 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. L'inconvénient majeur est que le document entier est dupliqué à chaque modification , ce qui entraînera le stockage de nombreux contenus en double lorsque vous traitez documents. Cette approche est très bien quand vous traitez des documents de petite taille et/ou ne mettez pas à jour des documents très souvent.

Enregistrer uniquement les modifications dans une nouvelle version

Une autre approche serait de stocker uniquement les champs modifiés dans une nouvelle version . Ensuite, vous pouvez aplatir votre historique pour reconstruire n'importe quelle version du document. C'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.

Enregistrer les modifications dans le document

Chaque champ peut également avoir une histoire individuelle. La reconstruction de 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é dans une version est cependant quelque peu gênant. Vous pouvez introduire un champ state pour les pièces qui peuvent être supprimées/restaurées 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 mise à jour et aplatie dans une collection et les données d'historique dans une collection distincte. Cela devrait améliorer les délais de requête si vous êtes uniquement intéressé 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, plutôt qu'une. Ainsi, le choix d'utiliser une seule collection par rapport à 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é. En y repensant, la première option est probablement la solution la plus simple et la meilleure, à moins que la surcharge de 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 de mise en œuvre à moins que vous ne puissiez vraiment pas opter pour l'option un.

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

92
Niels van der Rest

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

7
Amala

Pourquoi pas une variation sur Enregistrer les modifications dans le document?

Au lieu de stocker des versions sur 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" }
    }
  ]
}
4
Paul Taylor

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 seront 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" }
      ]
    }
  ]
}
2
Paul Kar.