Pregunta

Tenemos el requisito en el proyecto de almacenar todas las revisiones (Historial de cambios) de las entidades en la base de datos.Actualmente tenemos 2 propuestas diseñadas para esto:

p.ej.para la entidad "empleado"

Diseño 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)"

Diseño 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, .., ..)"

¿Hay alguna otra forma de hacer esto?

El problema con el "Diseño 1" es que tenemos que analizar XML cada vez que necesitamos acceder a datos.Esto ralentizará el proceso y también agregará algunas limitaciones, como que no podemos agregar uniones en los campos de datos de revisiones.

Y el problema con el "Diseño 2" es que tenemos que duplicar todos y cada uno de los campos de todas las entidades (tenemos alrededor de 70 a 80 entidades para las que queremos mantener revisiones).

¿Fue útil?

Solución

  1. Hacer no Ponlo todo en una tabla con un atributo discriminador IsCurrent.Esto solo causa problemas en el futuro, requiere claves sustitutas y todo tipo de otros problemas.
  2. El diseño 2 tiene problemas con los cambios de esquema.Si cambia la tabla Empleados, debe cambiar la tabla EmployeeHistories y todos los procesos relacionados que la acompañan.Potencialmente duplica su esfuerzo de cambio de esquema.
  3. El diseño 1 funciona bien y, si se hace correctamente, no cuesta mucho en términos de mejora del rendimiento.Podría utilizar un esquema xml e incluso índices para solucionar posibles problemas de rendimiento.Su comentario sobre el análisis del xml es válido, pero puede crear fácilmente una vista usando xquery, que puede incluir en las consultas y unirse.Algo como esto...
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 

Otros consejos

Creo que la pregunta clave que debemos plantearnos aquí es "¿Quién/qué va a utilizar el historial?".

Si va a ser principalmente para informes/historial legible por humanos, hemos implementado este esquema en el pasado...

Cree una tabla llamada 'AuditTrail' o algo que tenga los siguientes 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

Luego puede agregar una columna 'LastUpdatedByUserID' a todas sus tablas, que debe configurarse cada vez que realiza una actualización/inserción en la tabla.

Luego puede agregar un activador a cada tabla para detectar cualquier inserción/actualización que ocurra y crear una entrada en esta tabla para cada campo que haya cambiado.Debido a que la tabla también se proporciona con el 'LastUpdateByUserID' para cada actualización/inserción, puede acceder a este valor en el activador y usarlo al agregarlo a la tabla de auditoría.

Usamos el campo RecordID para almacenar el valor del campo clave de la tabla que se está actualizando.Si es una clave combinada, simplemente hacemos una concatenación de cadenas con un '~' entre los campos.

Estoy seguro de que este sistema puede tener inconvenientes: para bases de datos muy actualizadas, el rendimiento puede verse afectado, pero para mi aplicación web, obtenemos muchas más lecturas que escrituras y parece estar funcionando bastante bien.Incluso escribimos una pequeña utilidad VB.NET para escribir automáticamente los activadores según las definiciones de la tabla.

¡Solo un pensamiento!

El Tablas de historia artículo en el Programador de bases de datos El blog puede ser útil: cubre algunos de los puntos planteados aquí y analiza el almacenamiento de deltas.

Editar

En el Tablas de historia ensayo, el autor (Kenneth Downs), recomienda mantener una tabla histórica de al menos siete columnas:

  1. Marca de tiempo del cambio,
  2. Usuario que realizó el cambio,
  3. Un token para identificar el registro que se modificó (donde el historial se mantiene por separado del estado actual),
  4. Si el cambio fue una inserción, una actualización o una eliminación,
  5. El viejo valor
  6. El nuevo valor,
  7. El delta (para cambios en valores numéricos).

Las columnas que nunca cambian, o cuyo historial no es necesario, no se deben rastrear en la tabla de historial para evitar el exceso.Almacenar el delta de valores numéricos puede facilitar las consultas posteriores, aunque se pueda derivar de los valores nuevos y antiguos.

La tabla de historial debe ser segura y los usuarios que no pertenecen al sistema deben evitar insertar, actualizar o eliminar filas.Solo se debe admitir la purga periódica para reducir el tamaño total (y si lo permite el caso de uso).

Hemos implementado una solución muy similar a la que sugiere Chris Roberts y nos funciona bastante bien.

La única diferencia es que solo almacenamos el nuevo valor.Después de todo, el valor anterior se almacena en la fila del historial anterior.

