MongoDB / NoSQL: mantenimiento del historial de cambios de documentos

134

Un requisito bastante común en las aplicaciones de bases de datos es rastrear los cambios a una o más entidades específicas en una base de datos. Escuché esto llamado versiones de fila, una tabla de registro o una tabla de historial (estoy seguro de que tiene otros nombres). Hay varias maneras de abordarlo en un RDBMS: puede escribir todos los cambios de todas las tablas de origen en una sola tabla (más de un registro) o tener una tabla de historial separada para cada tabla de origen. También tiene la opción de administrar el inicio de sesión en el código de la aplicación o mediante activadores de la base de datos.

Estoy tratando de pensar cómo sería una solución al mismo problema en una base de datos NoSQL / document (específicamente MongoDB), y cómo se resolvería de manera uniforme. ¿Sería tan simple como crear números de versión para documentos y nunca sobrescribirlos? ¿Crear colecciones separadas para documentos "reales" versus "registrados"? ¿Cómo afectaría esto a las consultas y el rendimiento?

De todos modos, ¿es este un escenario común con las bases de datos NoSQL, y si es así, hay una solución común?

Phil Sandler
fuente
¿Qué controlador de idioma estás usando?
Joshua Partogi
Todavía no se ha decidido: todavía está jugando y aún no ha finalizado la elección de los back- end (aunque MongoDB parece extremadamente probable). He estado jugando con NoRM (C #), y me gustan algunos de los nombres asociados con ese proyecto, por lo que parece muy probable que sea la elección.
Phil Sandler
2
Sé que esta es una pregunta antigua, pero para cualquiera que esté buscando versiones con MongoDB, esta pregunta SO está relacionada y, en mi opinión, con mejores respuestas.
AWolf

Respuestas:

107

Buena pregunta, yo también estaba investigando esto.

Crea una nueva versión en cada cambio

Encontré el módulo de control de versiones del controlador Mongoid para Ruby. No lo he usado yo mismo, pero por lo que pude encontrar , agrega un número de versión a cada documento. Las versiones anteriores están incrustadas en el documento mismo. El principal inconveniente es que todo el documento se duplica en cada cambio , lo que dará como resultado que se almacene una gran cantidad de contenido duplicado cuando se trata de documentos grandes. Sin embargo, este enfoque está bien cuando se trata de documentos de pequeño tamaño y / o no actualiza documentos con mucha frecuencia.

Solo almacenar cambios en una nueva versión

Otro enfoque sería almacenar solo los campos modificados en una nueva versión . Luego puede 'aplanar' su historial para reconstruir cualquier versión del documento. Sin embargo, esto es bastante complejo, ya que necesita realizar un seguimiento de los cambios en su modelo y almacenar actualizaciones y eliminaciones de manera que su aplicación pueda reconstruir el documento actualizado. Esto puede ser complicado, ya que se trata de documentos estructurados en lugar de tablas SQL planas.

Almacenar cambios dentro del documento

Cada campo también puede tener un historial individual. Reconstruir documentos a una versión dada es mucho más fácil de esta manera. En su aplicación no tiene que hacer un seguimiento explícito de los cambios, sino simplemente crear una nueva versión de la propiedad cuando cambie su valor. Un documento podría verse así:

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

Sin embargo, marcar parte del documento como eliminado en una versión sigue siendo algo incómodo. Puede introducir un statecampo para partes que se pueden eliminar / restaurar desde su aplicación:

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

Con cada uno de estos enfoques, puede almacenar una versión actualizada y plana en una colección y los datos del historial en una colección separada. Esto debería mejorar los tiempos de consulta si solo está interesado en la última versión de un documento. Pero cuando necesite la última versión y los datos históricos, deberá realizar dos consultas, en lugar de una. Por lo tanto, la elección de usar una sola colección frente a dos colecciones separadas dependerá de la frecuencia con la que su aplicación necesite las versiones históricas .

La mayor parte de esta respuesta es solo una descarga mental de mis pensamientos, aún no he probado nada de esto. Mirando hacia atrás, la primera opción es probablemente la mejor y más fácil solución, a menos que la sobrecarga de datos duplicados sea muy importante para su aplicación. La segunda opción es bastante compleja y probablemente no valga la pena. La tercera opción es básicamente una optimización de la opción dos y debería ser más fácil de implementar, pero probablemente no valga la pena el esfuerzo de implementación a menos que realmente no pueda ir con la opción uno.

Esperamos recibir comentarios sobre esto y las soluciones de otras personas al problema :)

Niels van der Rest
fuente
¿Qué pasa con el almacenamiento de deltas en algún lugar, para que tenga que aplanar para obtener un documento histórico y siempre tener el actual disponible?
jpmc26
@ jpmc26 Eso es similar al segundo enfoque, pero en lugar de guardar los deltas para llegar a las últimas versiones, está guardando los deltas para llegar a las versiones históricas. El enfoque a utilizar depende de la frecuencia con la que necesitará las versiones históricas.
Niels van der Rest
Podría agregar un párrafo sobre el uso del documento como una vista en el estado actual de las cosas y tener un segundo documento como un registro de cambios que hará un seguimiento de cada cambio, incluida una marca de tiempo (los valores iniciales deben aparecer en este registro); luego puede 'reproducir' 'a cualquier punto en el tiempo y, por ejemplo, correlacionar lo que estaba sucediendo cuando su algoritmo lo tocó o ver cómo se mostraba un elemento cuando el usuario hacía clic en él.
Manuel Arwed Schmidt
¿Esto afectará el rendimiento si los campos indexados se representan como matrices?
DmitriD
@All: ¿podría compartir algún código para lograr esto?
Pra_A
8

Hemos implementado parcialmente esto en nuestro sitio y utilizamos 'Almacenar revisiones en un documento separado "(y una base de datos separada). Escribimos una función personalizada para devolver los diferenciales y la almacenamos. No es tan difícil y puede permitir la recuperación automatizada.

Amala
fuente
2
¿Podría por favor compartir un código similar? Este enfoque parece prometedor
Pra_A
1
@smilyface - la integración de Primavera de arranque Javers es la mejor manera de lograr esto
Pra_A
@PAA - He hecho una pregunta (casi el mismo concepto). stackoverflow.com/questions/56683389/… ¿Tiene alguna entrada para eso?
smilyface
6

¿Por qué no una variación en los cambios de la Tienda dentro del documento ?

En lugar de almacenar versiones en cada par de claves, los pares de claves actuales en el documento siempre representan el estado más reciente y se almacena un 'registro' de cambios dentro de una matriz de historial. Solo las claves que han cambiado desde la creación tendrán una entrada en el 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" }
    }
  ]
}
Paul Taylor
fuente
2

Uno puede tener una base de datos NoSQL actual y una base de datos NoSQL histórica. Habrá un ETL nocturno todos los días. Este ETL registrará cada valor con una marca de tiempo, por lo que en lugar de valores siempre serán tuplas (campos versionados). Solo registrará un nuevo valor si se realizó un cambio en el valor actual, ahorrando espacio en el proceso. Por ejemplo, este histórico archivo json de base de datos NoSQL puede verse así:

{
  _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
fuente
0

Para los usuarios de Python (python 3+, y por supuesto), hay HistoricalCollection que es una extensión del objeto de Colección de pymongo.

Ejemplo de los documentos:

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

Divulgación completa, soy el autor del paquete. :)

Dash2TheDot
fuente