Versión que controla los contenidos de una base de datos

16

Estoy trabajando en un proyecto web que incluye contenido editable por el usuario, y me gustaría poder hacer un seguimiento de la versión del contenido real, que vive en una base de datos. Básicamente, quiero implementar historiales de cambio de estilo wiki.

Al investigar un poco, veo mucha documentación sobre cómo versionar el esquema de su base de datos (la mía ya está controlada), pero cualquier estrategia existente sobre cómo rastrear los cambios en el contenido de su base de datos se pierde en la avalancha de material de versiones del esquema, al menos en mis búsquedas

Se me ocurren algunas formas de implementar mi propio seguimiento de cambios, pero todas parecen bastante crudas:

  • Guarde la fila completa en cada cambio, relacione la fila de nuevo con la identificación de origen con una Clave primaria (lo que me estoy inclinando actualmente es la más simple). Sin embargo, muchos pequeños cambios podrían producir mucha hinchazón en la mesa.
  • guarde antes / después / usuario / marca de tiempo para cada cambio, con un nombre de columna para relacionar el cambio con la columna correspondiente.
  • guardar antes / después / usuario / marca de tiempo con una tabla para cada columna (daría como resultado demasiadas tablas).
  • guarde diffs / user / timestamp para cada cambio con una columna (esto significaría que tendría que recorrer todo el historial de cambios que interviene para volver a una fecha determinada).

¿Cuál es el mejor enfoque aquí? Hacer rodar el mío parece que probablemente estoy reinventando la base de código (mejor) de otra persona.


Puntos de bonificación para PostgreSQL.

Nombre falso
fuente
Esta pregunta ya se ha discutido en SO: stackoverflow.com/questions/3874199/… . Google para "historial de registro de base de datos", y encontrará algunos artículos más.
Doc Brown
1
Suena como un candidato ideal para el abastecimiento de eventos
James
¿Por qué no usar el registro de transacciones del servidor SQL para hacer el truco?
Thomas Junk

Respuestas:

11

La técnica que normalmente he usado es guardar el registro completo, con un campo end_timestamp. Existe una regla de negocio de que solo una fila puede tener una nula end_timestamp, y este es, por supuesto, el contenido actualmente activo.

Si adopta este sistema, le recomiendo que agregue un índice o restricción para hacer cumplir la regla. Esto es fácil con Oracle, ya que un índice único puede contener uno y solo un valor nulo. Otras bases de datos pueden ser más problemáticas. Hacer que la base de datos haga cumplir la regla mantendrá su código honesto.

Tiene razón en que muchos cambios pequeños crearán hinchazón, pero debe cambiar esto por el código y la simplicidad de los informes.

kiwiron
fuente
Tenga en cuenta que otros motores de bases de datos pueden comportarse de manera diferente, por ejemplo, MySQL permite múltiples valores NULL en una columna con índice único. Esto hace que esta restricción sea mucho más difícil de aplicar.
qbd
El uso de una marca de tiempo real no es seguro, pero algunas bases de datos de MVCC funcionan internamente almacenando números de serie de transacción mínimos y máximos junto con tuplas.
user2313838
"Esto es fácil con Oracle, ya que un índice único puede contener uno y solo un nulo". Incorrecto. Oracle no incluye valores nulos en los índices en absoluto. No hay límite en el número de nulos en una columna con un índice único.
Gerrat
@Gerrat Han pasado varios años desde que diseñé una base de datos que tenía este requisito, y ya no tengo acceso a esa base de datos. Tiene razón en que un índice único estándar puede admitir múltiples valores nulos, pero creo que usamos una restricción única o posiblemente un índice funcional.
kiwiron
8

Tenga en cuenta que si usa Microsoft SQL Server, ya existe una característica para eso llamada Cambiar captura de datos . Aún tendrá que escribir código para acceder a las revisiones anteriores más tarde (CDC crea vistas específicas para eso), pero al menos no tiene que cambiar el esquema de sus tablas, ni implementar el seguimiento de cambios en sí.

Debajo del capó , lo que sucede es que:

  • Los CDC crean una tabla adicional que contiene las revisiones,

  • Su tabla original se usa como estaba antes, es decir, cualquier actualización se refleja en esta tabla directamente,

  • La tabla CDC almacena solo los valores modificados, lo que significa que la duplicación de datos se mantiene al mínimo.

