Implementazione del sistema di versioning con MySQL


15

So che questo è stato chiesto qui e qui , ma ho la stessa idea con un'implementazione diversa possibile e ho bisogno di aiuto.

Inizialmente avevo il mio blogstoriestavolo con questa struttura:

| Column    | Type        | Description                                    |
|-----------|-------------|------------------------------------------------|
| uid       | varchar(15) | 15 characters unique generated id              |
| title     | varchar(60) | story title                                    |
| content   | longtext    | story content                                  |
| author    | varchar(10) | id of the user that originally wrote the story |
| timestamp | int         | integer generated with microtime()             |

Dopo aver deciso che volevo implementare un sistema di controllo delle versioni per ogni storia del blog, la prima cosa che mi venne in mente era creare una tabella diversa per contenere le modifiche ; successivamente, ho pensato di poter modificare la tabella esistente per contenere le versioni anziché le modifiche . Questa è la struttura che mi è venuta in mente:

| Column        | Type          | Description                                       |
|------------   |-------------  |------------------------------------------------   |
| story_id      | varchar(15)   | 15 characters unique generated id                 |
| version_id    | varchar(5)    | 5 characters unique generated id                  |
| editor_id     | varchar(10)   | id of the user that commited                      |
| author_id     | varchar(10)   | id of the user that originally wrote the story    |
| timestamp     | int           | integer generated with microtime()                |
| title         | varchar(60)   | current story title                               |
| content       | longtext      | current story text                                |
| coverimg      | varchar(20)   | cover image name                                  |

