Pergunta

Temos um requisito no projeto para armazenar todas as revisões (histórico de alterações) das entidades no banco de dados.Atualmente temos 2 propostas desenhadas para isso:

por exemplo.para Entidade "Funcionário"

Projeto 1:

-- Holds Employee Entity
"Employees (EmployeeId, FirstName, LastName, DepartmentId, .., ..)"

-- Holds the Employee Revisions in Xml. The RevisionXML will contain
-- all data of that particular EmployeeId
"EmployeeHistories (EmployeeId, DateModified, RevisionXML)"

Projeto 2:

-- Holds Employee Entity
"Employees (EmployeeId, FirstName, LastName, DepartmentId, .., ..)"

-- In this approach we have basically duplicated all the fields on Employees 
-- in the EmployeeHistories and storing the revision data.
"EmployeeHistories (EmployeeId, RevisionId, DateModified, FirstName, 
      LastName, DepartmentId, .., ..)"

Existe alguma outra maneira de fazer isso?

O problema com o "Design 1" é que temos que analisar o XML sempre que precisarmos acessar os dados.Isso retardará o processo e também adicionará algumas limitações, como não podermos adicionar junções nos campos de dados de revisões.

E o problema com o "Design 2" é que temos que duplicar cada campo em todas as entidades (temos cerca de 70-80 entidades para as quais queremos manter revisões).

Foi útil?

Solução

  1. Fazer não coloque tudo em uma tabela com um atributo discriminador IsCurrent.Isso só causa problemas no futuro, requer chaves substitutas e todo tipo de outros problemas.
  2. O Design 2 tem problemas com alterações de esquema.Se você alterar a tabela Employees, deverá alterar a tabela EmployeeHistories e todos os sprocs relacionados que a acompanham.Potencialmente duplica seu esforço de mudança de esquema.
  3. O Design 1 funciona bem e, se feito corretamente, não custa muito em termos de impacto no desempenho.Você poderia usar um esquema xml e até índices para superar possíveis problemas de desempenho.Seu comentário sobre a análise do xml é válido, mas você pode facilmente criar uma visualização usando xquery - que pode ser incluída em consultas e associada.Algo assim...
CREATE VIEW EmployeeHistory
AS
, FirstName, , DepartmentId

SELECT EmployeeId, RevisionXML.value('(/employee/FirstName)[1]', 'varchar(50)') AS FirstName,

  RevisionXML.value('(/employee/LastName)[1]', 'varchar(100)') AS LastName,

  RevisionXML.value('(/employee/DepartmentId)[1]', 'integer') AS DepartmentId,

FROM EmployeeHistories 

Outras dicas

Acho que a pergunta principal a ser feita aqui é 'Quem/O que usará o histórico'?

Se for principalmente para relatórios/histórico legível por humanos, implementamos esse esquema no passado...

Crie uma tabela chamada 'AuditTrail' ou algo que tenha os seguintes campos...

[ID] [int] IDENTITY(1,1) NOT NULL,
[UserID] [int] NULL,
[EventDate] [datetime] NOT NULL,
[TableName] [varchar](50) NOT NULL,
[RecordID] [varchar](20) NOT NULL,
[FieldName] [varchar](50) NULL,
[OldValue] [varchar](5000) NULL,
[NewValue] [varchar](5000) NULL

Você pode então adicionar uma coluna 'LastUpdatedByUserID' a todas as suas tabelas, que deve ser definida sempre que você fizer uma atualização/inserção na tabela.

Você pode então adicionar um gatilho a cada tabela para capturar qualquer inserção/atualização que aconteça e criar uma entrada nesta tabela para cada campo que for alterado.Como a tabela também está sendo fornecida com o 'LastUpdateByUserID' para cada atualização/inserção, você pode acessar esse valor no trigger e utilizá-lo ao adicionar na tabela de auditoria.

Usamos o campo RecordID para armazenar o valor do campo-chave da tabela que está sendo atualizada.Se for uma chave combinada, apenas fazemos uma concatenação de strings com um '~' entre os campos.