[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 tienes una tabla con 20 columnas.De esta manera, solo tendrá que almacenar la columna exacta que ha cambiado en lugar de tener que almacenar toda la fila.

Evite el Diseño 1;no es muy útil una vez que necesitará, por ejemplo, revertir a versiones antiguas de los registros, ya sea automáticamente o "manualmente" usando la consola del administrador.

Realmente no veo desventajas del Diseño 2.Creo que la segunda tabla, Historial, debería contener todas las columnas presentes en la primera, tabla Registros.P.ej.En MySQL puedes crear fácilmente una tabla con la misma estructura que otra tabla (create table X like Y).Y, cuando esté a punto de cambiar la estructura de la tabla de Registros en su base de datos en vivo, debe usar alter table comandos de todos modos, y no supone un gran esfuerzo ejecutar estos comandos también para su tabla Historial.

Notas

  • La tabla de registros contiene sólo la última revisión;
  • La tabla de historial contiene todas las revisiones anteriores de los registros en la tabla de registros;
  • La clave principal de la tabla de historial es una clave principal de la tabla de registros con agregado RevisionId columna;
  • Piense en campos auxiliares adicionales como ModifiedBy - el usuario que creó una revisión particular.Es posible que también quieras tener un campo DeletedBy para rastrear quién eliminó una revisión particular.
  • Pensar en qué DateModified debería significar: significa dónde se creó esta revisión en particular, o significará cuándo esta revisión en particular fue reemplazada por otra.El primero requiere que el campo esté en la tabla Registros y parece más intuitivo a primera vista;Sin embargo, la segunda solución parece ser más práctica para los registros eliminados (fecha en la que se eliminó esta revisión en particular).Si opta por la primera solución, probablemente necesitará un segundo campo. DateDeleted (sólo si lo necesitas por supuesto).Depende de ti y de lo que realmente quieras grabar.

Las operaciones en Diseño 2 son muy triviales:

Modificar
  • copie el registro de la tabla Registros a la tabla Historial, asígnele un nuevo RevisionId (si aún no está presente en la tabla Registros), maneje Fecha de modificación (depende de cómo lo interprete, consulte las notas anteriores)
  • continuar con la actualización normal del registro en la tabla de Registros
Borrar
  • Haga exactamente lo mismo que en el primer paso de la operación Modificar.Maneje DateModified/DateDeleted en consecuencia, según la interpretación que haya elegido.
Recuperar (o revertir)
  • tome la revisión más alta (¿o alguna en particular?) de la tabla Historial y cópiela en la tabla Registros
Listar el historial de revisión de un registro particular
  • seleccione de la tabla Historial y la tabla Registros
  • piense qué espera exactamente de esta operación;probablemente determinará qué información necesita de los campos Fecha de modificación/Fecha eliminada (consulte las notas anteriores)

Si opta por el Diseño 2, todos los comandos SQL necesarios para hacerlo serán muy, muy fáciles, ¡así como el mantenimiento!Tal vez será mucho más fácil. si utiliza las columnas auxiliares (RevisionId, DateModified) también en la tabla Registros - para mantener ambas tablas exactamente en la misma estructura (excepto claves únicas)!Esto permitirá comandos SQL simples, que serán tolerantes a cualquier cambio en la estructura de datos:

insert into EmployeeHistory select * from Employe where ID = XX

¡No olvides utilizar transacciones!

En cuanto a la escala, esta solución es muy eficiente, ya que no transforma ningún dato de XML de un lado a otro, simplemente copia filas enteras de la tabla (consultas muy simples, usando índices), ¡muy eficiente!

Si tiene que almacenar el historial, cree una tabla paralela con el mismo esquema que la tabla que está rastreando y una columna "Fecha de revisión" y "Tipo de revisión" (p. ej.'eliminar', 'actualizar').Escriba (o genere; consulte a continuación) un conjunto de activadores para completar la tabla de auditoría.

Es bastante sencillo crear una herramienta que lea el diccionario de datos del sistema para una tabla y genere un script que cree la tabla oculta y un conjunto de activadores para completarla.

No intente utilizar XML para esto, el almacenamiento XML es mucho menos eficiente que el almacenamiento de tablas de bases de datos nativas que utiliza este tipo de activador.

Ramesh, estuve involucrado en el desarrollo del sistema basado en el primer enfoque.
Resultó que almacenar revisiones como XML está generando un enorme crecimiento de la base de datos y ralentizando significativamente las cosas.
Mi enfoque sería tener una tabla por entidad:

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

dónde Está activo es una señal de la última versión

Si desea asociar información adicional con revisiones, puede crear una tabla separada que contenga esa información y vincularla con las tablas de entidad utilizando la relación PK fk.

De esta manera puede almacenar todas las versiones de los empleados en una tabla.Ventajas de este enfoque:

  • Estructura de base de datos simple
  • No hay conflictos desde que la tabla pasa a ser solo para anexar
  • Puede retroceder a la versión anterior simplemente cambiando el indicador IsActive
  • No es necesario realizar combinaciones para obtener el historial de objetos.

Tenga en cuenta que debe permitir que la clave principal no sea única.

La forma en que he visto esto hecho en el pasado es

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

Nunca "actualiza" esta tabla (excepto para cambiar la validez de isCurrent), simplemente inserta nuevas filas.Para cualquier Id. de empleado determinado, solo 1 fila puede tener isCurrent == 1.

La complejidad de mantener esto puede ocultarse mediante vistas y activadores "en lugar de" (en Oracle, supongo que hay cosas similares en otros RDBMS), incluso puede ir a vistas materializadas si las tablas son demasiado grandes y no pueden ser manejadas por índices). .

