Implementando sistema de versionamento com MySQL

15

Sei que isso foi solicitado aqui e aqui , mas tenho a mesma idéia com uma implementação possível diferente e preciso de ajuda.

Inicialmente eu tive minha blogstoriesmesa com esta estrutura:

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

Depois que decidi que queria implementar algum sistema de versão para cada matéria do blog, a primeira coisa que me veio à mente foi criar uma tabela diferente para realizar edições ; depois disso, pensei em modificar a tabela existente para conter versões em vez de edições . Esta é a estrutura que me veio à 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                                  |

As razões pelas quais eu vim aqui:

  • O uidcampo da tabela inicial era UNIQUE na tabela. Agora, o story_idnão é mais exclusivo. Como devo lidar com isso? (Eu pensei que poderia resolver story_id = xe encontrar a versão mais recente, mas isso parece consumir muitos recursos, por isso, dê seu conselho)
  • author_ido valor do campo está se repetindo em cada linha da tabela. Onde e como devo guardar?

Editar

O processo de geração de códigos exclusivos está na CreateUniqueCodefunção:

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

O código está escrito em Hack e foi originalmente escrito em PHP por @Scott em sua resposta .

Os campos author_ide editor_id podem ser diferentes, porque existem usuários com permissões suficientes para editar as histórias de qualquer pessoa.

Vencedor
fonte

Respostas:

23

Analisando o cenário - que apresenta características associadas ao assunto conhecido como bancos de dados temporais - de uma perspectiva conceitual, pode-se determinar que: (a) uma versão “presente” da história do blog e (b) uma versão “anterior” da história do blog , embora muito semelhantes, são entidades de diferentes tipos.

Além disso, ao trabalhar no nível lógico de abstração, fatos (representados por linhas) de tipos distintos devem ser retidos em tabelas distintas. No caso em consideração, mesmo quando bastante semelhantes, (i) fatos sobre as versões "presentes" são diferentes de (ii) fatos sobre as versões "passadas" .

Portanto, recomendo gerenciar a situação por meio de duas tabelas:

  • um dedicado exclusivamente para as versões "atuais" ou "presentes" das histórias do blog e

  • um que é separado, mas também vinculado ao outro, para todas as versões “anteriores” ou “passadas” ;

cada uma com (1) um número ligeiramente distinto de colunas e (2) um grupo diferente de restrições.

De volta à camada conceitual, considero que - em seu ambiente de negócios - autor e editor são noções que podem ser delineadas como funções que podem ser desempenhadas por um usuário , e esses aspectos importantes dependem da derivação de dados (através de operações de manipulação no nível lógico) e interpretação (realizada pelos leitores e escritores do Blog Stories , no nível externo do sistema informatizado de informações, com a assistência de um ou mais programas aplicativos).

Vou detalhar todos esses fatores e outros pontos relevantes da seguinte forma.

Regras do negócio

De acordo com meu entendimento de seus requisitos, as seguintes formulações de regras de negócios (reunidas em termos dos tipos de entidades relevantes e seus tipos de inter-relações) são especialmente úteis no estabelecimento do esquema conceitual correspondente :

  • Um usuário grava zero ou um ou muitos BlogStories
  • Um BlogStory contém zero-um-ou-muitos BlogStoryVersions
  • Um usuário escreveu zero-um-ou-muitos BlogStoryVersions

Diagrama do IDEF1X expositivo

Consequentemente, para expor minha sugestão em virtude de um dispositivo gráfico, criei uma amostra IDEF1X, um diagrama derivado das regras de negócios formuladas acima e de outros recursos que parecem pertinentes. É mostrado na Figura 1 :

Figura 1 - Versões da história do blog Diagrama IDEF1X

Por que o BlogStory e o BlogStoryVersion são conceituados como dois tipos diferentes de entidade?

