Implémentation d'un système de versioning avec MySQL

15

Je sais que cela a été demandé ici et ici , mais j'ai la même idée avec une implémentation possible différente et j'ai besoin d'aide.

Au départ, j'avais ma blogstoriestable avec cette structure:

| 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()             |

Après avoir décidé que je voulais implémenter un système de versioning pour chaque article du blog, la première chose qui m'est venue à l'esprit était de créer un tableau différent pour les modifications ; après cela, j'ai pensé que je pouvais modifier la table existante pour contenir des versions au lieu de modifications . Voici la structure qui m'est venue à l'esprit:

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

Les raisons pour lesquelles je suis venu ici:

  • Le uidchamp de la table initiale était UNIQUE dans la table. Maintenant, le story_idn'est plus unique. Comment dois-je gérer cela? (Je pensais pouvoir aborder story_id = xpuis trouver la dernière version, mais cela semble consommer beaucoup de ressources, alors donnez-nous vos conseils)
  • author_idla valeur du champ se répète dans chaque ligne du tableau. Où et comment dois-je le conserver?

Éditer

Le processus unique de génération de codes est dans la CreateUniqueCodefonction:

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

Le code est écrit en Hack , et a été initialement écrit en PHP par @Scott dans sa réponse .

Les champs author_idet editor_id peuvent être différents, car il y a des utilisateurs avec suffisamment d'autorisations pour modifier les histoires de n'importe qui.

Victor
la source

Réponses:

23

En analysant le scénario - qui présente des caractéristiques associées au sujet connu sous le nom de bases de données temporelles - d'un point de vue conceptuel, on peut déterminer que: (a) une version «actuelle» du blog et (b) une version «passée» du blog , bien que très ressemblant, sont des entités de différents types.

De plus, lorsque vous travaillez au niveau logique de l'abstraction, les faits (représentés par des lignes) de types différents doivent être conservés dans des tableaux distincts. Dans le cas considéré, même lorsqu'ils sont assez similaires, (i) les faits concernant les versions «actuelles» sont différents des (ii) faits concernant les versions «passées» .

Par conséquent, je recommande de gérer la situation au moyen de deux tableaux:

  • une dédiée exclusivement aux versions «actuelles» ou «actuelles» des histoires du blog , et

  • une qui est distincte, mais également liée à l'autre, pour toutes les versions «précédentes» ou «passées» ;

chacun avec (1) un nombre légèrement distinct de colonnes et (2) un groupe différent de contraintes.

De retour à la couche conceptuelle, je considère que - dans votre environnement professionnel - Auteur et éditeur sont des notions qui peuvent être définies comme des rôles pouvant être joués par un utilisateur , et ces aspects importants dépendent de la dérivation des données (via des opérations de manipulation au niveau logique) et interprétation (réalisée par les lecteurs et rédacteurs de Blog Stories , au niveau externe du système d'information informatisé, avec l'aide d'un ou plusieurs programmes d'application).

Je détaillerai tous ces facteurs et d'autres points pertinents comme suit.

Règles métier

Selon ma compréhension de vos besoins, les formulations de règles commerciales suivantes (regroupées en termes de types d'entités pertinents et de leurs types d'interrelations) sont particulièrement utiles pour établir le schéma conceptuel correspondant :

  • Un utilisateur écrit zéro ou un ou plusieurs BlogStories
  • Un BlogStory contient zéro ou un ou plusieurs BlogStoryVersions
  • Un utilisateur a écrit zéro ou un ou plusieurs BlogStoryVersions

Diagramme expositoire IDEF1X

Par conséquent, afin d'exposer ma suggestion grâce à un dispositif graphique, j'ai créé un exemple d'IDEF1X, un diagramme dérivé des règles métier formulées ci-dessus et d'autres fonctionnalités qui semblent pertinentes. Il est illustré à la figure 1 :

Figure 1 - Diagramme IDEF1X des versions du blog

Pourquoi BlogStory et BlogStoryVersion sont - ils conceptualisés comme deux types d'entités différents?