Este método está bien, pero puede terminar con algunas consultas complejas.

Personalmente, me gusta mucho tu forma de hacerlo en Diseño 2, que es como lo he hecho yo también en el pasado.Es sencillo de entender, sencillo de implementar y sencillo de mantener.

También crea muy poca sobrecarga para la base de datos y la aplicación, especialmente cuando se realizan consultas de lectura, que es probablemente lo que hará el 99% del tiempo.

También sería bastante fácil automatizar la creación de tablas de historial y activadores para mantener (suponiendo que se hiciera mediante activadores).

Voy a compartir con ustedes mi diseño y es diferente de ambos diseños en que requiere una tabla para cada tipo de entidad.Descubrí que la mejor manera de describir cualquier diseño de base de datos es a través de ERD, aquí está el mío:

enter image description here

En este ejemplo tenemos una entidad llamada empleado. usuario La tabla contiene los registros de sus usuarios y entidad y revisión_entidad Hay dos tablas que contienen el historial de revisiones de todos los tipos de entidades que tendrá en su sistema.Así es como funciona este diseño:

Los dos campos de id_entidad y id_revisión

Cada entidad en su sistema tendrá una identificación de entidad única.Es posible que su entidad pase por revisiones, pero su id_entidad seguirá siendo el mismo.Debe mantener esta identificación de entidad en su tabla de empleados (como clave externa).También debe almacenar el tipo de su entidad en el entidad mesa (por ej.'empleado').Ahora, en cuanto a revision_id, como su nombre lo muestra, realiza un seguimiento de las revisiones de su entidad.La mejor manera que encontré para esto es usar el ID de empleado como su revision_id.Esto significa que tendrá identificadores de revisión duplicados para diferentes tipos de entidades, pero esto no es un placer para mí (no estoy seguro de su caso).La única nota importante que hay que hacer es que la combinación de id_entidad y id_revisión debe ser única.

También hay un estado campo dentro revisión_entidad cuadro que indicaba el estado de revisión.Puede tener uno de los tres estados: latest, obsolete o deleted (no depender de la fecha de revisiones te ayuda mucho a potenciar tus consultas).

Una última nota sobre revision_id: no creé una clave externa que conecte empleado_id con revision_id porque no queremos alterar la tabla entidad_revisión para cada tipo de entidad que podamos agregar en el futuro.

INSERCIÓN

Para cada empleado que desea insertar en la base de datos, también agregará un registro a entidad y revisión_entidad.Estos dos últimos registros le ayudarán a realizar un seguimiento de quién y cuándo se insertó un registro en la base de datos.

ACTUALIZAR

Cada actualización de un registro de empleado existente se implementará como dos inserciones, una en la tabla de empleados y otra en entidad_revisión.El segundo le ayudará a saber quién y cuándo se actualizó el registro.

SUPRESIÓN

Para eliminar un empleado, se inserta un registro enentity_revision indicando la eliminación y listo.

Como puede ver en este diseño, nunca se modifica ni elimina ningún dato de la base de datos y, lo que es más importante, cada tipo de entidad requiere solo una tabla.Personalmente encuentro este diseño realmente flexible y fácil de trabajar.Pero no estoy seguro de usted, ya que sus necesidades pueden ser diferentes.

[ACTUALIZAR]

Habiendo admitido particiones en las nuevas versiones de MySQL, creo que mi diseño también tiene uno de los mejores rendimientos.Uno puede dividir entity mesa usando type campo durante la partición entity_revision usando su state campo.Esto impulsará la SELECT consultas con diferencia manteniendo el diseño simple y limpio.

Si de hecho todo lo que necesita es un seguimiento de auditoría, me inclinaría por la solución de tabla de auditoría (completa con copias desnormalizadas de la columna importante en otras tablas, por ejemplo, UserName).Tenga en cuenta, sin embargo, que la amarga experiencia indica que una única tabla de auditoría será un gran cuello de botella en el futuro;Probablemente valga la pena el esfuerzo de crear tablas de auditoría individuales para todas sus tablas auditadas.

