MongoDB / NoSQL: mantenere la cronologia delle modifiche al documento


134

Un requisito abbastanza comune nelle applicazioni di database è tenere traccia delle modifiche a una o più entità specifiche in un database. Ho sentito questo chiamato versionamento delle righe, una tabella di registro o una tabella di cronologia (sono sicuro che ci sono altri nomi per questo). Esistono diversi modi per accedervi in ​​un RDBMS: è possibile scrivere tutte le modifiche da tutte le tabelle di origine in una singola tabella (più di un registro) o disporre di una tabella cronologica separata per ciascuna tabella di origine. Hai anche la possibilità di gestire la registrazione nel codice dell'applicazione o tramite i trigger del database.

Sto cercando di capire come sarebbe una soluzione allo stesso problema in un database NoSQL / document (in particolare MongoDB) e come sarebbe risolto in modo uniforme. Sarebbe semplice come creare numeri di versione per i documenti e non sovrascriverli mai? Creazione di raccolte separate per documenti "reali" rispetto a "registrati"? In che modo ciò influirebbe sulle query e sulle prestazioni?

Ad ogni modo, si tratta di uno scenario comune con i database NoSQL e, in tal caso, esiste una soluzione comune?


Quale driver di lingua stai usando?
Joshua Partogi,

Non ancora deciso - ancora armeggiare e non ho ancora finalizzato la scelta dei back-end (sebbene MongoDB sembri estremamente probabile). Ho armeggiato con NoRM (C #) e mi piacciono alcuni dei nomi associati a quel progetto, quindi sembra molto probabile che sia la scelta.
Phil Sandler,

2
So che questa è una vecchia domanda, ma per chiunque stia cercando un versioning con MongoDB, questa domanda SO è correlata e nella mia opinione con risposte migliori.
AWolf

Risposte:


107

Bella domanda, stavo esaminando anche questo.

Crea una nuova versione per ogni modifica

Mi sono imbattuto nel modulo di versioning del driver Mongoid per Ruby. Non l'ho usato da solo, ma da quello che ho trovato , aggiunge un numero di versione a ciascun documento. Le versioni precedenti sono incorporate nel documento stesso. Lo svantaggio principale è che l' intero documento viene duplicato su ogni modifica , il che comporta la memorizzazione di molti contenuti duplicati quando si ha a che fare con documenti di grandi dimensioni. Questo approccio va bene anche quando hai a che fare con documenti di piccole dimensioni e / o non aggiorni documenti molto spesso.

Memorizza solo le modifiche in una nuova versione

Un altro approccio sarebbe quello di memorizzare solo i campi modificati in una nuova versione . Quindi puoi 'appiattire' la tua storia per ricostruire qualsiasi versione del documento. Ciò è piuttosto complesso, poiché è necessario tenere traccia delle modifiche nel modello e archiviare gli aggiornamenti e le eliminazioni in modo tale che l'applicazione possa ricostruire il documento aggiornato. Questo potrebbe essere difficile, dato che hai a che fare con documenti strutturati piuttosto che con tabelle SQL piatte.

Memorizza le modifiche all'interno del documento

Ogni campo può anche avere una storia individuale. La ricostruzione di documenti per una data versione è molto più semplice in questo modo. Nell'applicazione non è necessario tenere traccia in modo esplicito delle modifiche, ma è sufficiente creare una nuova versione della proprietà quando si modifica il suo valore. Un documento potrebbe assomigliare a questo:

{
  _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" }
      ]
    }
  ]
}

Contrassegnare parte del documento come eliminato in una versione è comunque un po 'imbarazzante. È possibile introdurre un statecampo per le parti che possono essere eliminate / ripristinate dalla propria applicazione:

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