Porque:

  • Uma instância do BlogStoryVersion (por exemplo, uma "passada") sempre mantém um valor para uma propriedade UpdatedDateTime , enquanto uma ocorrência do BlogStory (por exemplo, uma "presente") nunca a mantém.

  • Além disso, as entidades desses tipos são identificadas exclusivamente pelos valores de dois conjuntos distintos de propriedades: BlogStoryNumber (no caso das ocorrências do BlogStory ) e BlogStoryNumber mais CreatedDateTime (no caso das instâncias do BlogStoryVersion ).


uma definição de integração para modelagem de informações ( IDEF1X ) é uma técnica de modelagem de dados altamente recomendável que foi estabelecida como padrão em dezembro de 1993 pelo Instituto Nacional de Padrões e Tecnologia dos Estados Unidos(NIST). Baseia-se no material teórico inicial criado pelo único autordo modelo relacional , ou seja, Dr. EF Codd ; navisão Entity-Relationship of Data, desenvolvida pelo Dr. PP Chen ; e também na Logical Database Design Technique, criada por Robert G. Brown.


Layout SQL-DDL lógico ilustrativo

Então, com base na análise conceitual apresentada anteriormente, declarei o design de nível lógico abaixo:

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

Testado neste SQL Fiddle que roda no MySQL 5.6.

A BlogStorymesa

Como você pode ver no design da demonstração, eu defini a BlogStorycoluna PRIMARY KEY (PK for brevity) com o tipo de dados INT. Nesse sentido, convém corrigir um processo automático interno que gera e atribui um valor numérico para essa coluna em cada inserção de linha. Se você não se importa em deixar lacunas ocasionalmente nesse conjunto de valores, pode empregar o atributo AUTO_INCREMENT , comumente usado em ambientes MySQL.

Ao inserir todos os seus BlogStory.CreatedDateTimepontos de dados individuais , você pode utilizar a função NOW () , que retorna os valores de Data e Hora atuais no servidor de banco de dados no instante exato da operação INSERT. Para mim, essa prática é decididamente mais adequada e menos propensa a erros do que o uso de rotinas externas.

Desde que, conforme discutido nos comentários (agora removidos), você deseje evitar a possibilidade de manter BlogStory.Titlevalores duplicados, precisará configurar uma restrição UNIQUE para esta coluna. Devido ao fato de que um determinado título pode ser compartilhado por várias (ou mesmo todas) as BlogStoryVersions "passadas" , uma restrição ÚNICA não deve ser estabelecida para a BlogStoryVersion.Titlecoluna.

Incluí a BlogStory.IsActivecoluna do tipo BIT (1) (embora um TINYINT também possa ser usado), caso você precise fornecer a funcionalidade DELETE "leve" ou "lógica".

Detalhes sobre a BlogStoryVersiontabela

Por outro lado, a PK da BlogStoryVersiontabela é composta por (a) BlogStoryNumbere (b) uma coluna denominada CreatedDateTimeque, é claro, marca o instante exato em que uma BlogStorylinha passou por um INSERT.

BlogStoryVersion.BlogStoryNumber, além de fazer parte do PK, também é restrito como uma FOREIGN KEY (FK) que faz referência BlogStory.BlogStoryNumber, uma configuração que impõe integridade referencial entre as linhas dessas duas tabelas. Nesse sentido, a implementação de uma geração automática de a BlogStoryVersion.BlogStoryNumbernão é necessária porque, sendo definido como um FK, os valores INSERTed nesta coluna devem ser "extraídos" daqueles já incluídos na BlogStory.BlogStoryNumbercontraparte relacionada .

A BlogStoryVersion.UpdatedDateTimecoluna deve reter, como esperado, o momento em que uma BlogStorylinha foi modificada e, como conseqüência, adicionada à BlogStoryVersiontabela. Portanto, você também pode usar a função NOW () nessa situação.

O Intervalo compreendeu BlogStoryVersion.CreatedDateTimee BlogStoryVersion.UpdatedDateTimeexpressa todo o Período durante o qual uma BlogStorylinha estava "presente" ou "atual".

