用 MySQL 实现版本控制系统

Vic*_*tor 17 mysql database-design

我知道这里这里有人问这个问题,但我有不同的可能实现的相同想法,我需要一些帮助。

最初,我的blogstories表具有以下结构:

| 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()             |
Run Code Online (Sandbox Code Playgroud)

在我决定要为博客上的每个故事实施一些版本控制系统后,我想到的第一件事就是创建一个不同的表来保存编辑;在那之后,我想我可以修改现有的表来保存版本而不是edits。这是我想到的结构:

| 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                                  |
Run Code Online (Sandbox Code Playgroud)

我来这里的原因:

  • uid初始表的字段在表中是唯一的。现在,它story_id不再是独一无二的了。我该如何处理?(我以为我可以解决story_id = x然后找到最新版本,但这似乎非常消耗资源,所以请提供您的建议)
  • author_id字段值在表的每一行中重复。我应该在哪里以及如何保存它?

编辑

唯一码生成过程在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;
  }
}
Run Code Online (Sandbox Code Playgroud)

该代码是用Hack编写的,最初是由@Scott在他的回答中用PHP 编写的。

字段author_ideditor_id 可以不同,因为有足够权限的用户可以编辑任何人的故事。

MDC*_*CCL 25

从概念的角度分析场景——它呈现与称为时间数据库的主题相关的特征——可以确定:(a)“现在”的博客故事版本和(b)“过去”的博客故事版本,尽管非常类似,是不同类型的实体。

除此之外,在逻辑抽象级别工作时,不同类型的事实(由行表示)必须保留在不同的表中。在所考虑的情况下,即使非常相似,(i) 关于“现在”版本的事实与 (ii) 关于“过去”版本的事实不同。

因此,我建议通过两个表来管理这种情况:

  • 一个专门致力于为“当前”或“存在”版本的的博客故事,并

  • 对于所有“以前”或“过去”版本,一个是独立的,但也与另一个相关联;

每个都有(1)稍微不同的列数和(2)不同的约束组。

回到概念层,我认为——在你的业务环境中——作者编辑器是可以被描述为可由用户扮演的角色的概念,这些重要方面取决于数据推导(通过逻辑级操作操作)和解释(由博客故事的读者和作者在计算机化信息系统的外部级别在一个或多个应用程序的帮助下进行)。

我将在下面详细说明所有这些因素和其他相关要点。

商业规则

根据我对您的需求的理解,以下业务规则制定(根据相关实体类型及其相互关系的类型放在一起)特别有助于建立相应的概念模式:

  • 一个用户写道零一或一对多BlogStories
  • 一个BlogStory持有零一或一对多BlogStoryVersions
  • 一个用户写了零一或多的BlogStoryVersions

说明性IDEF1X图

因此,为了借助图形设备阐述我的建议,我创建了一个示例 IDEF1X,图表源自上述制定的业务规则和其他似乎相关的功能。如图1所示:

图 1 - 博客故事版本 IDEF1X 图

为什么BlogStoryBlogStoryVersion 被概念化为两种不同的实体类型?

因为:

  • 一个BlogStoryVersion实例(即“过去”之一)始终保持为一个值UpdatedDateTime财产,而BlogStory发生(即“存在”一个)从不保存它。

  • 此外,这些类型的实体由两组不同属性的值唯一标识:BlogStoryNumber(在出现BlogStory 的情况下)和BlogStoryNumber加上CreatedDateTime(在BlogStoryVersion实例的情况下)。


一个 用于信息建模集成定义 IDEF1X)是被确立为一个非常可取的数据建模技术标准是由美国在1993年12月美国国家标准与技术研究院(NIST)。它是基于早期的理论材料撰写由独家发起的的关系模型,即EF科德博士; 关于数据的实体关系视图,由PP Chen 博士开发;以及由 Robert G. Brown 创建的逻辑数据库设计技术。


说明性的逻辑 SQL-DDL 布局

然后,根据之前介绍的概念分析,我在下面声明了逻辑级设计:

-- 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.
);
Run Code Online (Sandbox Code Playgroud)

在 MySQL 5.6 上运行的这个 SQL Fiddle 中进行了测试。

BlogStory

正如您在演示设计中看到的那样,我已经BlogStory使用 INT 数据类型定义了PRIMARY KEY(为简洁起见,PK)列。在这方面,您可能希望修复一个内置的自动过程,该过程在每行插入中为此类列生成并分配一个数值。如果您不介意在这组值中偶尔留下空白,那么您可以使用MySQL 环境中常用的AUTO_INCREMENT属性。

输入所有单个BlogStory.CreatedDateTime数据点时,您可以使用NOW() 函数,该函数返回数据库服务器中准确 INSERT 操作瞬间的当前日期和时间值。对我来说,这种做法显然比使用外部例程更合适,也更不容易出错。

如果如(现已删除)评论中所述,您想避免维护BlogStory.Title重复值的可能性,则必须为此列设置UNIQUE约束。由于给定标题可以由几个(甚至全部)“过去”共享BlogStoryVersions,然后UNIQUE约束应该不会被用于建立BlogStoryVersion.Title列。

我包含了BIT(1)BlogStory.IsActive类型的列(尽管也可以使用TINYINT),以防您需要提供“软”或“逻辑”删除功能。

BlogStoryVersion表的详细信息

另一方面,BlogStoryVersion表的 PK由 (a)BlogStoryNumber和 (b) 一列组成,该列CreatedDateTime当然标记了BlogStory行经历 INSERT的精确时刻。