Tenho certeza de que este sistema pode ter desvantagens - para bancos de dados muito atualizados, o desempenho pode ser prejudicado, mas para meu aplicativo da web, obtemos muito mais leituras do que gravações e parece estar funcionando muito bem.Até escrevemos um pequeno utilitário VB.NET para escrever automaticamente os gatilhos com base nas definições da tabela.

Apenas um pensamento!

O Tabelas de histórico artigo no Programador de banco de dados blog pode ser útil - aborda alguns dos pontos levantados aqui e discute o armazenamento de deltas.

Editar

No Tabelas de histórico ensaio, o autor (Kenneth Downs), recomenda manter uma tabela de histórico com pelo menos sete colunas:

  1. Carimbo de data e hora da mudança,
  2. Usuário que fez a alteração,
  3. Um token para identificar o registro que foi alterado (onde o histórico é mantido separadamente do estado atual),
  4. Quer a alteração tenha sido uma inserção, atualização ou exclusão,
  5. O valor antigo,
  6. O novo valor,
  7. O delta (para alterações em valores numéricos).

Colunas que nunca mudam, ou cujo histórico não é obrigatório, não devem ser rastreadas na tabela de histórico para evitar inchaço.Armazenar o delta para valores numéricos pode facilitar as consultas subsequentes, mesmo que ele possa ser derivado dos valores antigos e novos.

A tabela de histórico deve ser segura, impedindo que usuários que não sejam do sistema insiram, atualizem ou excluam linhas.Apenas a limpeza periódica deve ser suportada para reduzir o tamanho geral (e se permitido pelo caso de uso).

Implementamos uma solução muito semelhante à solução sugerida por Chris Roberts e que funciona muito bem para nós.

A única diferença é que armazenamos apenas o novo valor.Afinal, o valor antigo é armazenado na linha anterior do histórico

[ID] [int] IDENTITY(1,1) NOT NULL,
[UserID] [int] NULL,
[EventDate] [datetime] NOT NULL,
[TableName] [varchar](50) NOT NULL,
[RecordID] [varchar](20) NOT NULL,
[FieldName] [varchar](50) NULL,
[NewValue] [varchar](5000) NULL

Digamos que você tenha uma tabela com 20 colunas.Dessa forma, você só precisa armazenar a coluna exata que foi alterada, em vez de armazenar a linha inteira.

Evite o Projeto 1;não é muito útil, pois você precisará, por exemplo, reverter para versões antigas dos registros - automaticamente ou "manualmente" usando o console do administrador.

Eu realmente não vejo desvantagens do Design 2.Acho que a segunda tabela Histórico deve conter todas as colunas presentes na primeira tabela Registros.Por exemplo.no mysql você pode facilmente criar uma tabela com a mesma estrutura de outra tabela (create table X like Y).E, quando você estiver prestes a alterar a estrutura da tabela Records em seu banco de dados ativo, você deverá usar alter table comandos de qualquer maneira - e não há grande esforço em executar esses comandos também para sua tabela de Histórico.

Notas

  • A tabela de registros contém apenas a última revisão;
  • A tabela Histórico contém todas as revisões anteriores de registros na tabela Registros;
  • A chave primária da tabela Histórico é uma chave primária da tabela Registros com adição RevisionId coluna;
  • Pense em campos auxiliares adicionais como ModifiedBy - o usuário que criou a revisão específica.Você também pode querer ter um campo DeletedBy para rastrear quem excluiu uma revisão específica.
  • Pensar sobre o que DateModified deveria significar - ou significa onde esta revisão específica foi criada, ou significará quando esta revisão específica foi substituída por outra.O primeiro exige que o campo esteja na tabela Registros e parece mais intuitivo à primeira vista;a segunda solução, no entanto, parece ser mais prática para registros excluídos (data em que esta revisão específica foi excluída).Se você optar pela primeira solução, provavelmente precisará de um segundo campo DateDeleted (somente se você precisar, é claro).Depende de você e do que você realmente deseja gravar.

As operações no Design 2 são muito triviais:

Modificar
  • copie o registro da tabela Records para a tabela History, forneça um novo RevisionId (se ainda não estiver presente na tabela Records), manipule DateModified (depende de como você o interpreta, veja as notas acima)
  • continue com a atualização normal do registro na tabela Records
Excluir
  • faça exatamente o mesmo que na primeira etapa da operação Modificar.Trate DateModified/DateDeleted adequadamente, dependendo da interpretação que você escolheu.