Considerações para uma Versioncoluna

Pode ser útil pensar BlogStoryVersion.CreatedDateTimecomo a coluna que contém o valor que representa uma versão "passada" específica de uma BlogStory . Considero isso muito mais benéfico do que um VersionIdou VersionCode, uma vez que é mais amigável no sentido em que as pessoas tendem a estar mais familiarizadas com os conceitos de tempo . Por exemplo, os autores ou leitores do blog podem se referir a uma BlogStoryVersion de uma maneira semelhante à seguinte:

  • "Quero ver a versão específica do BlogStory identificada pelo número 1750 que foi criado em 26 August 2015às 9:30".

O Autor e Editor de Funções: derivação de dados e interpretação

Com esta abordagem, você pode facilmente distinguir quem detém o “original” AuthorIdde um concreto BlogStory selecionando a opção “primeira” versão de um determinado BlogStoryIda partir da BlogStoryVersiontabela, em virtude da aplicação da função MIN () para BlogStoryVersion.CreatedDateTime.

Dessa forma, cada BlogStoryVersion.AuthorIdvalor contido em todas as linhas "posteriores" ou "subsequentes" das versões indica, naturalmente, o identificador de autor da respectiva versão em questão, mas também se pode dizer que esse valor está, ao mesmo tempo, denotando o papel desempenhado pelo usuário envolvido como editor da versão "original" de uma BlogStory .

Sim, um determinado AuthorIdvalor pode ser compartilhado por várias BlogStoryVersionlinhas, mas essa é realmente uma informação que diz algo muito significativo sobre cada Versão , portanto, a repetição do referido dado não é um problema.

O formato das colunas DATETIME

Quanto ao tipo de dados DATETIME, sim, você está certo: “O MySQL recupera e exibe valores DATETIME no YYYY-MM-DD HH:MM:SSformato ' ' ' , mas é possível inserir com segurança os dados pertinentes dessa maneira e, quando você precisar executar uma consulta, basta use as funções incorporadas de DATA e HORA para, entre outras coisas, mostrar os valores relativos no formato apropriado para seus usuários. Ou você certamente poderia realizar esse tipo de formatação de dados por meio do código dos programas de aplicativos.

Implicações das BlogStoryoperações UPDATE

Toda vez que uma BlogStorylinha sofre um UPDATE, você deve garantir que os valores correspondentes que estavam "presentes" até a modificação ocorreu sejam INSERTed na BlogStoryVersiontabela. Portanto, sugiro realizar essas operações em uma única TRANSAÇÃO ÁCIDA para garantir que elas sejam tratadas como uma Unidade de Trabalho indivisível. Você também pode empregar TRIGGERS, mas eles tendem a tornar as coisas desarrumadas, por assim dizer.

Introduzindo uma VersionIdou VersionCodecoluna

Se você optar por (devido às circunstâncias comerciais ou preferência pessoal) incorporar uma BlogStory.VersionIdou BlogStory.VersionCodecoluna para distinguir as Versões do BlogStory , considere as seguintes possibilidades:

  1. Uma VersionCodepoderia ser necessária para ser único no (i) o conjunto BlogStorytabela e também em (ii) BlogStoryVersion.

    Portanto, você deve implementar um método cuidadosamente testado e totalmente confiável para gerar e atribuir cada Codevalor.

  2. Talvez, os VersionCodevalores possam ser repetidos em BlogStorylinhas diferentes , mas nunca duplicados junto com o mesmo BlogStoryNumber. Por exemplo, você poderia ter:

    • um BlogStoryNumber 3- versão83o7c5c e, simultaneamente,
    • um BlogStoryNumber 86- Versão83o7c5c e
    • um BlogStoryNumber 958- Versão83o7c5c .

