dbmover/core

基于PHP的数据库版本控制工具,核心组件

0.10.3 2023-05-22 12:28 UTC

README

基于PHP的数据库版本控制工具,核心包。

安装

推荐使用Composer安装DbMover。目前DbMover支持通过dbmover/pgsqldbmover/mysql分别使用PostgreSQL和MySQL。例如:

composer require dbmover/pgsql

设计目标

Web应用程序通常与SQL数据库一起工作。程序员将这样的数据库布局在一个“模式文件”中,这本质上是SQL语句。当一名新的程序员开始在一个项目上工作时,这是可行的,因为她可以简单地创建数据库,并运行模式以启动。问题是当在开发过程中或应用程序的生命周期中,需要对此模式进行更改时。

手动执行此操作既繁琐又容易出错。记得为每个更改编写迁移也很繁琐,跟踪哪些迁移已经被应用(或未应用)也很容易出错(真实生活案例:导入特定数据库的较旧版本以解决特定问题,并且迁移“注册表”本身也过时)。

DbMover通过查看中央的、领先的、版本控制的模式文件并应用所需的任何更改来自动化此任务。这允许您盲目运行vendor/bin/dbmover,例如在post-receive钩子中。

dbmover.json文件

DbMover使用dbmover.json文件进行配置。这应该在项目的根目录中(即从vendor/bin向上两个文件夹)。格式如下:

{
    "your dsn": {
        "user": "yourUserName",
        "pass": "something secret",
        "schema": ["path/to/file1.sql", "path/to/file2.sql"[, ...]],
        "plugins": []
    }
}

每次您运行DbMover时,它将遍历所有条目并应用您请求的内容。许多项目将使用单个数据库,但如您所见,DbMover在单个配置文件中完全支持多个数据库。

当然,您不希望将实际的用户名/密码放在受版本控制的配置文件中(您确实希望这样做)。最佳实践(除非您独自工作)是版本控制一个带有用户名/密码空白的dbmover.json.sample文件,但包含其他重要信息(模式和插件)。

DSN

这是数据库的“DSN”连接字符串。确切格式将因供应商而异,但通常为“vendor:dname=NAME;host=HOST;port=PORT”类型,其中port通常可以省略以使用默认值。如有疑问,请咨询系统管理员。目前支持的供应商是pgsqlmysql;如果您想贡献另一个供应商,请参阅下面的“贡献”部分。

模式

这是一个模式文件的数组,相对于您仓库的根目录。DbMover将按顺序处理它们。请注意,提供将模式拆分为多个文件的选项是为了方便/可维护性 - 在DbMover开始工作之前,DbMover会将它们全部连接在一起。

插件

DbMover将加载的插件数组以执行迁移。通过使用插件,我们使DbMover非常适用于您的确切需求。要仅使用合理的默认值,只需指定特定数据库供应商的插件(例如,Dbmover\Pgsql\Plugin)。有关插件的更多信息见下文。

运行DbMover

只需简单地执行vendor/bin/dbmover。对于每个指定的数据库,它将对您定义的模式执行请求的操作。如果您已经根据上面的教程填写了dbmover.json并在现在运行它...什么也没有发生。这是因为所有实际功能都在插件中。您需要在dbmover.json配置中指定它们。

插件

截至版本0.6,DbMover使用插件来指定操作。需要注意的是,一个插件本身不应该改变数据库中的任何内容;它们用于收集执行迁移时执行的命令。因此,由于您的plugins数组此时为空,DbMover尚不知道要做什么。请参阅上面的语法。

插件按指定顺序处理,并且可以指定多次。在这种情况下,它们将简单地多次运行(这实际上是很有用的)。

每个插件实际上运行两次;一次修改SQL,一次在__destruct中进行清理。销毁调用按与调用调用相同的顺序进行(请参阅下文“编写自定义插件”)。

元包

插件还可以加载其他插件;事实上,有一些官方提供的元插件。通常,它们会为您数据库的类型和设计执行所需操作。但是,您也可以混合搭配,编写自己的或组合这些。

例如,假设您有一个MySQL数据库,并且只想让DbMover迁移所有内容。在这种情况下,您应该安装以下插件

composer require dbmover/mysql

...并注册此单个插件

{
    ...
    plugins: ["Dbmover\\Mysql\\Plugin"]
}

编写自定义插件

每个插件都必须实现Dbmover\Core\PluginInterface。通常您会想要扩展抽象的Dbmover\Core\Plugin,但在某些情况下这可能是不可取的(因此有接口)。