El hecho de que los cambios se almacenen en una tabla diferente tiene dos consecuencias principales:

  • Las selecciones de la tabla original son tan rápidas como sin CDC. Si recuerdo bien, los CDC ocurren después de la actualización, por lo que las actualizaciones son igualmente rápidas (aunque no recuerdo bien cómo los CDC manejan la consistencia de los datos).

  • Algunos cambios en el esquema de la tabla original conducen a la eliminación de CDC. Por ejemplo, si agrega una columna, los CDC no saben cómo manejar eso. Por otro lado, agregar un índice o una restricción debería estar bien. Esto se convierte rápidamente en un problema si habilita CDC en una tabla que está sujeta a cambios frecuentes. Puede haber una solución que permita cambiar el esquema sin perder CDC, pero no lo he buscado.

Arseni Mourzenko
fuente
6

Resuelva el problema "filosóficamente" y en código primero. Y luego "negociar" con el código y la base de datos para que esto suceda.

Como ejemplo , si se trata de artículos genéricos, un concepto inicial para un artículo podría verse así:

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

Y en el siguiente nivel más básico, quiero mantener una lista de revisiones:

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

Y podría darme cuenta de que el cuerpo actual es solo la última revisión. Y eso significa dos cosas: necesito que cada revisión esté fechada o numerada:

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

Y ... y el cuerpo actual del artículo no necesita ser distinto de la última revisión:

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;
}

Faltan algunos detalles; pero ilustra que probablemente quieras dos entidades . Uno representa el artículo (u otro tipo de encabezado), y el otro es una lista de revisiones (agrupando cualquier campo que tenga un buen sentido "filosófico" para agrupar). Inicialmente no necesita restricciones especiales de la base de datos, porque su código no se preocupa por ninguna de las revisiones en sí mismas; son propiedades de un artículo que conoce las revisiones.

Por lo tanto, no necesita preocuparse por marcar revisiones de ninguna manera especial o apoyarse en una restricción de la base de datos para marcar el artículo "actual". Solo debe ponerles una marca de tiempo (incluso una identificación autoincluida estaría bien), hacer que se relacionen con su artículo principal y dejar que el artículo se encargue de saber que el "último" es el más relevante.

Y deja que un ORM maneje los detalles menos filosóficos, o los oculta en una clase de utilidad personalizada si no está utilizando un ORM listo para usar.

Mucho más tarde, después de haber realizado algunas pruebas de estrés, puede pensar en hacer que la propiedad de revisión lazy-load, o que su cuerpo atribuya lazy-load solo la revisión más importante. Pero, su estructura de datos en este caso no debería tener que cambiar para acomodar esas optimizaciones.

svidgen
fuente
2

Hay una página wiki de PostgreSQL para un desencadenante de seguimiento de auditoría que lo guía a través de cómo configurar un registro de auditoría que hará lo que necesita.

Rastrea los datos originales completos de un cambio, así como la lista de nuevos valores para actualizaciones (para inserciones y eliminaciones, solo hay un valor). Si desea restaurar una versión anterior, puede obtener la copia de los datos originales del registro de auditoría. Tenga en cuenta que si sus datos incluyen claves foráneas, es posible que esos registros también tengan que revertirse para mantener la coherencia.

En términos generales, si su aplicación de base de datos pasa la mayor parte de su tiempo solo con los datos actuales, creo que es mejor que rastree versiones alternativas en una tabla separada de los datos actuales. Esto mantendrá sus índices de tabla activos más manejables.

Si las filas que está rastreando son muy grandes y el espacio es una preocupación seria, podría intentar desglosar los cambios y almacenar diferencias / parches mínimos, pero eso definitivamente es más trabajo para cubrir todos sus tipos de tipos de datos. He hecho esto antes, y fue difícil reconstruir versiones antiguas de datos al revisar todos los cambios hacia atrás, uno a la vez.

Ben Turner
fuente
1

Bueno, terminé yendo con la opción más simple, un disparador que copia la versión anterior de una fila en un registro de historial por tabla.

Si termino con demasiada hinchazón de la base de datos, puedo ver el posible colapso de algunos de los cambios menores en el historial, si es necesario.

La solución terminó siendo bastante desordenada, ya que quería generar las funciones de activación automáticamente. Soy SQLAlchemy, así que pude producir la tabla de historial haciendo algunos enlaces de herencia, lo cual fue bueno, pero las funciones de disparo reales terminaron requiriendo un poco de mezcla de cadenas para generar correctamente las funciones de PostgreSQL correctamente, y mapear las columnas de una tabla a otro correctamente

De todos modos, todo está en Github aquí .

Nombre falso
fuente