Car:

  • Une instance BlogStoryVersion (c'est-à-dire une «ancienne») contient toujours une valeur pour une propriété UpdatedDateTime , tandis qu'une occurrence BlogStory (c'est-à-dire une «présente») ne la contient jamais.

  • En outre, les entités de ces types sont identifiées de manière unique par les valeurs de deux ensembles de propriétés distincts: BlogStoryNumber (dans le cas des occurrences BlogStory ) et BlogStoryNumber plus CreatedDateTime (dans le cas des instances BlogStoryVersion ).


Une définition d'intégration pour la modélisation de l'information ( IDEF1X ) est une technique de modélisation de données hautement recommandable qui a été établie en tant que norme en décembre 1993 par le National Institute of Standards and Technology (NIST)des États-Unis. Il est basé sur les premiers éléments théoriques rédigés par le seul auteur du modèle relationnel , c'est-à-dire le Dr EF Codd ; sur la vue Entité-Relation des données, développée par le Dr PP Chen ; et également sur la technique de conception de bases de données logiques, créée par Robert G. Brown.


Exemple de disposition logique SQL-DDL

Ensuite, sur la base de l'analyse conceptuelle présentée précédemment, j'ai déclaré la conception au niveau logique ci-dessous:

-- 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.
);

Testé dans ce SQL Fiddle qui fonctionne sur MySQL 5.6.

La BlogStorytable

Comme vous pouvez le voir dans la conception de la démonstration, j'ai défini la BlogStorycolonne PRIMARY KEY (PK pour la brièveté) avec le type de données INT. À cet égard, vous pouvez souhaiter corriger un processus automatique intégré qui génère et attribue une valeur numérique pour une telle colonne à chaque insertion de ligne. Si cela ne vous dérange pas de laisser occasionnellement des lacunes dans cet ensemble de valeurs, vous pouvez utiliser l' attribut AUTO_INCREMENT , couramment utilisé dans les environnements MySQL.

Lorsque vous entrez tous vos BlogStory.CreatedDateTimepoints de données individuels , vous pouvez utiliser la fonction NOW () , qui renvoie les valeurs de date et d'heure qui sont actuelles dans le serveur de base de données à l'instant exact de l'opération INSERT. Pour moi, cette pratique est décidément plus adaptée et moins sujette aux erreurs que l'utilisation de routines externes.

À condition que, comme indiqué dans les commentaires (désormais supprimés), vous souhaitiez éviter la possibilité de conserver BlogStory.Titledes valeurs en double, vous devez configurer une contrainte UNIQUE pour cette colonne. En raison du fait qu'un donné titre peut être partagée par plusieurs (voire tous les) « passé » BlogStoryVersions , alors une contrainte UNIQUE doit pas être établie pour la BlogStoryVersion.Titlecolonne.

J'ai inclus la BlogStory.IsActivecolonne de type BIT (1) (même si un TINYINT peut aussi bien être utilisé) au cas où vous auriez besoin de fournir une fonctionnalité de suppression «douce» ou «logique».

Détails sur la BlogStoryVersiontable

D'autre part, le PK de la BlogStoryVersiontable est composé de (a) BlogStoryNumberet (b) une colonne nommée CreatedDateTimequi, bien sûr, marque l'instant précis où une BlogStoryligne a subi un INSERT.

BlogStoryVersion.BlogStoryNumber, en plus de faire partie du PK, est également contraint en tant que CLÉ ÉTRANGÈRE (FK) qui fait référence BlogStory.BlogStoryNumber, une configuration qui applique l' intégrité référentielle entre les lignes de ces deux tables. À cet égard, la mise en œuvre d'une génération automatique de a BlogStoryVersion.BlogStoryNumbern'est pas nécessaire car, étant définie comme un FK, les valeurs INSÉRÉES dans cette colonne doivent être «tirées» de celles déjà incluses dans la BlogStory.BlogStoryNumbercontrepartie associée .

La BlogStoryVersion.UpdatedDateTimecolonne doit conserver, comme prévu, le moment où une BlogStoryligne a été modifiée et, par conséquent, ajoutée au BlogStoryVersiontableau. Par conséquent, vous pouvez également utiliser la fonction NOW () dans cette situation.

L' intervalle compris entre BlogStoryVersion.CreatedDateTimeet BlogStoryVersion.UpdatedDateTimeexprime toute la période pendant laquelle une BlogStoryligne était «présente» ou «actuelle».

Considérations pour une Versioncolonne

Il peut être utile de penser BlogStoryVersion.CreatedDateTimeque la colonne qui contient la valeur qui représente un « passé » notamment la version d'un BlogStory . Je considère cela beaucoup plus bénéfique qu'un VersionIdou VersionCode, car il est plus convivial dans le sens où les gens ont tendance à être plus familiers avec les concepts de temps . Par exemple, les auteurs ou lecteurs de blog peuvent se référer à BlogStoryVersion d'une manière similaire à ce qui suit:

  • « Je veux voir la spécifique version du BlogStory identifié par nombre 1750 qui a été créé sur 26 August 2015à 9:30».

L' auteur et éditeur Rôles: dérivation et interprétation des données

Avec cette approche, vous pouvez facilement distinguer qui détient le « original » AuthorIdd'un béton BlogStory sélectionnant l'option « plus tôt » la version d'une certaine BlogStoryIdde la BlogStoryVersiontable en vertu de l' application de la fonction MIN () à BlogStoryVersion.CreatedDateTime.

De cette façon, chaque BlogStoryVersion.AuthorIdvaleur contenue dans toutes les lignes des versions "ultérieures" ou "suivantes" indique, naturellement, l' identifiant d' auteur de la version respective à portée de main, mais on peut également dire qu'une telle valeur est, en même temps, dénotant le rôle joué par l'impliqué utilisateur comme éditeur du « original » version d'un BlogStory .

Oui, une AuthorIdvaleur donnée peut être partagée par plusieurs BlogStoryVersionlignes, mais il s'agit en fait d'une information qui indique quelque chose de très significatif sur chaque version , donc la répétition de ladite donnée n'est pas un problème.

Le format des colonnes DATETIME

En ce qui concerne le type de données DATETIME, oui, vous avez raison, " MySQL récupère et affiche les valeurs DATETIME au YYYY-MM-DD HH:MM:SSformat ' ' ", mais vous pouvez entrer en toute confiance les données pertinentes de cette manière, et lorsque vous devez effectuer une requête, vous n'avez qu'à utilisez les fonctions DATE et TIME intégrées afin d'afficher, entre autres, les valeurs concernées au format approprié pour vos utilisateurs. Ou vous pouvez certainement effectuer ce type de formatage de données via le code de votre programme d'application.

Implications des BlogStoryopérations UPDATE

Chaque fois qu'une BlogStoryligne subit une MISE À JOUR, vous devez vous assurer que les valeurs correspondantes qui étaient «présentes» jusqu'à ce que la modification ait eu lieu sont ensuite INSÉRÉES dans le BlogStoryVersiontableau. Ainsi, je suggère fortement de réaliser ces opérations au sein d'une seule ACIDE TRANSACTION afin de garantir qu'elles soient traitées comme une Unité de Travail indivisible. Vous pouvez aussi bien utiliser TRIGGERS, mais ils ont tendance à rendre les choses désordonnées, pour ainsi dire.

Présentation d'une VersionIdou d' une VersionCodecolonne

Si vous choisissez (en raison de circonstances commerciales ou de préférences personnelles) d'incorporer une BlogStory.VersionIdou une BlogStory.VersionCodecolonne pour distinguer les BlogStoryVersions , vous devez réfléchir aux possibilités suivantes:

  1. A VersionCodepourrait être requis pour être UNIQUE dans (i) l'ensemble du BlogStorytableau et également (ii) BlogStoryVersion.

    Par conséquent, vous devez mettre en œuvre une méthode soigneusement testée et totalement fiable afin de générer et d'attribuer chaque Codevaleur.

  2. Peut-être, les VersionCodevaleurs pourraient être répétées dans différentes BlogStorylignes, mais jamais dupliquées avec la même chose BlogStoryNumber. Par exemple, vous pourriez avoir:

    • un BlogStoryNumber 3- Version83o7c5c et, simultanément,
    • un BlogStoryNumber 86- Version83o7c5c et
    • un BlogStoryNumber 958- Version83o7c5c .

La dernière possibilité ouvre une autre alternative:

  1. Garder un VersionNumberpour le BlogStories, donc il pourrait y avoir:

    • BlogStoryNumber 23- Versions1, 2, 3… ;
    • BlogStoryNumber 650- Versions1, 2, 3… ;
    • BlogStoryNumber 2254- Versions1, 2, 3… ;
    • etc.

Tenir les versions «originales» et «ultérieures» dans un seul tableau

Bien que toutes les BlogStoryVersions soient conservées dans la même personne table de base soit possible, je suggère de ne pas le faire car vous mélangeriez deux types de faits (conceptuels) distincts, ce qui a donc des effets secondaires indésirables sur

  • contraintes et manipulation des données (au niveau logique), ainsi que
  • le traitement et le stockage associés (au niveau physique).

Mais, à condition que vous choisissiez de suivre cette ligne de conduite, vous pouvez toujours profiter de nombreuses idées détaillées ci-dessus, par exemple:

  • un PK composite composé d'une colonne INT (BlogStoryNumber ) et d'une colonne DATETIME ( CreatedDateTime);
  • l'utilisation des fonctions du serveur afin d'optimiser les processus pertinents, et
  • les rôles dérivables de l' auteur et de l' éditeur .

Voyant qu'en procédant à une telle approche, une BlogStoryNumbervaleur sera dupliquée dès que des versions «plus récentes» seront ajoutées, une option que vous pourriez évaluer (qui est très similaire à celles mentionnées dans la section précédente) est d'établir un BlogStoryPK composé des colonnes BlogStoryNumberet VersionCode, de cette manière, vous seriez en mesure d'identifier de manière unique chaque version d'un BlogStory . Et vous pouvez essayer avec une combinaison de BlogStoryNumberet VersionNumberaussi.

Scénario similaire

Vous pouvez trouver ma réponse à cette question d'aide, car je propose également d'activer les capacités temporelles dans la base de données concernée pour faire face à un scénario comparable.

MDCCL
la source
2

Une option consiste à utiliser Version Normal Form (vnf). Les avantages comprennent:

  • Les données actuelles et toutes les données passées résident dans la même table.
  • La même requête est utilisée pour récupérer les données actuelles ou les données qui étaient actuelles à une date particulière.
  • Les références de clé étrangère aux données versionnées fonctionnent de la même manière que pour les données non versionnées.

Un avantage supplémentaire dans votre cas, car les données versionnées sont identifiées de manière unique en intégrant la date effective (la date à laquelle la modification a été apportée) dans la clé, un champ version_id distinct n'est pas requis.

Ici une explication pour un type d'entité très similaire.

Plus de détails peuvent être trouvés dans une présentation de diapositives ici et un document pas tout à fait terminé ici

TommCatt
la source
1

Votre relation

(story_id, version_id, editor_id, author_id, horodatage, titre, contenu, coverimg)

n'est pas en 3ème forme normale. Pour chaque version de votre histoire, le author_id est le même. Vous avez donc besoin de deux relations pour surmonter ce

(story_id, author_id)
(story_id, version_id, editor_id, horodatage, titre, contenu, coverimg)

La clé de la première relation est story_idla clé de la deuxième relation est la clé combinée (story_id, version_id). Si vous n'aimez pas la clé combinée, vous ne pouvez l'utiliser que version_idcomme clé

miracle173
la source
2
Cela ne semble pas résoudre mon problème, cela ne fait que le souligner
Victor
Donc, il ne répond même pas à la author_id valeur du champ de la requête qui se répète dans chaque ligne du tableau. Où et comment dois-je le conserver ?
miracle173
2
Je ne comprends pas vraiment ce que dit votre réponse. C'est peut-être parce que je ne suis pas un anglophone natif, alors pourriez-vous essayer de l'expliquer en termes plus simples et plus, s'il vous plaît?
Victor
Cela signifie que vous devez éviter la répétition du numéro author_id (si le story_id est égal pour deux lignes, leur author_id est également égal) et diviser votre table en deux tables comme décrit dans mon article. Vous pouvez ainsi éviter la répétition de author_id.
miracle173