MongoDB / NoSQL: Mantendo o histórico de alterações do documento

134

Um requisito bastante comum em aplicativos de banco de dados é rastrear alterações em uma ou mais entidades específicas em um banco de dados. Eu ouvi isso chamado versionamento de linha, uma tabela de log ou uma tabela de histórico (tenho certeza que existem outros nomes para ela). Existem várias maneiras de abordá-lo em um RDBMS - você pode gravar todas as alterações de todas as tabelas de origem em uma única tabela (mais de um log) ou ter uma tabela de histórico separada para cada tabela de origem. Você também tem a opção de gerenciar o logon no código do aplicativo ou por meio de gatilhos de banco de dados.

Estou tentando pensar em como seria uma solução para o mesmo problema em um banco de dados NoSQL / documento (especificamente o MongoDB) e como seria resolvida de maneira uniforme. Seria tão simples quanto criar números de versão para documentos e nunca substituí-los? Criando coleções separadas para documentos "reais" vs. "registrados"? Como isso afetaria a consulta e o desempenho?

Enfim, esse é um cenário comum com os bancos de dados NoSQL e, em caso afirmativo, existe uma solução comum?

Phil Sandler
fonte
Qual driver de idioma você está usando?
Joshua Partogi
Ainda não decidido - ainda mexendo e ainda nem finalizou a escolha dos back-ends (embora o MongoDB pareça extremamente provável). Tenho mexido no NoRM (C #) e gosto de alguns dos nomes associados a esse projeto, portanto, parece muito provável que seja a escolha.
Phil Sandler
2
Eu sei que essa é uma pergunta antiga, mas para quem está procurando por versões com o MongoDB, essa pergunta está relacionada e, na minha opinião, com melhores respostas.
AWolf

Respostas:

107

Boa pergunta, eu também estava investigando isso.

Crie uma nova versão em cada alteração

Me deparei com o módulo Versioning do driver Mongoid para Ruby. Eu não o usei, mas pelo que pude encontrar , ele adiciona um número de versão a cada documento. As versões mais antigas são incorporadas no próprio documento. A principal desvantagem é que todo o documento é duplicado a cada alteração , o que resultará em muito conteúdo duplicado sendo armazenado quando você estiver lidando com documentos grandes. Essa abordagem é boa quando você lida com documentos de tamanho pequeno e / ou não atualiza documentos com muita frequência.

Armazenar apenas alterações em uma nova versão

Outra abordagem seria armazenar apenas os campos alterados em uma nova versão . Em seguida, você pode 'achatar' seu histórico para reconstruir qualquer versão do documento. No entanto, isso é bastante complexo, pois você precisa acompanhar as alterações no modelo e armazenar as atualizações e exclusões de forma que seu aplicativo possa reconstruir o documento atualizado. Isso pode ser complicado, pois você está lidando com documentos estruturados em vez de tabelas SQL planas.

Armazenar alterações no documento

Cada campo também pode ter um histórico individual. Reconstruir documentos para uma determinada versão é muito mais fácil dessa maneira. No seu aplicativo, você não precisa controlar explicitamente as alterações, mas apenas crie uma nova versão da propriedade quando alterar seu valor. Um documento pode ser algo como isto:

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

Marcar parte do documento como excluído em uma versão ainda é um pouco estranho. Você pode introduzir um statecampo para peças que podem ser excluídas / restauradas do seu aplicativo:

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

Com cada uma dessas abordagens, você pode armazenar uma versão atualizada e nivelada em uma coleção e os dados do histórico em uma coleção separada. Isso deve melhorar o tempo de consulta se você estiver interessado apenas na versão mais recente de um documento. Mas quando você precisar da versão mais recente e dos dados históricos, precisará executar duas consultas, em vez de uma. Portanto, a escolha de usar uma única coleção versus duas coleções separadas deve depender da frequência com que seu aplicativo precisa das versões históricas .

A maior parte dessa resposta é apenas um despejo cerebral de meus pensamentos, ainda não tentei nada disso. Olhando para trás, a primeira opção é provavelmente a melhor e mais fácil, a menos que a sobrecarga de dados duplicados seja muito significativa para o seu aplicativo. A segunda opção é bastante complexa e provavelmente não vale o esforço. A terceira opção é basicamente uma otimização da opção dois e deve ser mais fácil de implementar, mas provavelmente não vale o esforço de implementação, a menos que você não possa ir com a opção um.

Ansioso para feedback sobre isso, e soluções de outras pessoas para o problema :)

Niels van der Rest
fonte
Que tal armazenar deltas em algum lugar, para que você precise achatar para obter um documento histórico e ter sempre o atual disponível?
jpmc26
@ jpmc26 É semelhante à segunda abordagem, mas, em vez de salvar os deltas para obter as versões mais recentes, você está salvando deltas para obter as versões históricas. Qual abordagem usar depende de quantas vezes você precisará das versões históricas.
Niels van der Rest
Você pode adicionar um parágrafo sobre como usar o documento como uma visualização do estado atual das coisas e ter um segundo documento como um registro de alterações que acompanhará cada alteração, incluindo um carimbo de data / hora (os valores iniciais precisam aparecer nesse log) - você poderá "reproduzir" 'para qualquer ponto no tempo e, por exemplo, correlacione o que estava acontecendo quando o algoritmo o tocou ou veja como um item foi exibido quando o usuário clicou nele.
Manuel Arwed Schmidt
Isso afetará o desempenho se os campos indexados representados como matrizes?
DmitriD
@All - Você poderia compartilhar algum código para conseguir isso?
Pra_A
8

Implementamos isso parcialmente em nosso site e usamos as 'Revisões da loja em um documento separado "(e banco de dados separado). Escrevemos uma função personalizada para retornar as diferenças e armazenamos isso. Não é tão difícil e pode permitir a recuperação automatizada.

Amala
fonte
2
Você poderia compartilhar algum código da mesma forma? Esta abordagem parece promissor
Pra_A
1
@smilyface - integração Primavera Bota Javers é melhor para conseguir isso
Pra_A
@PAA - Fiz uma pergunta (quase o mesmo conceito). stackoverflow.com/questions/56683389/… Você tem alguma entrada para isso?
smilyface
6

Por que não uma variação na loja muda dentro do documento ?

Em vez de armazenar versões em cada par de chaves, os pares de chaves atuais no documento sempre representam o estado mais recente e um 'log' de alterações é armazenado em uma matriz de histórico. Somente as chaves que foram alteradas desde a criação terão uma entrada no log.

{
  _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
fonte
2

Pode-se ter um banco de dados NoSQL atual e um banco de dados NoSQL histórico. Haverá um ETL noturno executado todos os dias. Esse ETL registrará todos os valores com um carimbo de data / hora; portanto, em vez de valores, sempre serão tuplas (campos com versão). Ele só registrará um novo valor se houver uma alteração no valor atual, economizando espaço no processo. Por exemplo, esse arquivo json histórico do banco de dados NoSQL pode ter a seguinte aparência:

{
  _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.
fonte
0

Para usuários de Python (python 3+ e, é claro), existe o HistoricalCollection que é uma extensão do objeto Collection do pymongo.

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

Divulgação completa, eu sou o autor do pacote. :)

Dash2TheDot
fonte