Como controlar a versão de um registro em um banco de dados

176

Digamos que eu tenha um registro no banco de dados e que usuários normais e administradores possam fazer atualizações.

Alguém pode sugerir uma boa abordagem / arquitetura como controlar a versão de todas as alterações nesta tabela, para que seja possível reverter um registro para uma revisão anterior.

Niels Bosma
fonte

Respostas:

164

Digamos que você tenha uma FOOtabela que administradores e usuários possam atualizar. Na maioria das vezes, você pode escrever consultas na tabela FOO. Dias felizes.

Então, eu criaria uma FOO_HISTORYtabela. Isso tem todas as colunas da FOOtabela. A chave primária é igual a FOO mais uma coluna RevisionNumber. Há uma chave estrangeira de FOO_HISTORYpara FOO. Você também pode adicionar colunas relacionadas à revisão, como UserId e RevisionDate. Preencha os RevisionNumbers de maneira crescente em todas as *_HISTORYtabelas (por exemplo, a partir de uma sequência Oracle ou equivalente). Não confie apenas em uma alteração em um segundo (ou seja, não insira RevisionDatea chave primária).

Agora, toda vez que você atualiza FOO, pouco antes de fazer a atualização, insira os valores antigos FOO_HISTORY. Você faz isso em algum nível fundamental no seu design, para que os programadores não possam acidentalmente perder esta etapa.

Se você deseja excluir uma linha, FOOvocê tem algumas opções. Faça cascata e exclua todo o histórico ou execute uma exclusão lógica sinalizando FOOcomo excluído.

Essa solução é boa quando você está bastante interessado nos valores atuais e apenas ocasionalmente na história. Se você sempre precisar do histórico, poderá colocar datas efetivas de início e término e manter todos os registros em FOOsi. Toda consulta precisa verificar essas datas.

WW.
fonte
1
Você pode fazer a atualização da tabela de auditoria com gatilhos de banco de dados se a camada de acesso a dados não a suportar diretamente. Além disso, não é difícil criar um gerador de código para gerar os gatilhos que usam a introspecção do dicionário de dados do sistema.
ConcernedOfTunbridgeWells
44
Eu recomendo que você realmente insira os novos dados, não os anteriores, para que a tabela de histórico tenha todos os dados. Embora armazene dados redundantes, elimina os casos especiais necessários para lidar com a pesquisa em ambas as tabelas quando dados históricos são necessários.
Nerdfest
6
Pessoalmente, eu recomendo não excluir nada (adie isso para uma atividade específica de limpeza) e tenha uma coluna "tipo de ação" para especificar se é inserir / atualizar / excluir. Para uma exclusão, copie a linha normalmente, mas coloque "delete" na coluna do tipo de ação.
Neil Barnwell
3
@Hydrargyrum Uma tabela contendo os valores atuais terá um desempenho melhor do que uma visualização da tabela histórica. Você também pode definir chaves estrangeiras referenciando os valores atuais.
WW.
2
There is a foreign key from FOO_HISTORY to FOO': má idéia, gostaria de excluir registros de foo sem alterar o histórico. a tabela de histórico deve ser inserida apenas em uso normal.
Jasen
46

Eu acho que você está procurando versionar o conteúdo dos registros do banco de dados (como o StackOverflow faz quando alguém edita uma pergunta / resposta). Um bom ponto de partida pode ser olhar para algum modelo de banco de dados que use o rastreamento de revisão .

O melhor exemplo que vem à mente é o MediaWiki, o mecanismo da Wikipedia. Compare o diagrama do banco de dados aqui , particularmente a tabela de revisão .

Dependendo de quais tecnologias você estiver usando, você precisará encontrar alguns bons algoritmos de diferença / mesclagem.

Verifique esta pergunta se for para .NET.

CMS
fonte
30

No mundo do BI, você pode fazer isso adicionando startDate e endDate à tabela que deseja versão. Quando você insere o primeiro registro na tabela, o startDate é preenchido, mas o endDate é nulo. Ao inserir o segundo registro, você também atualiza o endDate do primeiro registro com o startDate do segundo registro.

Quando você deseja visualizar o registro atual, seleciona aquele em que endDate é nulo.

Às vezes, isso é chamado de Dimensão de alteração lenta do tipo 2 . Veja também TupleVersioning

