Versione che controlla i contenuti di un database


16

Sto lavorando a un progetto Web che coinvolge contenuti modificabili dall'utente e mi piacerebbe essere in grado di eseguire il monitoraggio della versione del contenuto effettivo, che vive in un database. Fondamentalmente, voglio implementare le storie di cambiamento in stile wiki.

Facendo alcune ricerche di base, vedo molta documentazione su come eseguire la versione dello schema del database (il mio è in realtà già controllato), ma qualsiasi strategia esistente su come tenere traccia delle modifiche al contenuto del database viene persa nella valanga di elementi di versioning dello schema, almeno nelle mie ricerche.

Posso pensare ad alcuni modi per implementare il mio rilevamento delle modifiche, ma sembrano tutti piuttosto rozzi:

  • Salvare l'intera riga su ogni modifica, mettere in relazione la riga con l'id sorgente con una chiave primaria (ciò a cui mi sto appoggiando attualmente, è la più semplice). Molte piccole modifiche potrebbero produrre un sacco di gonfiore del tavolo, però.
  • salva prima / dopo / utente / data / ora per ogni modifica, con un nome di colonna per mettere in relazione la modifica con la relativa colonna.
  • salva prima / dopo / utente / timestamp con una tabella per ogni colonna (comporterebbe troppe tabelle).
  • salva diffs / user / timestamp per ogni modifica con una colonna (ciò significherebbe che dovresti percorrere l'intera cronologia delle modifiche intervenienti per tornare a una certa data).

Qual è l'approccio migliore qui? A rotazione, sembra che stia probabilmente reinventando la base di codice (migliore) di qualcun altro.


Punti bonus per PostgreSQL.


Questa domanda è già stata discussa su SO: stackoverflow.com/questions/3874199/… . Google per "cronologia dei record del database" e troverai altri articoli.
Doc Brown,

1
Sembra un candidato ideale per il sourcing di eventi
James,

Perché non usare il registro delle transazioni di SQL Server per fare il trucco?
Thomas Junk,

Risposte:


11

La tecnica che ho usato normalmente è quella di salvare il record completo, con un campo end_timestamp. Esiste una regola aziendale secondo cui solo una riga può avere un end_timestamp null e questo è ovviamente il contenuto attualmente attivo.

Se adotti questo sistema, ti consiglio vivamente di aggiungere un indice o un vincolo per applicare la regola. Questo è facile con Oracle, poiché un indice univoco può contenere uno e un solo null. Altri database potrebbero essere più un problema. Avere il database imporre la regola manterrà il tuo codice onesto.

Hai perfettamente ragione sul fatto che molte piccole modifiche creeranno un eccesso, ma devi scambiarlo con il codice e la semplicità dei rapporti.


Si noti che altri motori di database potrebbero comportarsi diversamente, ad esempio MySQL consente più valori NULL in una colonna con indice univoco. Questo rende questo vincolo molto più difficile da applicare.
qbd

L'uso di un timestamp effettivo non è sicuro, ma alcuni database MVCC funzionano internamente archiviando i numeri di serie minimi e massimi delle transazioni insieme alle tuple.
user2313838

"Questo è facile con Oracle, in quanto un indice univoco può contenere solo un nullo". Sbagliato. Oracle non include affatto valori nulli negli indici. Non esiste un limite al numero di null in una colonna con un indice univoco.
Gerrat,

@Gerrat Sono passati diversi anni da quando ho progettato un database che aveva questo requisito e non ho più accesso a quel database. Hai ragione nel dire che un indice univoco standard può supportare più null, ma penso che abbiamo usato un vincolo univoco o eventualmente un indice funzionale.
kiwiron,

8

Si noti che se si utilizza Microsoft SQL Server, esiste già una funzionalità per quella chiamata Change Data Capture . Sarà comunque necessario scrivere codice per accedere alle revisioni precedenti in un secondo momento (CDC crea viste specifiche per questo), ma almeno non è necessario modificare lo schema delle tabelle, né implementare il rilevamento delle modifiche stesso.

Sotto il cofano , ciò che accade è che:

  • CDC crea una tabella aggiuntiva contenente le revisioni,

  • La tabella originale viene utilizzata come in precedenza, ovvero qualsiasi aggiornamento si riflette direttamente in questa tabella,

  • La tabella CDC memorizza solo i valori modificati, il che significa che la duplicazione dei dati è ridotta al minimo.

Il fatto che le modifiche siano archiviate in una tabella diversa ha due conseguenze principali:

  • Le selezioni dalla tabella originale sono veloci come senza CDC. Se ricordo bene, succede CDC dopo l'aggiornamento, quindi gli aggiornamenti sono ugualmente veloci (anche se non ricordo bene come CDC gestisca la coerenza dei dati).

  • Alcune modifiche allo schema della tabella originale portano alla rimozione di CDC. Ad esempio, se aggiungi una colonna, CDC non sa come gestirla. D'altra parte, l'aggiunta di un indice o di un vincolo dovrebbe andare bene. Questo diventa rapidamente un problema se si abilita CDC su una tabella soggetta a frequenti modifiche. Potrebbe esserci una soluzione che consente di modificare lo schema senza perdere CDC, ma non l'ho cercato.


6

Risolvi il problema "filosoficamente" e prima nel codice. E poi "negoziare" con codice e database per realizzarlo.

Ad esempio , se hai a che fare con articoli generici, un concetto iniziale per un articolo potrebbe assomigliare a questo:

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

E al prossimo livello più elementare, voglio mantenere un elenco di revisioni:

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

E potrei rendermi conto che il corpo attuale è solo l'ultima revisione. Ciò significa due cose: ho bisogno che ogni revisione sia datata o numerata:

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

E ... e il corpo attuale dell'articolo non deve essere distinto dall'ultima revisione:

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

Mancano alcuni dettagli; ma dimostra che probabilmente vuoi due entità . Uno rappresenta l'articolo (o un altro tipo di intestazione) e l'altro è un elenco di revisioni (che raggruppano tutti i campi che hanno un buon senso "filosofico" da raggruppare). Inizialmente non hai bisogno di speciali vincoli del database, perché al tuo codice non interessano le revisioni in sé e per sé: sono proprietà di un articolo che conosce le revisioni.

Pertanto, non è necessario preoccuparsi di contrassegnare le revisioni in modo speciale o appoggiarsi a un vincolo del database per contrassegnare l'articolo "corrente". Devi solo timestamp (anche un ID con iscrizione automatica sarebbe OK), renderli correlati al loro articolo principale e lasciare che l'articolo sia responsabile di sapere che quello "più recente" è quello più pertinente.

E lasci che un ORM gestisca i dettagli meno filosofici o li nascondi in una classe di utilità personalizzata se non stai utilizzando un ORM pronto all'uso.

Molto più tardi, dopo aver eseguito alcuni stress test, potresti pensare di rendere quella proprietà di revisione lazy-load o di avere l'attributo Body lazy-load solo la revisione più in alto. Tuttavia, la struttura dei dati in questo caso non dovrebbe cambiare per adattarsi a tali ottimizzazioni.


2

C'è una pagina wiki PostgreSQL per un trigger di tracciamento dell'audit che ti guida attraverso come impostare un registro di controllo che farà ciò di cui hai bisogno.

Tiene traccia dei dati originali completi di una modifica, nonché dell'elenco di nuovi valori per gli aggiornamenti (per inserimenti ed eliminazioni, esiste un solo valore). Se si desidera ripristinare una versione precedente, è possibile acquisire la copia dei dati originali dal record di controllo. Se i tuoi dati riguardano chiavi esterne, potrebbe essere necessario eseguire il rollback di tali record per mantenere la coerenza.

In generale, se l'applicazione di database impiega la maggior parte del tempo solo sui dati correnti, penso che sia meglio tracciare versioni alternative in una tabella separata dai dati attuali. Ciò manterrà più gestibili gli indici delle tabelle attive.

Se le righe che stai monitorando sono molto grandi e lo spazio è una seria preoccupazione, potresti provare a scomporre le modifiche e memorizzare differenze / patch minime, ma è sicuramente più lavoro per coprire tutti i tuoi tipi di tipi di dati. L'ho già fatto in passato ed è stato doloroso ricostruire vecchie versioni di dati esaminando tutte le modifiche all'indietro, una alla volta.


1

Bene, sono finito con l'opzione più semplice, un trigger che copia la vecchia versione di una riga in un registro cronologico per tabella.

Se finisco con troppa quantità eccessiva di database, posso eventualmente verificare il collasso di alcune piccole modifiche della cronologia, se necessario.

La soluzione è risultata piuttosto disordinata, poiché volevo generare automaticamente le funzioni di trigger. Sono SQLAlchemy, quindi sono stato in grado di produrre la tabella della cronologia eseguendo alcuni hijink di ereditarietà, il che era bello, ma le funzioni di trigger effettive sono terminate richiedendo un po 'di munging delle stringhe per generare correttamente le funzioni PostgreSQL e mappare le colonne da una tabella a un altro correttamente.

Comunque, è tutto su github qui .

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.