BlogStoryVersion.BlogStoryNumber,除了作为 PK 的一部分之外,还被限制为引用 的外键 (FK) BlogStory.BlogStoryNumber,该配置强制执行这两个表的行之间的引用完整性。在这方面,BlogStoryVersion.BlogStoryNumber没有必要实现 a 的自动生成,因为设置为 FK,插入到此列中的值必须“从”已包含在相关BlogStory.BlogStoryNumber对应项中的值“提取” 。

BlogStoryVersion.UpdatedDateTime正如预期的那样,该列应保留BlogStory修改行并因此添加到BlogStoryVersion表中的时间点。因此,您也可以在这种情况下使用 NOW() 函数。

所述区间之间理解BlogStoryVersion.CreatedDateTimeBlogStoryVersion.UpdatedDateTime表达整个时期,在此期间一个BlogStory行是“存在”或“电流”。

Version列的注意事项

它可以认为是有用的BlogStoryVersion.CreatedDateTime作为保持代表一个特定的“过去”值的列版本A的BlogStory。我认为这比 aVersionId或更有益VersionCode,因为从人们往往更熟悉时间概念的意义上说,它对用户更友好。例如,博客作者或读者可以以类似于以下的方式引用BlogStoryVersion

  • “我希望看到具体的版本中的BlogStory通过识别号码 1750这是创建26 August 20159:30”。

作者编辑的角色:数据推导和解释

通过这种方法,可以很容易地分辨谁拥有“原始”的AuthorId一个具体的BlogStory选择“最早的”版本有一定的BlogStoryIdBlogStoryVersion凭借应用的表MIN()函数BlogStoryVersion.CreatedDateTime

这样,所有“以后”或“后继”版本行中BlogStoryVersion.AuthorId包含的每个值自然表示手头相应版本作者标识符,但也可以说这样的值同时表示相关用户作为博客故事“原始”版本编辑者所扮演的角色

是的,一个给定的AuthorId值可能被多BlogStoryVersion行共享,但这实际上是一条信息,它告诉了每个Version 的一些非常重要的东西,所以所述数据的重复不是问题。

DATETIME 列的格式

至于 DATETIME 数据类型,是的,您说得对,“ MySQL 以 ' YYYY-MM-DD HH:MM:SS' 格式检索并显示 DATETIME 值”,但是您可以放心地以这种方式输入相关数据,并且当您必须执行查询时,您只需要利用内置的DATE 和 TIME 函数,除其他外,以适当的格式为您的用户显示相关值。或者您当然可以通过您的应用程序代码执行这种数据格式化。

BlogStoryUPDATE 操作的含义

每次BlogStory一行发生 UPDATE 时,您必须确保在修改发生之前“存在”的相应值然后被插入到BlogStoryVersion表中。因此,我强烈建议在单个ACID 事务中完成这些操作,以保证它们被视为不可分割的工作单元。您也可以使用触发器,但可以这么说,它们往往会使事情变得不整洁。

引入一个VersionIdVersionCode

如果您选择(由于业务情况或个人喜好)加入一个BlogStory.VersionIdBlogStory.VersionCode列来区分BlogStoryVersions,您应该考虑以下可能性:

  1. AVersionCode可能需要在 (i) 整个BlogStory表和 (ii) 中都是唯一的BlogStoryVersion

    因此,您必须实施经过仔细测试且完全可靠的方法,以便生成和分配每个Code值。

  2. 也许,这些VersionCode值可以在不同的BlogStory行中重复,但绝不会与相同的BlogStoryNumber. 例如,你可以有:

    • 一个BlogStoryNumber 3- 版本83o7c5c,同时,
    • 一个BlogStoryNumber 86- 版本83o7c5c
    • 一个BlogStoryNumber 958- 版本83o7c5c

后一种可能性开启了另一种选择:

  1. 保持一个VersionNumberBlogStories,所以有可能是:

    • BlogStoryNumber 23- 版本1, 2, 3…
    • BlogStoryNumber 650- 版本1, 2, 3…
    • BlogStoryNumber 2254- 版本1, 2, 3…
    • 等等。

在一个表中保存“原始”和“后续”版本

尽管在同一个单独的表中维护所有BlogStoryVersions是可能的,但我建议不要这样做,因为你会混合两种不同的(概念)类型的事实,从而对

  • 数据约束和操作(在逻辑级别),以及
  • 相关的处理和存储(在物理层)。

但是,如果您选择遵循该行动方案,您仍然可以利用上面详述的许多想法,例如:

  • 一个复合PK由一个INT柱(的BlogStoryNumber)和DATETIME柱(CreatedDateTime);
  • 使用服务器功能以优化相关流程,以及
  • 作者编辑器导出角色

看到通过继续采用这种方法,一旦添加了“较新的”版本,就会复制一个BlogStoryNumber值,您可以评估的选项(与上一节中提到的非常相似)正在建立一个PK列组成,并以这种方式,你将能够唯一标识每个版本A的BlogStory。你也可以尝试结合和。BlogStoryBlogStoryNumberVersionCodeBlogStoryNumberVersionNumber

类似场景

您可能会找到我对这个帮助问题的回答,因为我还建议在相关数据库中启用时间功能来处理类似的场景。


Tom*_*att 5

一种选择是使用版本范式 (vnf)。优点包括:

  • 当前数据和所有过去的数据位于同一个表中。
  • 相同的查询用于检索当前数据或截至任何特定日期的当前数据。
  • 对版本化数据的外键引用的工作方式与对非版本化数据的外键引用相同。

在您的情况下,另一个好处是,由于版本化数据是通过将生效日期(进行更改的日期)作为密钥的一部分来唯一标识的,因此不需要单独的 version_id 字段。

是对非常相似类型的实体的解释。

更多详细信息可以在此处的幻灯片演示和此处未完全完成的文档中找到