Dave Neeley
fonte
Minha tabela não ficará muito grande usando essa abordagem?
Niels Bosma
1
Sim, mas você pode lidar com isso indexando e / ou particionando a tabela. Além disso, haverá apenas um pequeno punhado de mesas grandes. A maioria será muito menor.
ConcernedOfTunbridgeWells
Se não me engano, a única queda aqui é que ela limita as alterações a uma vez por segundo, corretas?
Pimbrouwers
@pimbrouwers sim, depende em última análise da precisão dos campos e da função que os preenche.
Dave Neeley
9

Atualize para o SQL 2008.

Tente usar o SQL Change Tracking, no SQL 2008. Em vez de timestamping e hacks de coluna de marca para exclusão, você pode usar esse novo recurso para rastrear alterações nos dados em seu banco de dados.

Rastreamento de alterações do MSDN SQL 2008

D3vtr0n
fonte
7

Só queria acrescentar que uma boa solução para esse problema é usar um banco de dados Temporal . Muitos fornecedores de banco de dados oferecem esse recurso imediatamente ou por meio de uma extensão. Eu usei com sucesso a extensão da tabela temporal com o PostgreSQL, mas outros também. Sempre que você atualiza um registro no banco de dados, ele também mantém a versão anterior desse registro.

wuher
fonte
6

Duas opções:

  1. Ter uma tabela de histórico - insira os dados antigos nessa tabela de histórico sempre que o original for atualizado.
  2. Tabela de auditoria - armazene os valores antes e depois - apenas para as colunas modificadas em uma tabela de auditoria, juntamente com outras informações, como quem atualizou e quando.
alok
fonte
5

Você pode executar a auditoria em uma tabela SQL por meio de gatilhos SQL. Em um gatilho, você pode acessar 2 tabelas especiais ( inseridas e excluídas ). Essas tabelas contêm as linhas exatas que foram inseridas ou excluídas toda vez que a tabela é atualizada. No SQL de gatilho, você pode pegar essas linhas modificadas e inseri-las na tabela de auditoria. Essa abordagem significa que sua auditoria é transparente para o programador; exigindo nenhum esforço deles ou qualquer conhecimento de implementação.

O bônus adicional dessa abordagem é que a auditoria ocorrerá independentemente de a operação sql ter ocorrido por meio de suas DLLs de acesso a dados ou por meio de uma consulta SQL manual; (como a auditoria é realizada no próprio servidor).

Doctor Jones
fonte
3

Você não diz qual banco de dados, e eu não o vejo nas tags de postagem. Se for para Oracle, posso recomendar a abordagem incorporada no Designer: use tabelas de diário . Se for para qualquer outro banco de dados, bem, eu basicamente recomendo da mesma forma também ...

O modo como funciona, caso você queira replicá-lo em outro banco de dados ou talvez apenas queira entendê-lo, é que para uma tabela também é criada uma tabela de sombra, apenas uma tabela de banco de dados normal, com as mesmas especificações de campo , além de alguns campos extras: como qual ação foi executada pela última vez (sequência, valores típicos "INS" para inserção, "UPD" para atualização e "DEL" para exclusão), data e hora para quando a ação ocorreu e ID do usuário para quem fez isto.

Por meio de gatilhos, toda ação em qualquer linha da tabela insere uma nova linha na tabela de diário com os novos valores, que ação foi executada, quando e por qual usuário. Você nunca exclui nenhuma linha (pelo menos nos últimos meses). Sim, ele crescerá grande, com facilidade milhões de linhas, mas você pode rastrear facilmente o valor de qualquer registro a qualquer momento desde o início do registro no diário ou as linhas antigas foram eliminadas pela última vez e quem fez a última alteração.

No Oracle, tudo que você precisa é gerado automaticamente como código SQL, tudo que você precisa fazer é compilar / executá-lo; e ele vem com um aplicativo CRUD básico (na verdade, apenas "R") para inspecioná-lo.

bart
fonte
2

Eu também estou fazendo a mesma coisa. Estou fazendo um banco de dados para planos de aula. Esses planos precisam de flexibilidade de versão de mudança atômica. Em outras palavras, cada mudança nos planos de aula, por menor que seja, precisa ser permitida, mas a versão antiga também precisa ser mantida intacta. Dessa forma, os criadores das lições podem editar os planos de aula enquanto os alunos os estiverem usando.