I motivi per cui sono venuto qui:

  • Il uidcampo della tabella iniziale era UNICO nella tabella. Ora, story_idnon è più unico. Come dovrei affrontarlo? (Pensavo di poter indirizzare story_id = xe poi trovare l'ultima versione, ma questo sembra molto dispendioso in termini di risorse, quindi per favore dai il tuo consiglio)
  • author_idil valore del campo si ripete in ogni riga della tabella. Dove e come dovrei tenerlo?

modificare

Il processo di generazione di codici univoci è nella CreateUniqueCodefunzione:

trait UIDFactory {
  public function CryptoRand(int $min, int $max): int {
    $range = $max - $min;
    if ($range < 1) return $min;
    $log = ceil(log($range, 2));
    $bytes = (int) ($log / 8) + 1;
    $bits = (int) $log + 1;
    $filter = (int) (1 << $bits) - 1;
    do {
        $rnd = hexdec(bin2hex(openssl_random_pseudo_bytes($bytes)));
        $rnd = $rnd & $filter;
    } while ($rnd >= $range);
    return $min + $rnd;
  }
  public function CreateUID(int $length): string {
    $token = "";
    $codeAlphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
    $codeAlphabet.= "abcdefghijklmnopqrstuvwxyz";
    $codeAlphabet.= "0123456789";
    $max = strlen($codeAlphabet) - 1;
    for ($i=0; $i < $length; $i++) {
        $token .= $codeAlphabet[$this->CryptoRand(0, $max)];
    }
    return $token;
  }
}

Il codice è scritto in Hack ed è stato originariamente scritto in PHP da @Scott nella sua risposta .

I campi author_ide editor_id possono essere diversi, perché ci sono utenti con autorizzazioni sufficienti per modificare le storie di chiunque.

Risposte:


23

Analizzando lo scenario - che presenta le caratteristiche associate all'argomento noto come database temporali - da una prospettiva concettuale, si può determinare che: (a) una versione "attuale" di Blog Story e (b) una versione "passato" di Blog Story , sebbene molto simili, sono entità di diversi tipi.

Inoltre, quando si lavora a livello logico di astrazione, i fatti (rappresentati da righe) di tipi distinti devono essere conservati in tabelle distinte. Nel caso in esame, anche se abbastanza simili, (i) i fatti relativi alle versioni "attuali" sono diversi dai (ii) fatti relativi alle versioni "passate" .

Pertanto raccomando di gestire la situazione mediante due tabelle:

  • uno dedicato esclusivamente alle versioni "attuali" o "presenti" delle storie del blog , e

  • uno che è separato, ma anche collegato con l'altro, per tutte le versioni "precedenti" o "passate" ;

ciascuno con (1) un numero leggermente distinto di colonne e (2) un diverso gruppo di vincoli.

Tornando al livello concettuale, ritengo che, nel tuo ambiente aziendale, Autore ed Editor siano nozioni che possono essere delineate come Ruoli che possono essere riprodotti da un Utente e questi aspetti importanti dipendono dalla derivazione dei dati (tramite operazioni di manipolazione a livello logico) e interpretazione (effettuata dai lettori e scrittori di Blog Stories , a livello esterno del sistema informativo computerizzato, con l'assistenza di uno o più programmi applicativi).

Descriverò in dettaglio tutti questi fattori e altri punti rilevanti come segue.

Regole di business

Secondo la mia comprensione dei vostri requisiti, le seguenti formulazioni di regole commerciali (messe insieme in termini di tipi di entità rilevanti e dei loro tipi di interrelazioni) sono particolarmente utili per stabilire lo schema concettuale corrispondente :

  • Un utente scrive zero-one-o-many BlogStories
  • Un BlogStory contiene zero-uno-o-molti BlogStoryVersions
  • Un utente ha scritto zero-uno-o-molti BlogStoryVersions

Diagramma IDEF1X dell'esposizione

Di conseguenza, al fine di esporre il mio suggerimento in virtù di un dispositivo grafico, ho creato un IDEF1X di esempio un diagramma derivato dalle regole aziendali sopra formulate e altre caratteristiche che sembrano pertinenti. È mostrato in Figura 1 :

Figura 1 - Diagramma IDEF1X delle versioni di Blog Story

Perché BlogStory e BlogStoryVersion sono concettualizzati come due diversi tipi di entità?

Perché:

  • Un BlogStoryVersion esempio (cioè, uno “passato”) contiene sempre un valore per un UpdatedDateTime struttura, mentre una BlogStory evento (cioè, un “regalo” uno) non tiene.

  • Inoltre, le entità di questi tipi sono identificate in modo univoco dai valori di due distinti insiemi di proprietà: BlogStoryNumber (nel caso delle occorrenze BlogStory ) e BlogStoryNumber più CreatedDateTime (nel caso delle istanze BlogStoryVersion ).


una Integration Definition for Information Modeling ( IDEF1X ) è una tecnica di modellazione dei dati altamente raccomandabile che è stata stabilita come standard nel dicembre 1993 dal National Institute of Standards and Technology (NIST)degli Stati Uniti. Si basa sul primo materiale teorico creato dall'unico creatore del modello relazionale , cioè il dott. EF Codd ; sullavisione Entity-Relationship dei dati, sviluppata dal Dr. PP Chen ; e anche sulla tecnica di progettazione del database logico, creata da Robert G. Brown.


Layout logico SQL-DDL illustrativo

Quindi, in base all'analisi concettuale precedentemente presentata, ho dichiarato il progetto a livello logico di seguito:

-- You should determine which are the most fitting 
-- data types and sizes for all your table columns 
-- depending on your business context characteristics.

-- Also you should make accurate tests to define the most
-- convenient index strategies at the physical level.

-- As one would expect, you are free to make use of 
-- your preferred (or required) naming conventions.    

CREATE TABLE UserProfile (
    UserId          INT      NOT NULL,
    FirstName       CHAR(30) NOT NULL,
    LastName        CHAR(30) NOT NULL,
    BirthDate       DATETIME NOT NULL,
    GenderCode      CHAR(3)  NOT NULL,
    UserName        CHAR(20) NOT NULL,
    CreatedDateTime DATETIME NOT NULL,
    --
    CONSTRAINT UserProfile_PK  PRIMARY KEY (UserId),
    CONSTRAINT UserProfile_AK1 UNIQUE ( -- Composite ALTERNATE KEY.
        FirstName,
        LastName,
        BirthDate,
        GenderCode
    ), 
    CONSTRAINT UserProfile_AK2 UNIQUE (UserName) -- ALTERNATE KEY.
);

CREATE TABLE BlogStory (
    BlogStoryNumber INT      NOT NULL,
    Title           CHAR(60) NOT NULL,
    Content         TEXT     NOT NULL,
    CoverImageName  CHAR(30) NOT NULL,
    IsActive        BIT(1)   NOT NULL,
    AuthorId        INT      NOT NULL,
    CreatedDateTime DATETIME NOT NULL,
    --
    CONSTRAINT BlogStory_PK              PRIMARY KEY (BlogStoryNumber),
    CONSTRAINT BlogStory_AK              UNIQUE      (Title), -- ALTERNATE KEY.
    CONSTRAINT BlogStoryToUserProfile_FK FOREIGN KEY (AuthorId)
        REFERENCES UserProfile (UserId)
);

CREATE TABLE BlogStoryVersion  (
    BlogStoryNumber INT      NOT NULL,
    CreatedDateTime DATETIME NOT NULL,
    Title           CHAR(60) NOT NULL,
    Content         TEXT     NOT NULL,
    CoverImageName  CHAR(30) NOT NULL,
    IsActive        BIT(1)   NOT NULL,
    AuthorId        INT      NOT NULL,
    UpdatedDateTime DATETIME NOT NULL,
    --
    CONSTRAINT BlogStoryVersion_PK              PRIMARY KEY (BlogStoryNumber, CreatedDateTime), -- Composite PK.
    CONSTRAINT BlogStoryVersionToBlogStory_FK   FOREIGN KEY (BlogStoryNumber)
        REFERENCES BlogStory (BlogStoryNumber),
    CONSTRAINT BlogStoryVersionToUserProfile_FK FOREIGN KEY (AuthorId)
        REFERENCES UserProfile (UserId),
    CONSTRAINT DatesSuccession_CK               CHECK       (UpdatedDateTime > CreatedDateTime) --Let us hope that MySQL will finally enforce CHECK constraints in a near future version.
);

Testato in questo SQL Fiddle che funziona su MySQL 5.6.

Il BlogStorytavolo

Come puoi vedere nella progettazione demo, ho definito la BlogStorycolonna PRIMARY KEY (PK per brevità) con il tipo di dati INT. A questo proposito, potresti voler correggere un processo automatico incorporato che genera e assegna un valore numerico per tale colonna in ogni inserimento di riga. Se non ti dispiace lasciare occasionalmente delle lacune in questo insieme di valori, puoi utilizzare l' attributo AUTO_INCREMENT , comunemente usato negli ambienti MySQL.

Quando si inseriscono tutti i singoli BlogStory.CreatedDateTimepunti dati, è possibile utilizzare la funzione NOW () , che restituisce i valori di data e ora correnti nel server di database nell'istante esatto dell'operazione INSERT. Per me, questa pratica è decisamente più adatta e meno soggetta a errori rispetto all'uso di routine esterne.

A condizione che, come discusso nei commenti (ora rimossi), si desidera evitare la possibilità di mantenere BlogStory.Titlevalori duplicati, è necessario impostare un vincolo UNIQUE per questa colonna. A causa del fatto che un determinato titolo può essere condiviso da più (o anche da tutte) le "precedenti" BlogStoryVersions , non è necessario stabilire un vincolo UNICO per la BlogStoryVersion.Titlecolonna.

Ho incluso la BlogStory.IsActivecolonna di tipo BIT (1) (anche se può essere usato anche un TINYINT ) nel caso in cui sia necessario fornire funzionalità DELETE "soft" o "logico".

Dettagli sul BlogStoryVersiontavolo

D'altra parte, il PK della BlogStoryVersiontabella è composto da (a) BlogStoryNumbere (b) una colonna denominata CreatedDateTimeche, ovviamente, segna il preciso istante in cui una BlogStoryriga ha subito un INSERTO.

BlogStoryVersion.BlogStoryNumber, oltre a far parte del PK, è anche vincolato come FOREIGN KEY (FK) che fa riferimento BlogStory.BlogStoryNumber, una configurazione che impone l' integrità referenziale tra le righe di queste due tabelle. A questo proposito, BlogStoryVersion.BlogStoryNumbernon è necessaria l' implementazione di una generazione automatica di a perché, essendo impostati come FK, i valori INSERITI in questa colonna devono essere "estratti da" quelli già racchiusi nella relativa BlogStory.BlogStoryNumbercontroparte.

La BlogStoryVersion.UpdatedDateTimecolonna dovrebbe conservare, come previsto, il momento in cui una BlogStoryriga è stata modificata e, di conseguenza, aggiunta alla BlogStoryVersiontabella. Quindi, puoi usare la funzione NOW () anche in questa situazione.

L' intervallo compreso tra BlogStoryVersion.CreatedDateTimeed BlogStoryVersion.UpdatedDateTimeesprime l'intero Periodo durante il quale una BlogStoryriga era "presente" o "corrente".

Considerazioni per una Versioncolonna

Può essere utile pensare BlogStoryVersion.CreatedDateTimecome la colonna che contiene il valore che rappresenta un particolare “passato” versione di un BlogStory . Lo ritengo molto più vantaggioso di un VersionIdo VersionCode, poiché è più user-friendly, nel senso che le persone tendono ad avere più familiarità con i concetti del tempo . Ad esempio, gli autori o i lettori del blog potrebbero fare riferimento a BlogStoryVersion in un modo simile al seguente:

  • “Voglio vedere la specifica versione del BlogStory identificato da numero 1750 che è stato Creato su 26 August 2015a 9:30”.

L' autore e Editor Ruoli: derivazione dei dati e l'interpretazione

Con questo approccio, si può facilmente distinguere chi detiene il “originale” AuthorIddi un calcestruzzo BlogStory selezionando il “prima” versione di un certo BlogStoryIdFROM il BlogStoryVersiontavolo in virtù dell'applicazione della funzione di MIN () a BlogStoryVersion.CreatedDateTime.

In questo modo, ogni BlogStoryVersion.AuthorIdvalore contenuto in tutte le righe delle versioni "successive" o "successive" indica, naturalmente, l' identificatore dell'autore della rispettiva versione a portata di mano, ma si può anche dire che tale valore è, allo stesso tempo, denotante il ruolo svolto dalla coinvolto utente come redattore della “originale” versione di un BlogStory .

Sì, un determinato AuthorIdvalore può essere condiviso da più BlogStoryVersionrighe, ma in realtà si tratta di un'informazione che dice qualcosa di molto significativo su ciascuna versione , quindi la ripetizione di detto dato non è un problema.

Il formato delle colonne DATETIME

Per quanto riguarda il tipo di dati DATETIME, sì, hai ragione, " MySQL recupera e visualizza i valori DATETIME in" YYYY-MM-DD HH:MM:SSformato ", ma puoi inserire con sicurezza i dati pertinenti in questo modo e quando devi eseguire una query devi solo utilizzare le funzioni DATE e TIME integrate per mostrare, tra le altre cose, i valori relativi nel formato appropriato per gli utenti. Oppure potresti certamente eseguire questo tipo di formattazione dei dati tramite il codice dei tuoi programmi applicativi.

Implicazioni delle BlogStoryoperazioni di AGGIORNAMENTO

Ogni volta che una BlogStoryriga subisce un AGGIORNAMENTO, tu necessario assicurarsi che i valori corrispondenti che erano "presenti" fino a quando non ha avuto luogo la modifica vengano quindi INSERITI nella BlogStoryVersiontabella. Pertanto, consiglio vivamente di compiere queste operazioni all'interno di una singola TRANSAZIONE ACIDA per garantire che siano trattate come un'unità di lavoro indivisibile. Puoi anche impiegare TRIGGERS, ma tendono a rendere le cose disordinate, per così dire.

Presentazione di a VersionIdo VersionCodecolumn

Se si sceglie (a causa di circostanze aziendali o preferenze personali) di incorporare una BlogStory.VersionIdo una BlogStory.VersionCodecolonna per distinguere BlogStoryVersions , è necessario ponderare le seguenti possibilità:

  1. UN VersionCode potrebbe essere richiesto di essere UNICO in (i) l'intera BlogStorytabella e anche in (ii) BlogStoryVersion.

    Pertanto, è necessario implementare un metodo accuratamente testato e totalmente affidabile al fine di generare e assegnare ciascunoCode valore.

  2. Forse, i VersionCodevalori potrebbero essere ripetuti in BlogStoryrighe diverse , ma mai duplicati insieme allo stesso BlogStoryNumber. Ad esempio, potresti avere:

    • un BlogStoryNumber3 - Versione83o7c5c e, contemporaneamente,
    • un BlogStoryNumber86 - Versione83o7c5c e
    • un BlogStoryNumber 958- Versione83o7c5c .

La possibilità successiva apre un'altra alternativa:

  1. Mantenere un VersionNumberper ilBlogStories , quindi potrebbero esserci:

    • BlogStoryNumber 23 - Versioni1, 2, 3… ;
    • BlogStoryNumber 650- Versioni1, 2, 3… ;
    • BlogStoryNumber 2254- Versioni1, 2, 3… ;
    • eccetera.

Tenendo le versioni "originali" e "successive" in una singola tabella

Sebbene sia possibile mantenere tutte le BlogStoryVersions nella stessa tabella di base individuale , suggerisco di non farlo perché si mescolerebbero due tipi distinti (concettuali) di fatti, che quindi hanno effetti collaterali indesiderati

  • vincoli e manipolazione dei dati (a livello logico), insieme a
  • la relativa elaborazione e archiviazione (a livello fisico).

Ma, a condizione che tu scelga di seguire quel corso d'azione, puoi comunque trarre vantaggio da molte delle idee sopra descritte, ad esempio:

  • un PK composito costituito da una colonna INT ( BlogStoryNumber) e una colonna DATETIME ( CreatedDateTime);
  • l'utilizzo delle funzioni del server al fine di ottimizzare i processi pertinenti e
  • l' autore e Editor derivabili ruoli .

Visto che, procedendo con tale approccio, un BlogStoryNumbervalore verrà duplicato non appena verranno aggiunte versioni “più recenti” , un'opzione che e che potresti valutare (che è molto simile a quelle menzionate nella sezione precedente) sta stabilendo un BlogStoryPK composto dalle colonne BlogStoryNumbere VersionCode, in questo modo, saresti in grado di identificare in modo univoco ogni versione di una BlogStory . E puoi provare con una combinazione di BlogStoryNumbereVersionNumber anche.

Scenario simile

Potresti trovare la mia risposta a questa domanda di aiuto, poiché anche io propongo di abilitare le capacità temporali nel database in questione per affrontare uno scenario comparabile.


2

Un'opzione è quella di utilizzare il formato normale versione (vnf). I vantaggi includono:

  • I dati correnti e tutti i dati precedenti risiedono nella stessa tabella.
  • La stessa query viene utilizzata per recuperare dati o dati correnti a partire da una data specifica.
  • I riferimenti di chiave esterna ai dati con versione funzionano allo stesso modo dei dati senza versione.

Un ulteriore vantaggio nel tuo caso, poiché i dati con versione vengono identificati in modo univoco rendendo la data effettiva (la data in cui è stata apportata la modifica) parte della chiave, non è richiesto un campo version_id separato.

Qui una spiegazione per un tipo di entità molto simile.

Maggiori dettagli sono disponibili in una presentazione di diapositive qui e in un documento non del tutto completato qui


1

La tua relazione

(story_id, version_id, editor_id, author_id, timestamp, titolo, contenuto, copertina)

non è in terza forma normale. Per ogni versione della tua storia author_id è lo stesso. Quindi hai bisogno di due relazioni per superare questo

(story_id, author_id)
(story_id, version_id, editor_id, timestamp, titolo, contenuto, copertina)

La chiave della prima relazione è story_id, la chiave della seconda relazione è la chiave combinata (story_id, version_id). Se non ti piace la chiave combinata, puoi usare solo version_idcome chiave


2
Questo non sembra risolvere il mio problema, ma li sottolinea semplicemente
Victor,

Quindi non risponde nemmeno al author_id valore del campo della query che si ripete in ogni riga della tabella. Dove e come dovrei tenerlo ?
miracle173,

2
Non capisco davvero cosa afferma la tua risposta. Potrebbe essere perché non sono un madrelingua inglese, quindi potresti provare a spiegarlo in parole più e semplici, per favore?
Victor,

Vuol dire che dovresti evitare la ripetizione del numero author_id (se story_id è uguale per due righe, anche il loro author_id è uguale) e dividi la tua tabella nelle due tabelle come descritto nel mio post. Quindi puoi evitare la ripetizione di author_id.
miracle173,
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.