插件使用单个参数构造:正在运行迁移的Dbmover\Core\Loader实例。通过此对象,您可以使用getPdo()方法访问底层PDO实例。它还通过getDatabase()公开当前数据库的名称。

插件的主要任务是接收当前可用的SQL,将其相关部分转换为迁移加载器的操作,并返回(通常是修改后的)SQL字符串。理想情况下,在所有插件都运行后,没有SQL可以检查。

上述主要任务是通过魔法__invoke方法完成的。它接受当前SQL作为字符串参数,并必须返回(可选修改后)的新当前SQL。

插件还可以选择实现一个__destroy方法。插件在运行顺序与运行顺序相同,在所有插件运行后。

元插件将覆盖__construct方法并手动加载它们自己的“子插件”。不要在__invoke__destruct实现中添加新插件 - 在这些运行时,DbMover已经组装了插件和行为的指定是不明确的(并且可能是不可预测的)。

数据库供应商通常会添加非常特定的行为。我们已经实现了我们所能想到的最多常见用例(即,它们适用于我们自己的相当复杂的数据库),但总可以总是进行改进。如果您编写了有用的插件并希望分享,请参阅下文的“贡献”。

编写您的模式

您应该将模式编写成好像要在完全空的数据库上运行一样 - 之后您应该有一些可以工作的东西,可能包括默认数据。

添加表

只需将新的表定义添加到模式中,然后重新运行。

添加列

忘记在数据库中添加列?没问题,只需在您的模式中添加它,然后重新运行DbMover。

请注意,新列始终追加到表的末尾。一些数据库驱动程序(如MySQL)支持BEFORE关键字,但例如PostgreSQL不支持,DbMover尽可能地数据库无关。

修改列

只需在模式文件中更改列定义,DbMover 会为您进行修改。这假设列保留相同的名称,并且其中包含的数据与新类型兼容(或可以丢弃);对于更复杂的修改,请参阅以下内容。

删除列

只需从模式中删除它们并重新运行。注意:之后它们将真正、真正地被删除,数据库不支持撤销。

删除表、视图等。

只需从模式中删除它们并重新运行。再次提醒:它们将被真正删除。

索引和外键约束

根据您的数据库供应商,在创建表时可能允许指定这些约束。对此的支持仍然是非常实验性的,并且肯定不完整。所以如果可能的话,不要这样做。相反,使用 CREATE INDEXALTER TABLE 语句在表创建后创建这些约束。

主键可能在 CREATE TABLE 块中已经指定。其他约束仍在开发中。

松散的 ALTER 语句

有时您在创建后需要特别对表进行 ALTER,例如当它有一个外键引用您稍后需要创建的表时。例如,一个 blog_posts 表可能引用一个 lastcomment,而 blog_post_comments 又反过来引用 blog_posts 上的 blog_id。在这种情况下,您首先创建帖子表,然后创建评论表(带其外键约束),最后将约束添加到帖子表中。

DbMover 将按模式文件中指定的顺序单独运行每个 ALTER TABLE 语句,因此只需在逻辑上添加外键的位置添加外键即可。该语句要么会静默失败(如果列不存在或类型错误,等待迁移),要么最终会成功。

更复杂的模式更改

某些事情自动确定可能比较困难,比如表或列重命名。您应该使用 IF 块包装这些更改,并在迁移需要时通过条件通过,否则失败。

根据您的数据库供应商,可能需要将这些内容包装在一个“废弃”过程中。例如,MySQL 只支持过程内的 IF。DbMover 的供应商特定类会为您处理这些。废弃过程以 tmp_ 为前缀。

请注意,条件运算符(ELSE IFELSIF)的确切语法也取决于供应商。确定是否需要重命名表的方法也取决于供应商(尽管在当前版本中 DbMover 仅支持 ANSI 兼容的数据库,因此您可以使用 INFORMATION_SCHEMA 来此目的)。

条件运算符

DbMover 通过 dbmover/conditionals 插件支持在模式中包含 IF 块。这是对 SQL 的扩展,因为这些块通常仅在过程中允许。DbMover 会为您包装它们。

插入默认数据

为防止重复插入,应将其包装在 IF NOT EXISTS () 条件中,如下所示

IF NOT EXISTS (SELECT 1 FROM mytable WHERE id = 1) THEN
    INSERT INTO mytable (id, value1, value2, valueN)
        VALUES (1, 2, 3, 4);
END IF;

这通常需要 dbmover/VENDOR-conditionals 插件(它未包含在元包中)。有关更多信息,请参阅 dbmover/mysql-conditionalsdbmover/pgsql-conditionals

从一个表转移到另一个表的数据