Recuperar (ou reverter)
  • pegue a revisão mais alta (ou alguma em particular?) da tabela Histórico e copie-a para a tabela Registros
Listar o histórico de revisões para um registro específico
  • selecione na tabela Histórico e na tabela Registros
  • pense exatamente no que você espera desta operação;provavelmente determinará quais informações você precisa dos campos DateModified/DateDeleted (veja as notas acima)

Se você optar pelo Design 2, todos os comandos SQL necessários para isso serão muito fáceis, assim como a manutenção!Talvez seja muito mais fácil se você usar as colunas auxiliares (RevisionId, DateModified) também na tabela Records - para manter ambas as tabelas exatamente na mesma estrutura (exceto para chaves exclusivas)!Isso permitirá comandos SQL simples, que serão tolerantes a qualquer alteração na estrutura de dados:

insert into EmployeeHistory select * from Employe where ID = XX

Não se esqueça de usar transações!

Quanto à escalação, esta solução é muito eficiente, pois você não transforma nenhum dado do XML para frente e para trás, apenas copiando linhas inteiras da tabela - consultas muito simples, usando índices - muito eficiente!

Se você precisar armazenar o histórico, crie uma tabela sombra com o mesmo esquema da tabela que você está rastreando e uma coluna 'Data de revisão' e 'Tipo de revisão' (por exemplo,'excluir', 'atualizar').Escreva (ou gere – veja abaixo) um conjunto de gatilhos para preencher a tabela de auditoria.

É bastante simples criar uma ferramenta que leia o dicionário de dados do sistema para uma tabela e gere um script que crie a tabela sombra e um conjunto de gatilhos para preenchê-la.

Não tente usar XML para isso, o armazenamento XML é muito menos eficiente do que o armazenamento de tabelas de banco de dados nativo que esse tipo de gatilho usa.

Ramesh, estive envolvido no desenvolvimento de um sistema baseado na primeira abordagem.
Descobriu-se que o armazenamento de revisões como XML está levando a um enorme crescimento do banco de dados e retardando significativamente as coisas.
Minha abordagem seria ter uma tabela por entidade:

Employee (Id, Name, ... , IsActive)  

onde Está ativo é um sinal da versão mais recente

Se você quiser associar algumas informações adicionais às revisões, poderá criar uma tabela separada contendo essas informações e vinculando-as a tabelas de entidade usando a relação PK\FK.

Dessa forma você pode armazenar todas as versões dos funcionários em uma tabela.Prós desta abordagem:

  • Estrutura de banco de dados simples
  • Não há conflitos, pois a tabela se torna somente anexada
  • Você pode reverter para a versão anterior simplesmente alterando o sinalizador IsActive
  • Não há necessidade de junções para obter o histórico do objeto

Observe que você deve permitir que a chave primária não seja exclusiva.

A maneira como vi isso ser feito no passado é

Employees (EmployeeId, DateModified, < Employee Fields > , boolean isCurrent );

Você nunca "atualiza" esta tabela (exceto para alterar a validade de isCurrent), apenas insere novas linhas.Para qualquer EmployeeId, apenas 1 linha pode ter isCurrent == 1.

A complexidade de manter isso pode ser ocultada por visualizações e gatilhos "em vez de" (no Oracle, presumo coisas semelhantes em outros RDBMS), você pode até ir para visualizações materializadas se as tabelas forem muito grandes e não puderem ser manipuladas por índices) .

Este método é bom, mas você pode acabar com algumas consultas complexas.

Pessoalmente, gosto muito da sua maneira de fazer isso no Design 2, que também foi como fiz no passado.É simples de entender, simples de implementar e simples de manter.

Ele também cria muito pouca sobrecarga para o banco de dados e o aplicativo, especialmente ao realizar consultas de leitura, que provavelmente será o que você fará 99% do tempo.

Também seria muito fácil automatizar a criação das tabelas de histórico e dos gatilhos a serem mantidos (assumindo que isso seria feito por meio de gatilhos).

Vou compartilhar com vocês meu design e ele é diferente dos dois porque requer uma tabela para cada tipo de entidade.Descobri que a melhor maneira de descrever qualquer design de banco de dados é através do ERD, aqui está o meu:

