web-dev-qa-db-fra.com

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

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 table blogstories 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 histoire du blog, la première chose qui m'est venue à l'esprit était de créer un tableau différent pour contenir les modifications ; après cela, j'ai pensé que je pouvais modifier la table existante pour contenir les versions au lieu de les 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 champ uid de la table initiale était UNIQUE dans la table. Maintenant le story_id n'est plus unique. Comment dois-je gérer cela? (Je pensais pouvoir répondre à story_id = x puis trouvez la dernière version, mais cela semble très consommateur de ressources, veuillez donc donner votre avis)
  • author_id la valeur du champ se répète dans chaque ligne du tableau. Où et comment dois-je le conserver?

Modifier

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

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é écrit à l'origine en PHP par @ Scott dans son réponse .

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

15
Victor

Analyser le scénario - qui présente des caractéristiques associées au sujet connu sous le nom bases de données temporelles - d'un point de vue conceptuel, on peut déterminer que: (a) un "présent" Version de l'histoire du blog et (b) un "passé" Version de l'histoire du blog, bien que très similaires, sont des entités de types différents.

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

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

  • une dédiée exclusivement aux "actuelles" ou "présentes" Versions des Blog Stories, et

  • l'un qui est séparé, mais aussi lié à l'autre, pour toutes les "précédentes" ou "passées" Versions;

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élimitées comme Rôles qui peut être joué par un utilisateur, et ces aspects importants dépendent des données dérivation (via des opérations de manipulation au niveau logique) et interprétation (réalisée par les Blog Stories lecteurs et écrivains, 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:

  • A Utilisateur écrit zéro-un-ou-plusieurs BlogStories
  • A BlogStory contient zéro un ou plusieurs BlogStoryVersions
  • A Utilisateur a écrit zéro-un-ou-plusieurs BlogStoryVersions

Diagramme expositoire IDEF1X

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

Figure 1 - Blog Story Versions IDEF1X Diagram

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

Parce que:

  • Une instance BlogStoryVersion (c.-à-d. Une instance "passée") contient toujours une valeur pour une propriété UpdatedDateTime, tandis qu'une propriété BlogStory = l'occurrence (c'est-à-dire une occurrence "présente") ne la retient 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).


a   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 standard en décembre 1993 par États-Unis Institut national des normes et de la technologie (NIST). Il est basé sur les premiers matériaux théoriques rédigés par le seul créateur du modèle relationnel , c'est-à-dire Dr E. F. Codd ; sur la vue Entité-Relation des données, développée par Dr P. P. 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 table BlogStory

Comme vous pouvez le voir dans la conception de la démonstration, j'ai défini la colonne BlogStory PRIMARY KEY (PK pour la brièveté) avec le type de données INT. À cet égard, vous souhaiterez peut-être 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 saisissez tous vos points de données BlogStory.CreatedDateTime Individuels, vous pouvez utiliser la fonction MAINTENANT () , qui renvoie les valeurs Date et 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 les valeurs en double de BlogStory.Title, Vous devez configurer un UNIQUE contrainte pour cette colonne. En raison du fait qu'un Titre donné peut être partagé par plusieurs (ou même tous les) "passés" BlogStoryVersions, alors une contrainte UNIQUE devrait pas être établi pour la colonne BlogStoryVersion.Title.

J'ai inclus la colonne BlogStory.IsActive De type BIT (1) (bien qu'un TINYINT puisse tout aussi bien être utilisé) au cas où vous auriez besoin de fournir une fonctionnalité de suppression "douce" ou "logique".

Détails sur la table BlogStoryVersion

D'autre part, le PK de la table BlogStoryVersion est composé de (a) BlogStoryNumber et (b) une colonne nommée CreatedDateTime qui, bien sûr, marque l'instant précis dans lequel une ligne BlogStory 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 le lignes de ces deux tableaux. À cet égard, l'implémentation d'une génération automatique d'un BlogStoryVersion.BlogStoryNumber N'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 le BlogStory.BlogStoryNumber homologue.

La colonne BlogStoryVersion.UpdatedDateTime Doit conserver, comme prévu, le moment où une ligne BlogStory a été modifiée et, par conséquent, ajoutée à la table BlogStoryVersion. Par conséquent, vous pouvez également utiliser la fonction NOW () dans cette situation.

Le Intervalle compris entre BlogStoryVersion.CreatedDateTime Et BlogStoryVersion.UpdatedDateTime Exprime la totalité Période pendant laquelle une ligne BlogStory a été "Présent" ou "actuel".

Considérations pour une colonne Version

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

  • "Je veux voir le Version spécifique du BlogStory identifié par Number1750 Qui était = Créé sur 26 August 2015 À 9:30 ”.

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" AuthorId d'un béton BlogStory SÉLECTIONNER le "plus tôt" Version d'un certains BlogStoryId DE la table BlogStoryVersion grâce à l'application de la fonction MIN () à BlogStoryVersion.CreatedDateTime.

De cette façon, chaque valeur BlogStoryVersion.AuthorId Contenue dans tous les lignes "ultérieures" ou "suivantes" Versions indiquent, naturellement, les Author identifiant du Version respectif à portée de main, mais on peut aussi dire qu'une telle valeur est, en même temps, dénotant le Rôle joué par la personne impliquée Utilisateur comme Éditeur de l'original Version d'un BlogStory .

Oui, une valeur AuthorId donnée peut être partagée par plusieurs lignes BlogStoryVersion, mais il s'agit en fait d'une information qui dit quelque chose de très significatif sur chaque Version, donc la répétition de ladite donnée 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 format 'YYYY-MM-DD HH:MM:SS' ", mais vous pouvez entrer en toute confiance les données pertinentes de cette manière , et lorsque vous devez effectuer une requête, il vous suffit d'utiliser les fonctions intégrées fonctions DATE et TIME afin, entre autres, d'afficher les valeurs concernées dans le 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 opérations BlogStory UPDATE

Chaque fois qu'une ligne BlogStory subit une MISE À JOUR, vous devez vous assurer que les valeurs correspondantes qui étaient "présentes" jusqu'à ce que la modification ait lieu sont ensuite INSÉRÉES dans le BlogStoryVersion table. Ainsi, je suggère fortement de réaliser ces opérations dans un seul ACID TRANSACTION pour garantir qu'elles sont 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 colonne VersionId ou VersionCode

Si vous optez (en raison de circonstances professionnelles ou de préférences personnelles) pour incorporer une colonne BlogStory.VersionId Ou BlogStory.VersionCode Pour distinguer les BlogStoryVersions, vous devez réfléchir aux possibilités suivantes:

  1. Un VersionCode peut être requis pour être UNIQUE dans (i) l'ensemble de la table BlogStory et également dans (ii) BlogStoryVersion.

    Par conséquent, vous devez implémenter une méthode soigneusement testée et totalement fiable afin de générer et d'assigner chaque valeur Code.

  2. Peut-être que les valeurs VersionCode pourraient être répétées dans différentes lignes BlogStory, mais jamais dupliqué avec le même BlogStoryNumber. Par exemple, vous pourriez avoir:

    • a BlogStoryNumber 3 - Version 83o7c5c et, simultanément,
    • a BlogStoryNumber 86 - Version 83o7c5c et
    • a BlogStoryNumber 958 - Version 83o7c5c.

La dernière possibilité ouvre une autre alternative:

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

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

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

Bien que le maintien de la table BlogStoryVersions dans la table même individuelle base soit possible, je vous suggère de ne pas le faire car vous le feriez mélanger 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:

  • a composite PK composé d'une colonne INT (BlogStoryNumber) et d'une colonne DATETIME (CreatedDateTime);
  • l'utilisation de fonctions serveur afin d'optimiser les processus pertinents, et
  • le Auteur et Éditeur dérivable Rôles.

Voyant qu'en procédant à une telle approche, une valeur BlogStoryNumber sera dupliquée dès que "plus récent" Versions seront ajoutés, un L'option que et que vous pourriez évaluer (qui est très similaire à celles mentionnées dans la section précédente) établit un BlogStory PK composé des colonnes BlogStoryNumber et VersionCode, dans ce de manière à pouvoir identifier de manière unique chaque Version d'un BlogStory. Et vous pouvez aussi essayer avec une combinaison de BlogStoryNumber et VersionNumber.

Scénario similaire

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

23
MDCCL

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 à jour à 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 d'entrée en vigueur (la date à laquelle la modification a été effectuée) dans la clé, un champ version_id distinct n'est pas requis.

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

2
TommCatt

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_id, la 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 utiliser que version_id comme clé

1
miracle173