Con ciascuno di questi approcci è possibile archiviare una versione aggiornata e appiattita in una raccolta e i dati della cronologia in una raccolta separata. Ciò dovrebbe migliorare i tempi delle query se sei interessato solo all'ultima versione di un documento. Ma quando hai bisogno sia della versione più recente che dei dati storici, devi eseguire due query, anziché una. Pertanto, la scelta di utilizzare una raccolta singola rispetto a due raccolte separate dovrebbe dipendere dalla frequenza con cui l'applicazione richiede le versioni storiche .

Gran parte di questa risposta è solo una scarica di cervello dei miei pensieri, non ho ancora provato nulla di tutto questo. Ripensandoci, la prima opzione è probabilmente la soluzione più semplice e migliore, a meno che il sovraccarico di dati duplicati non sia molto significativo per la tua applicazione. La seconda opzione è piuttosto complessa e probabilmente non vale la pena. La terza opzione è fondamentalmente un'ottimizzazione dell'opzione due e dovrebbe essere più facile da implementare, ma probabilmente non vale lo sforzo di implementazione a meno che non si possa davvero andare con l'opzione uno.

In attesa di feedback su questo, e soluzioni di altre persone al problema :)


Che ne dite di archiviare delta da qualche parte, in modo da dover appiattire per ottenere un documento storico e avere sempre la corrente disponibile?
jpmc26

@ jpmc26 È simile al secondo approccio, ma invece di salvare i delta per arrivare alle ultime versioni, stai salvando i delta per arrivare alle versioni storiche. L'approccio da utilizzare dipende dalla frequenza con cui avrai bisogno delle versioni storiche.
Niels van der Rest

È possibile aggiungere un paragrafo sull'utilizzo del documento come vista nello stato attuale delle cose e sull'avere un secondo documento come un log delle modifiche che seguirà ogni modifica incluso un timestamp (i valori iniziali devono apparire in questo registro) - è quindi possibile 'riprodurre "a un dato momento e ad es. correlare ciò che stava accadendo quando l'algoritmo lo ha toccato o vedere come un elemento è stato visualizzato quando l'utente ha fatto clic su di esso.
Manuel Arwed Schmidt,

Ciò influirà sulle prestazioni se i campi indicizzati rappresentati come matrici?
DmitriD il

@Tutti - Potresti condividere un po 'di codice per raggiungere questo obiettivo?
Pra_A

8

Abbiamo parzialmente implementato questo sul nostro sito e utilizziamo le "Revisioni dello store in un documento separato" (e database separato). Abbiamo scritto una funzione personalizzata per restituire le differenze e lo memorizziamo. Non è così difficile e può consentire il ripristino automatico.


2
Potresti condividere un po 'di codice nello stesso modo? Questo approccio sembra promettente
Pra_A

1
@smilyface - L'integrazione di Spring Boot Javers è la cosa migliore per raggiungere questo obiettivo
Pra_A

@PAA - Ho fatto una domanda (quasi lo stesso concetto). stackoverflow.com/questions/56683389/… Hai qualche input per questo?
smilyface,

6

Perché non una variazione su Store cambia all'interno del documento ?

Invece di archiviare le versioni su ciascuna coppia di chiavi, le coppie di chiavi correnti nel documento rappresentano sempre lo stato più recente e un "registro" delle modifiche viene archiviato in un array di cronologia. Solo le chiavi che sono state modificate dalla creazione avranno una voce nel registro.

{
  _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" }
    }
  ]
}

2

Si può avere un database NoSQL corrente e un database NoSQL storico. Ogni giorno ci sarà un ETL notturno. Questo ETL registrerà ogni valore con un timestamp, quindi al posto dei valori sarà sempre tupla (campi con versione). Registrerà un nuovo valore solo se è stata apportata una modifica al valore corrente, risparmiando spazio nel processo. Ad esempio, questo storico file json del database NoSQL può apparire così:

{
  _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" }
      ]
    }
  ]
}

0

Per gli utenti di Python (python 3+ e ovviamente), c'è HistoricalCollection che è un'estensione dell'oggetto Collection di pymongo.

Esempio dai documenti:

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": "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'}]

Divulgazione completa, sono l'autore del pacchetto. :)

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.