enter image description here

Neste exemplo temos uma entidade chamada funcionário. do utilizador tabela contém os registros dos seus usuários e entidade e entidade_revisão são duas tabelas que contêm o histórico de revisões para todos os tipos de entidades que você terá em seu sistema.Veja como esse design funciona:

Os dois campos de id_da_entidade e revisão_id

Cada entidade em seu sistema terá um ID de entidade exclusivo.Sua entidade pode passar por revisões, mas seu entidade_id permanecerá o mesmo.Você precisa manter esse ID de entidade na tabela de funcionários (como uma chave estrangeira).Você também deve armazenar o tipo de sua entidade no arquivo entidade mesa (ex.'funcionário').Já o revision_id, como o próprio nome mostra, ele acompanha as revisões da sua entidade.A melhor maneira que encontrei para isso é usar o ID do Empregado como seu revision_id.Isso significa que você terá IDs de revisão duplicados para diferentes tipos de entidades, mas isso não é um prazer para mim (não tenho certeza sobre o seu caso).A única observação importante a fazer é que a combinação de entidade_id e revision_id deve ser única.

Há também um estado campo dentro entidade_revisão tabela que indicava o estado da revisão.Pode ter um dos três estados: latest, obsolete ou deleted (não depender da data das revisões ajuda muito a potencializar suas dúvidas).

Uma última observação sobre revision_id: não criei uma chave estrangeira conectando Employee_id a revision_id porque não queremos alterar a tabela entidade_revision para cada tipo de entidade que possamos adicionar no futuro.

INSERÇÃO

Para cada funcionário que deseja inserir no banco de dados, você também adicionará um registro ao entidade e entidade_revisão.Esses dois últimos registros ajudarão você a saber por quem e quando um registro foi inserido no banco de dados.

ATUALIZAR

Cada atualização de um registro de funcionário existente será implementada como duas inserções, uma na tabela de funcionários e outra na entidade_revisão.O segundo irá ajudá-lo a saber por quem e quando o registro foi atualizado.

ELIMINAÇÃO

Para excluir um funcionário, um registro é inserido em entidade_revisão informando a exclusão e pronto.

Como você pode ver neste design, nenhum dado é alterado ou removido do banco de dados e, mais importante, cada tipo de entidade requer apenas uma tabela.Pessoalmente, acho este design muito flexível e fácil de trabalhar.Mas não tenho certeza sobre você, pois suas necessidades podem ser diferentes.

[ATUALIZAR]

Tendo suporte a partições nas novas versões do MySQL, acredito que meu design também vem com um dos melhores desempenhos.Pode-se particionar entity tabela usando type campo enquanto partição entity_revision usando seu state campo.Isto irá impulsionar o SELECT consultas de longe, mantendo o design simples e limpo.

Se de fato uma trilha de auditoria for tudo que você precisa, eu preferiria a solução de tabela de auditoria (completa com cópias desnormalizadas da coluna importante em outras tabelas, por exemplo, UserName).Tenha em mente, porém, que a amarga experiência indica que uma única tabela de auditoria será um enorme gargalo no futuro;provavelmente vale a pena criar tabelas de auditoria individuais para todas as tabelas auditadas.

Se você precisar rastrear as versões históricas reais (e/ou futuras), a solução padrão é rastrear a mesma entidade com várias linhas usando alguma combinação de valores de início, fim e duração.Você pode usar uma visualização para facilitar o acesso aos valores atuais.Se essa for a abordagem adotada, você poderá ter problemas se seus dados com versão fizerem referência a dados mutáveis, mas sem versão.

As revisões de dados são um aspecto do 'Tempo válido'conceito de banco de dados temporal.Muitas pesquisas foram feitas sobre isso e muitos padrões e diretrizes surgiram.Escrevi uma resposta longa com um monte de referências a esse pergunta para os interessados.

Se você quiser fazer o primeiro, talvez queira usar XML para a tabela Employees também.A maioria dos bancos de dados mais recentes permite consultar campos XML, portanto isso nem sempre é um problema.E pode ser mais simples ter uma maneira de acessar os dados dos funcionários, independentemente de ser a versão mais recente ou anterior.