Si necesita realizar un seguimiento de las versiones históricas reales (y/o futuras), entonces la solución estándar es realizar un seguimiento de la misma entidad con varias filas utilizando alguna combinación de valores de inicio, fin y duración.Puede utilizar una vista para facilitar el acceso a los valores actuales.Si este es el enfoque que adopta, puede tener problemas si sus datos versionados hacen referencia a datos mutables pero no versionados.

Las revisiones de datos son un aspecto del 'tiempo valido'Concepto de Base de Datos Temporal.Se han realizado muchas investigaciones sobre esto y han surgido muchos patrones y pautas.Escribí una respuesta extensa con un montón de referencias a este pregunta para los interesados.

Si desea hacer el primero, es posible que también desee utilizar XML para la tabla Empleados.La mayoría de las bases de datos más nuevas le permiten realizar consultas en campos XML, por lo que esto no siempre es un problema.Y podría ser más sencillo tener una forma de acceder a los datos de los empleados, independientemente de si se trata de la última versión o de una versión anterior.

Sin embargo, probaría el segundo enfoque.Podrías simplificar esto teniendo solo una tabla de Empleados con un campo Fecha de modificación.EmployeeId + DateModified sería la clave principal y puede almacenar una nueva revisión simplemente agregando una fila.De esta manera, también es más fácil archivar versiones anteriores y restaurar versiones desde el archivo.

Otra forma de hacer esto podría ser la modelo de bóveda de datos por Dan Linstedt.Hice un proyecto para la oficina de estadísticas holandesa que utilizó este modelo y funciona bastante bien.Pero no creo que sea directamente útil para el uso diario de bases de datos.Sin embargo, es posible que obtengas algunas ideas leyendo sus artículos.

Qué tal si:

  • ID de empleado
  • Fecha modificada
    • y/o número de revisión, dependiendo de cómo quieras rastrearlo
  • Modificado por ID de usuario
    • además de cualquier otra información que desee rastrear
  • Campos de empleados

Usted crea la clave principal (EmployeeId, DateModified) y para obtener los registros "actuales" simplemente selecciona MAX(DateModified) para cada ID de empleado.Almacenar un IsCurrent es una muy mala idea porque, en primer lugar, se puede calcular y, en segundo lugar, es demasiado fácil que los datos no estén sincronizados.

También puede crear una vista que enumere solo los registros más recientes y utilizarla principalmente mientras trabaja en su aplicación.Lo bueno de este enfoque es que no tiene datos duplicados y no tiene que recopilar datos de dos lugares diferentes (actuales en Empleados y archivados en Historial de empleados) para obtener todo el historial o revertir, etc.) .

Si desea confiar en los datos del historial (por motivos de generación de informes), debe utilizar una estructura similar a esta:

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

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

O solución global para la aplicación:

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

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

Puede guardar sus revisiones también en XML, entonces solo tendrá un registro para una revisión.Esto se verá así:

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

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

Hemos tenido requisitos similares y lo que descubrimos fue que muchas veces el usuario sólo quiere ver lo que se ha cambiado, no necesariamente revertir ningún cambio.

No estoy seguro de cuál es su caso de uso, pero lo que hicimos fue crear una tabla de auditoría que se actualiza automáticamente con los cambios en una entidad comercial, incluido el nombre descriptivo de las referencias y enumeraciones de claves externas.

Cada vez que el usuario guarda sus cambios, recargamos el objeto antiguo, ejecutamos una comparación, registramos los cambios y guardamos la entidad (todo se hace en una sola transacción de base de datos en caso de que haya algún problema).

Esto parece funcionar muy bien para nuestros usuarios y nos ahorra el dolor de cabeza de tener una tabla de auditoría completamente separada con los mismos campos que nuestra entidad comercial.

Parece que desea realizar un seguimiento de los cambios en entidades específicas a lo largo del tiempo, p.ID 3, "bob", "123 main street", luego otro ID 3, "bob" "234 elm st", y así sucesivamente, en esencia, poder vomitar un historial de revisiones que muestra cada dirección en la que ha estado "bob". .

La mejor manera de hacer esto es tener un campo "está actual" en cada registro y (probablemente) una marca de tiempo o FK para una tabla de fecha/hora.

Las inserciones deben luego establecer "está actual" y también desarmar "está actual" en el registro anterior "está actual".Las consultas deben especificar "es actual", a menos que desee todo el historial.

Hay más ajustes si se trata de una tabla muy grande o si se espera una gran cantidad de revisiones, pero este es un enfoque bastante estándar.

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top