A maneira como funcionaria é que, uma vez que um aluno tenha feito uma lição, seus resultados serão anexados à versão que eles concluíram. Se uma alteração for feita, seus resultados sempre apontarão para sua versão.

Dessa forma, se um critério de lição for excluído ou movido, seus resultados não serão alterados.

No momento, estou fazendo isso manipulando todos os dados em uma tabela. Normalmente, eu teria apenas um campo de identificação, mas com esse sistema, estou usando uma identificação e um sub_id. O sub_id sempre permanece na linha, por meio de atualizações e exclusões. O ID é incrementado automaticamente. O software do plano de aula será vinculado ao sub_id mais novo. Os resultados do aluno serão vinculados ao ID. Também incluí um carimbo de data e hora para rastrear quando as alterações ocorreram, mas não é necessário lidar com o controle de versão.

Uma coisa que posso mudar, depois de testá-lo, é usar a ideia nula endDate mencionada anteriormente. No meu sistema, para encontrar a versão mais recente, eu precisaria encontrar o máximo (id). O outro sistema procura apenas endDate = null. Não tenho certeza se os benefícios estão saindo com outro campo de data.

Meus dois centavos.

Jordânia
fonte
2

Enquanto @WW. A resposta é uma boa resposta. Outra maneira é criar uma coluna de versão e manter todas as suas versões na mesma tabela.

Para uma abordagem de tabela, você:

  • Use uma bandeira para indicar as últimas ala Word Press
  • OU faça um desagradável maior que a versão outer join.

Um exemplo de SQL do outer joinmétodo usando números de revisão é:

SELECT tc.*
FROM text_content tc
LEFT OUTER JOIN text_content mc ON tc.path = mc.path
AND mc.revision > tc.revision
WHERE mc.revision is NULL 
AND tc.path = '/stuff' -- path in this case is our natural id.

A má notícia é que as opções acima requerem uma outer joinjunção externa e pode ser lenta. A boa notícia é que criar novas entradas é teoricamente mais barato, porque você pode fazê-lo em uma operação de gravação sem transações (assumindo que seu banco de dados seja atômico).

Um exemplo para uma nova revisão '/stuff'pode ser:

INSERT INTO text_content (id, path, data, revision, revision_comment, enabled, create_time, update_time)
(
SELECT
(md5(random()::text)) -- {id}
, tc.path
, 'NEW' -- {data}
, (tc.revision + 1)
, 'UPDATE' -- {comment}
, 't' -- {enabled}
, tc.create_time
, now() 
FROM text_content tc
LEFT OUTER JOIN text_content mc ON tc.path = mc.path
AND mc.revision > tc.revision
WHERE mc.revision is NULL 
AND tc.path = '/stuff' -- {path}
)

Nós inserimos usando os dados antigos. Isso é particularmente útil se, por exemplo, você quiser atualizar apenas uma coluna e evitar transações e / ou bloqueio otimistas.

A abordagem de sinalizador e a tabela de histórico requerem dois linhas sejam inseridas / atualizadas.

A outra vantagem da outer joinabordagem do número de revisão é que você sempre pode refatorar a abordagem de várias tabelas posteriormente com gatilhos, porque seu gatilho deve essencialmente fazer algo como o descrito acima.

Adam Gent
fonte
2

Alok sugeriu Audit table acima, eu gostaria de explicá-lo no meu post.

Adotei esse design de tabela única sem esquema no meu projeto.

Esquema:

  • id - INCREMENTO AUTOMÁTICO INTEGER
  • nome de usuário - STRING
  • tablename - STRING
  • oldvalue - TEXT / JSON
  • newvalue - TEXT / JSON
  • createdon - DATETIME

Esta tabela pode conter registros históricos de cada tabela em um único local, com o histórico completo do objeto em um registro. Esta tabela pode ser preenchida usando gatilhos / ganchos nos quais os dados são alterados, armazenando o instantâneo de valor antigo e novo da linha de destino.

Profissionais com este design:

  • Menor número de tabelas para gerenciar o gerenciamento de histórico.
  • Armazena a captura instantânea completa de cada estado antigo e novo de cada linha.
  • Fácil de pesquisar em cada tabela.
  • Pode criar partição por tabela.
  • Pode definir a política de retenção de dados por tabela.

Contras com este design:

  • O tamanho dos dados pode ser grande, se o sistema tiver alterações frequentes.
Hassan Farid
fonte