Eu tentaria a segunda abordagem.Você poderia simplificar isso tendo apenas uma tabela Employees com um campo DateModified.O EmployeeId + DateModified seria a chave primária e você pode armazenar uma nova revisão apenas adicionando uma linha.Desta forma, arquivar versões mais antigas e restaurar versões do arquivo também é mais fácil.

Outra maneira de fazer isso poderia ser modelo de cofre de dados por Dan Linstedt.Fiz um projeto para o departamento de estatística holandês que utilizou esse modelo e funciona muito bem.Mas não acho que seja diretamente útil para o uso diário do banco de dados.Você pode obter algumas idéias lendo seus artigos.

Que tal:

  • ID do Empregado
  • Data modificada
    • e/ou número de revisão, dependendo de como você deseja rastreá-lo
  • Modificado por USerId
    • além de qualquer outra informação que você deseja rastrear
  • Campos de funcionários

Você cria a chave primária (EmployeeId, DateModified) e, para obter os registros "atuais", basta selecionar MAX(DateModified) para cada EmployeeID.Armazenar um IsCurrent é uma péssima ideia porque, em primeiro lugar, ele pode ser calculado e, em segundo lugar, é muito fácil os dados ficarem fora de sincronia.

Você também pode criar uma visualização que liste apenas os registros mais recentes e usá-la principalmente enquanto trabalha em seu aplicativo.O bom dessa abordagem é que você não tem dados duplicados e não precisa coletar dados de dois locais diferentes (atual em Employees e arquivado em EmployeesHistory) para obter todo o histórico ou reversão, etc.) .

Se você quiser confiar em dados históricos (por motivos de relatórios), você deve usar uma estrutura semelhante a esta:

// Holds Employee Entity
"Employees (EmployeeId, FirstName, LastName, DepartmentId, .., ..)"

// Holds the Employee revisions in rows.
"EmployeeHistories (HistoryId, EmployeeId, DateModified, OldValue, NewValue, FieldName)"

Ou solução global para aplicação:

// Holds Employee Entity
"Employees (EmployeeId, FirstName, LastName, DepartmentId, .., ..)"

// Holds all entities revisions in rows.
"EntityChanges (EntityName, EntityId, DateModified, OldValue, NewValue, FieldName)"

Você pode salvar suas revisões também em XML, então você terá apenas um registro para uma revisão.Isso será parecido com:

// Holds Employee Entity
"Employees (EmployeeId, FirstName, LastName, DepartmentId, .., ..)"

// Holds all entities revisions in rows.
"EntityChanges (EntityName, EntityId, DateModified, XMLChanges)"

Tivemos requisitos semelhantes e descobrimos que muitas vezes o usuário só quer ver o que foi alterado, não necessariamente reverterá quaisquer alterações.

Não tenho certeza de qual é o seu caso de uso, mas o que fizemos foi criar uma tabela de auditoria que é atualizada automaticamente com alterações em uma entidade comercial, incluindo o nome amigável de quaisquer referências e enumerações de chave estrangeira.

Sempre que o usuário salva suas alterações recarregamos o objeto antigo, fazemos uma comparação, registramos as alterações e salvamos a entidade (tudo é feito em uma única transação no banco de dados caso haja algum problema).

Isso parece funcionar muito bem para nossos usuários e nos poupa da dor de cabeça de ter uma tabela de auditoria completamente separada com os mesmos campos de nossa entidade comercial.

Parece que você deseja acompanhar alterações em entidades específicas ao longo do tempo, por exemplo.ID 3, "bob", "123 main street", depois outro ID 3, "bob" "234 elm st" e assim por diante, em essência, sendo capaz de vomitar um histórico de revisão mostrando todos os endereços em que "bob" esteve .

A melhor maneira de fazer isso é ter um campo "é atual" em cada registro e (provavelmente) um carimbo de data/hora ou FK para uma tabela de data/hora.

As inserções devem então definir "é atual" e também remover a definição de "é atual" no registro "é atual" anterior.As consultas devem especificar "é atual", a menos que você queira todo o histórico.

Há mais ajustes se for uma tabela muito grande ou se for esperado um grande número de revisões, mas esta é uma abordagem bastante padrão.

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top