Versão que controla o conteúdo de um banco de dados

16

Estou trabalhando em um projeto da web que envolve conteúdo editável pelo usuário e gostaria de poder acompanhar a versão do conteúdo real, que fica em um banco de dados. Basicamente, quero implementar históricos de alterações no estilo wiki.

Fazendo algumas pesquisas em segundo plano, vejo muita documentação sobre como versionar o esquema do banco de dados (o meu já está realmente controlado), mas qualquer estratégia existente sobre como rastrear as alterações no conteúdo do banco de dados é perdida na avalanche de material de versão do esquema, pelo menos nas minhas pesquisas.

Posso pensar em algumas maneiras de implementar meu próprio controle de alterações, mas todas elas parecem bastante grosseiras:

  • Salve a linha inteira em cada alteração, relacione a linha de volta ao ID de origem com uma chave Primária (no que estou inclinado atualmente, é a mais simples). Muitas pequenas alterações podem produzir muita inchaço na mesa, no entanto.
  • salve antes / depois / usuário / registro de data e hora para cada alteração, com um nome de coluna para relacionar a alteração novamente à coluna relevante.
  • salve antes / depois / usuário / carimbo de data / hora com uma tabela para cada coluna (resultaria em muitas tabelas).
  • salve diffs / user / timestamp para cada alteração com uma coluna (isso significa que você precisará percorrer todo o histórico de alterações para voltar a uma determinada data).

Qual é a melhor abordagem aqui? Rolar sozinho parece que provavelmente estou reinventando a (melhor) base de código de outra pessoa.


Pontos de bônus para o PostgreSQL.

Nome falso
fonte
Esta questão já foi discutida no SO: stackoverflow.com/questions/3874199/… . Pesquise no Google por "histórico de registros do banco de dados" e você encontrará mais alguns artigos.
Doc Brown)
1
Soa como um candidato ideal para Fornecimento Evento
James
Por que não usar o log de transações do SQL-Server para executar o truque?
Thomas Junk

Respostas:

11

A técnica que eu normalmente usei é salvar o registro completo, com um campo end_timestamp. Existe uma regra de negócios em que apenas uma linha pode ter um end_timestamp nulo, e esse é, naturalmente, o conteúdo ativo no momento.

Se você adotar esse sistema, recomendo fortemente que você adicione um índice ou restrição para aplicar a regra. Isso é fácil com o Oracle, pois um índice exclusivo pode conter um e apenas um nulo. Outros bancos de dados podem ser mais um problema. A aplicação da regra ao banco de dados manterá seu código honesto.

Você está certo de que muitas pequenas alterações criarão inchaço, mas é necessário trocar isso com o código e a simplicidade dos relatórios.

kiwiron
fonte
Observe que outros mecanismos de banco de dados podem se comportar de maneira diferente, por exemplo, o MySQL permite vários valores NULL em uma coluna com índice exclusivo. Isso torna essa restrição muito mais difícil de impor.
Qd #
O uso de um carimbo de data / hora real não é seguro, mas alguns bancos de dados MVCC funcionam internamente, armazenando números de série mínimo e máximo de transações junto com tuplas.
User2313838
"Isso é fácil com o Oracle, pois um índice exclusivo pode conter um e apenas um nulo". Errado. O Oracle não inclui valores nulos em índices. Não há limite para o número de nulos em uma coluna com um índice exclusivo.
Gerrat
@Gerrat Faz vários anos que eu projetei um banco de dados que tinha esse requisito e não tenho mais acesso a esse banco de dados. Você está certo de que um índice exclusivo padrão pode suportar vários valores nulos, mas acho que usamos uma restrição exclusiva ou possivelmente um índice funcional.
Kiwiron
8

Observe que, se você usa o Microsoft SQL Server, já existe um recurso chamado Change Data Capture . Você ainda precisará escrever o código para acessar as revisões anteriores mais tarde (o CDC cria visualizações específicas para isso), mas pelo menos não precisa alterar o esquema de suas tabelas nem implementar o próprio rastreamento de alterações.

Sob o capô , o que acontece é que:

  • O CDC cria uma tabela adicional contendo as revisões,

  • Sua tabela original é usada como antes, ou seja, qualquer atualização é refletida diretamente nesta tabela,

  • A tabela CDC armazena apenas os valores alterados, o que significa que a duplicação de dados é mantida no mínimo.