这有时是必要的。在这种情况下,您应使用 IF 块并查询例如 INFORMATION_SCHEMA(根据您的供应商)以确定迁移是否已运行。

重要:如果迁移已运行,则 IF 应评估为 false,以避免运行两次。请小心处理。

简化和抽象的伪示例

IF NOT EXISTS (SELECT 1 FROM INFORMATION_SCHEMA.TABLES WHERE ...) THEN
    INSERT INTO target SELECT * FROM original;
END IF;

有关条件运算符的说明,请参阅上一节中的注释。

注意事项

整洁

DbMover假设SQL格式良好,关键字全部大写。它并不具体验证您的SQL,尽管任何错误都会导致语句失败并停止脚本(因此理论上它们不会造成太大损害...)。DbMover会告诉您它遇到了什么错误。

“整齐”的意思是写CREATE TABLE而不是create Table等。

DbMover也不识别例如MySQL使用反引号转义保留字的情况。不要这么做,这是邪恶的。

对于ignore正则表达式,如果您需要,可以使用“奇怪”的对象名称,因为这些是逐字正则化的。

对于提升,假设要提升的语句在行的开头(即,例如,正则表达式中的/^IF )。

数据库可能区分大小写,也可能不区分大小写;请注意,DbMover区分大小写的,因此请保持拼写一致。

存储引擎和校对规则

目前DbMover忽略了这些。计划支持MySQL;对于PostgreSQL,更改校对规则是数据库级别的操作,DbMover无法处理(需要重新创建整个数据库)。

首先测试您的模式

始终针对更新的模式在测试数据库上运行DbMover。每个人都可能犯错,您不希望这些错误损坏生产数据库。最好是对实际生产数据库的副本进行测试。

在迁移期间关闭您的应用程序

根据您的要求和您的数据集大小,迁移可能需要几分钟。您不希望用户在模式尚未稳定时编辑任何数据!

DbMover无法决定您的应用程序如何处理其down状态。一个简单的方法是为此编写自己的插件

{
    "dsn": {
        "plugins": ['Myplugins\\Down', ..., 'Myplugins\\Up']
    }
}

处理down/up状态的简单方法是在应用程序的根目录中写入一个空文件(例如,简单地称为down),在前端控制器中检查它,并在再次启动应用程序时删除它。一个非常基本的示例

<?php

namespace Myplugins;

use Dbmover\Core\PluginInterface;

class Down implements PluginInterface
{
    public $description = 'Bringing application down...';

    public function __invoke(string $sql) : string
    {
        $cwd = getcwd();
        `touch $cwd/down`;
        return $sql;
    }
}

class Up implements PluginInterface
{
    public $description = 'Briging application back up...';

    public function __destruct()
    {
        $cwd = getcwd();
        `rm $cwd/down`;
        parent::__destruct();
    }
}

...在你的前端控制器中(在这个例子中,简单地是index.php

<?php

if (file_exists('/path/to/down')) {
    die("Application is down for maintainance.");
}
// ...other code...

在迁移之前备份您的数据库

如果您对实际副本进行了测试并且它工作正常,这通常不是必需的,但毕竟安全比后悔好。您可能会在迁移过程中遇到停电!

此外,脚本运行正确并不一定意味着它做了您想做的事情。迁移后始终验证您的数据。

使用上一节中的UpDown自定义插件,您可以自动处理此操作。使用Loader的getErrors()方法查看是否需要回滚,或者您可以直接删除备份(或只是以防万一手动检查时出现任何可疑情况)。

贡献

SQLite支持计划在不久的将来实现,但它不是我的优先事项(客户偶尔会使用它,但它真的不是非常适合Web开发的数据库)。

MSSQL和Oracle是有效的选择,但我们无法访问它们。如果您可以并愿意移植DbMover的数据库特定部分,请随意创建仓库分支并发送给我们拉取请求!

没有正式的风格指南,但请查看现有代码,并尽量使您的编码风格与之保持一致。如果您正在处理我无法/不会支持的分销商,请确保您也为这些添加单元测试。

贡献也可以是错误报告的形式(请针对受影响的包提交!)和功能请求(分销商支持绝非详尽无遗,只是最常用的选项)。

插件包有时也包含在它们的README.md中的TODO列表。如果您的请求已经列在那里,就没有必要报告它,因为它已经在路线图中了。

运行单元测试,请执行vendor/bin/toast。测试需要一个名为dbmover_test的空MySQL数据库,用户名为dbmover_test,密码为moveit

调试和开发

使用--dry-run标志运行dbmover,仅组装要执行的操作列表,实际上不进行任何更改。