A possibilidade posterior abre outra alternativa:

  1. Mantendo um VersionNumberpara BlogStories, para que possa haver:

    • BlogStoryNumber 23- versões1, 2, 3… ;
    • BlogStoryNumber 650- versões1, 2, 3… ;
    • BlogStoryNumber 2254- versões1, 2, 3… ;
    • etc.

Mantendo as versões "original" e "subsequente" em uma única tabela

Embora seja possível manter todas as BlogStoryVersions na mesma tabela base individual , sugiro não fazer isso porque você misturaria dois tipos distintos (conceituais) de fatos, que, portanto, têm efeitos colaterais indesejáveis ​​no

  • restrições e manipulação de dados (no nível lógico), juntamente com
  • o processamento e armazenamento relacionados (na camada física).

Mas, desde que você opte por seguir esse curso de ação, ainda poderá tirar proveito de muitas das idéias detalhadas acima, por exemplo:

  • um composto de PK que consiste de uma coluna INT ( BlogStoryNumber) e uma coluna DATETIME ( CreatedDateTime);
  • o uso das funções do servidor para otimizar os processos pertinentes e
  • Funções derivadas do autor e do editor .

Visto que, ao prosseguir com essa abordagem, um BlogStoryNumbervalor será duplicado assim que versões "mais recentes" forem adicionadas, uma opção que você pode avaliar (que é muito parecida com as mencionadas na seção anterior) está estabelecendo uma BlogStoryPK composto pelas colunas BlogStoryNumbere VersionCode, dessa maneira, você seria capaz de identificar exclusivamente cada versão de uma BlogStory . E você pode tentar com uma combinação de BlogStoryNumbere VersionNumbertambém.

Cenário semelhante

Você pode encontrar minha resposta para essa pergunta de ajuda, pois também proponho permitir que recursos temporais no banco de dados relacionado lidem com um cenário comparável.

MDCCL
fonte
2

Uma opção é usar o formulário normal de versão (vnf). As vantagens incluem:

  • Os dados atuais e todos os dados anteriores residem na mesma tabela.
  • A mesma consulta é usada para recuperar dados atuais ou dados atuais em qualquer data específica.
  • As referências de chave estrangeira a dados com versão funcionam da mesma maneira que para dados sem versão.

Um benefício adicional no seu caso, como os dados com versão são identificados exclusivamente ao tornar a data efetiva (a data em que a alteração foi feita) parte da chave, um campo version_id separado não é necessário.

Aqui está uma explicação para um tipo de entidade muito semelhante.

Mais detalhes podem ser encontrados em uma apresentação de slides aqui e um documento não totalmente concluído aqui

TommCatt
fonte
1

Sua relação

(id da história, id da versão, id do editor, id do autor, carimbo de data e hora, título, conteúdo, capa)

não está na terceira forma normal. Para todas as versões da sua história, o author_id é o mesmo. Então você precisa de duas relações para superar isso

(id da história, id do autor)
(id da história, id da versão, id do editor, carimbo de data e hora, título, conteúdo, capa)

A chave da primeira relação é story_id, a chave da segunda relação é a chave combinada (story_id, version_id). Se você não gostar de chave combinada, poderá usar apenas version_idcomo chave

miracle173
fonte
2
Isso não parece resolver o meu problema, apenas enfatiza-os #
Victor Victor
Portanto, ele nem responde que o author_id valor do campo da consulta está se repetindo em cada linha da tabela. Onde e como devo guardá-lo '?
miracle173
2
Eu realmente não entendo o que sua resposta indica. Pode ser porque eu não sou um falante nativo de inglês, então você poderia tentar explicá-lo com mais e simples palavras, por favor?
Victor Victor
Isso significa que você deve evitar a repetição do número author_id (se o story_id for igual para duas linhas, o author_id também é igual) e dividir sua tabela nas duas tabelas, conforme descrito no meu post. Assim, você pode evitar a repetição de author_id.
precisa saber é o seguinte