O fato de as alterações serem armazenadas em uma tabela diferente tem duas consequências principais:

  • As seleções da tabela original são tão rápidas quanto sem CDC. Se bem me lembro, o CDC acontece após a atualização, portanto as atualizações são igualmente rápidas (embora não me lembre bem como o CDC gerencia a consistência dos dados).

  • Algumas alterações no esquema da tabela original levam à remoção do CDC. Por exemplo, se você adicionar uma coluna, o CDC não saberá como lidar com isso. Por outro lado, adicionar um índice ou restrição deve ser bom. Isso rapidamente se torna um problema se você ativar o CDC em uma tabela sujeita a alterações frequentes. Pode haver uma solução que permita alterar o esquema sem perder o CDC, mas não o procurei.

Arseni Mourzenko
fonte
6

Resolva o problema "filosoficamente" e primeiro no código. E então "negocie" com código e banco de dados para que isso aconteça.

Como exemplo , se você estiver lidando com artigos genéricos, um conceito inicial para um artigo pode ser assim:

class Article {
  public Int32 Id;
  public String Body;
}

E no próximo nível mais básico, quero manter uma lista de revisões:

class Article {
  public Int32 Id;
  public String Body;
  public List<String> Revisions;
}

E posso me dar conta de que o corpo atual é apenas a revisão mais recente. E isso significa duas coisas: preciso que cada revisão seja datada ou numerada:

class Revision {
  public Int32 Id;
  public Article ParentArticle;
  public DateTime Created;
  public String Body;
}

E ... e o corpo atual do artigo não precisa ser diferente da revisão mais recente:

class Article {
  public Int32 Id;
  public String Body {
    get {
      return (Revisions.OrderByDesc(r => r.Created))[0];
    }
    set {
      Revisions.Add(new Revision(value));
    }
  }
  public List<Revision> Revisions;
}

Faltam alguns detalhes; mas ilustra que você provavelmente deseja duas entidades . Um representa o artigo (ou outro tipo de cabeçalho) e o outro é uma lista de revisões (agrupando quaisquer campos que façam sentido "filosófico" bom agrupar). Inicialmente, você não precisa de restrições especiais no banco de dados, porque seu código não se importa com nenhuma das revisões por si só - elas são propriedades de um artigo que conhece revisões.

Portanto, você não precisa se preocupar em sinalizar revisões de nenhuma maneira especial ou se apoiar em uma restrição de banco de dados para marcar o artigo "atual". Você apenas os marca um carimbo de data / hora (até mesmo um ID incluído automaticamente), torna-os relacionados ao artigo pai e deixa o artigo encarregado de saber que o "mais recente" é o mais relevante.

E você permite que um ORM lide com os detalhes menos filosóficos - ou oculte-os em uma classe de utilitário personalizada, se você não estiver usando um ORM pronto para uso.

Muito mais tarde, depois de fazer alguns testes de estresse, você pode pensar em tornar essa propriedade de revisão lenta ou com o atributo Body como carga lenta apenas na revisão mais alta. Mas, nesse caso, sua estrutura de dados não precisa ser alterada para acomodar essas otimizações.

svidgen
fonte
2

Existe uma página wiki do PostgreSQL para um acionador de rastreamento de auditoria, que mostra como configurar um log de auditoria que fará o que você precisa.

Ele rastreia os dados originais completos de uma alteração, bem como a lista de novos valores para atualizações (para inserções e exclusões, há apenas um valor). Se você deseja restaurar uma versão antiga, pode pegar a cópia dos dados originais do registro de auditoria. Observe que, se seus dados envolverem chaves estrangeiras, esses registros também deverão ser revertidos para manter a consistência.

De um modo geral, se seu aplicativo de banco de dados passa a maior parte do tempo apenas com os dados atuais, acho melhor rastrear versões alternativas em uma tabela separada dos dados atuais. Isso manterá seus índices de tabela ativos mais gerenciáveis.

Se as linhas que você está rastreando são muito grandes e o espaço é uma preocupação séria, você pode tentar quebrar as alterações e armazenar diferenças / correções mínimas, mas é definitivamente mais trabalho para cobrir todos os tipos de tipos de dados. Já fiz isso antes e foi difícil reconstruir versões antigas de dados, percorrendo todas as alterações para trás, uma de cada vez.

Ben Turner
fonte
1

Bem, acabei seguindo a opção mais simples, um gatilho que copia a versão antiga de uma linha para um log de histórico por tabela.

Se eu acabar com muito inchaço no banco de dados, posso analisar, possivelmente, algumas das pequenas alterações no histórico, se necessário.

A solução acabou sendo bastante confusa, pois eu queria gerar as funções de gatilho automaticamente. Eu sou SQLAlchemy, então pude produzir a tabela de histórico fazendo alguns hijinks de herança, o que foi bom, mas as funções de acionamento reais acabaram exigindo algumas alterações na string para gerar corretamente as funções do PostgreSQL e mapear as colunas de uma tabela para outro corretamente.

De qualquer forma, está tudo no github aqui .

Nome falso
fonte