monomelodies/dbmover

基于PHP的数据库版本管理工具

0.3.14 2016-06-09 09:46 UTC

README

一个基于PHP的数据库版本管理工具。

安装

Composer(推荐使用)

composer require monomelodies/dbmover

手动安装

  1. 下载或克隆仓库;
  2. 根目录下有一个可执行的dbmover文件。

供应商支持

dbMover目前支持MySQL和PostgreSQL数据库引擎。计划在不久的将来支持SQLite。如果您有权访问MSSQL或Oracle(或另一个数据库)并愿意贡献,我们非常欢迎!请参阅本说明书的末尾。

设计目标

Web应用程序通常与SQL数据库一起工作。程序员会在一个“模式文件”中布置这样的数据库,这本质上是SQL语句。问题在于,在开发过程中或应用程序的生命周期中,需要更改此模式时,这种情况会出现。这涉及到手动将更改应用到所有开发人员的测试数据库中,可能是一个预发布数据库以及最终的生产数据库(们)。

这样做既繁琐又容易出错。记住为每个更改编写迁移也是繁琐的,跟踪哪些迁移已经应用(或尚未应用)也是容易出错的(实际应用案例:导入特定数据库的旧版本以解决特定问题,而迁移“登记册”本身已经过时)。

dbMover通过仅查看中央、领先、受版本控制的模式文件来自动化这项任务,并应用所需的任何更改。

用法

在项目的根目录中放置一个dbmover.json文件。该文件将包含dbMover的设置,如连接、数据库名称和模式文件的位置。

其格式如下

{
    "dsn": {
        "user": "yourUserName",
        "password": "something secret",
        "schema": ["path/to/schema/file.sql"],
        "ignore": ["/regex/"]
    }
}

"dsn"的内容略带特定于驱动程序,但通常类似于"engine:host=host,dbname=name,port=1234",其中有一个或多个是可选的(默认为引擎默认值)。这是PHP的PDO构造函数期望的完全相同的字符串。

模式文件的文件名必须相对于您运行的目录(推荐,因为通常您希望将它们与项目代码一起放在版本控制中)或绝对。

最佳实践:将配置文件排除在源代码控制之外,例如通过将其添加到.gitignore中。数据库连接凭据很少(如果有的话)会更改,因此设置应该主要是手动操作一次。这样,您的开发数据库可以使用一个可丢弃的密码,而生产数据库可以使用一个更强的密码,使用不同的主机(如localhost)等。

可选的"ignore"条目可以包含一个正则表达式对象的数组,在迁移过程中忽略这些对象(例如,当您的应用程序自动创建用于缓存数据的表时)。正则表达式被直接注入到PHP的preg_match中,因此也应包含分隔符,并可以可选地指定模式修饰符,如"/i"。它们会检查所有对象(表、视图、过程等)。

定义配置文件后,从同一位置运行可执行文件

vendor/bin/dbmover

就这样——您的数据库现在应该与您的模式(们)同步。

添加表

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

添加列

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

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

如果您的供应商支持此功能,并且您确实需要将列添加到特定位置,请使用以下描述的IF块。

修改列

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

删除列

只需从模式中删除它们并重新运行即可。

删除表

只需从模式中删除它们并重新运行即可。

外键约束和索引

根据您的数据库供应商,您可能在创建表时指定这些约束。这意味着如果表已经存在,dbMover永远不会看到它们!因此请不要这样做。相反,在创建表后使用ALTER TABLE语句创建这些约束。例外情况是在AUTOINCREMENT/SERIAL字段上的主键,因为如果没有约束,表创建将失败。然而,这些列很少改变,即使它们改变了,您也可以使用带有正确检查预/后状态的IF块手动应用更改。

MySQL中的示例

-- This is *wrong*:
CREATE TABLE foo (
    id INTEGER NOT NULL PRIMARY KEY AUTO_INCREMENT,
    bar INTEGER,
    INDEX (bar),
    CONSTRAINT FOREIGN KEY (bar) REFERENCES buzz(id)
) ENGINE='InnoDB';

-- Write it like this instead:
CREATE TABLE foo (
    id INTEGER NOT NULL PRIMARY KEY AUTO_INCREMENT,
    bar INTEGER,
) ENGINE='InnoDB';
ALTER TABLE foo ADD INDEX (bar),
    ADD CONSTRAINT FOREIGN KEY (bar) REFERENCES buzz(id);

在PostgreSQL中

-- This is *wrong*:
CREATE TABLE foo (
    id SERIAL PRIMARY KEY,
    bar INTEGER REFERENCES buzz(id)
);
CREATE INDEX foo_bar_idx ON foo(bar);

-- Write it like this instead:
CREATE TABLE foo (
    id SERIAL PRIMARY KEY,
    bar INTEGER,
);
CREATE INDEX foo_bar_idx ON foo(bar);
ALTER TABLE foo ADD CONSTRAINT FOREIGN KEY (bar) REFERENCES buzz(id);

原因是在迁移期间,dbMover会DROP所有现有的约束,并在迁移过程中重新创建它们,以确保数据库上仅存在您模式中意图存在的约束。这是实现此目的的最可靠方法,而不是分析您的SQL语句、考虑供应商特定的语法等等。

对于非常大的表,重新创建索引可能会显著减慢迁移过程。这就是生活。

松散的ALTER语句

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

dbMover将按照模式文件中指定的顺序单独运行每个ALTER TABLE语句,所以只需在逻辑上添加外键即可。如果列不存在或类型不正确(需要迁移),则语句将静默失败,或者最终会成功。

更复杂的模式更改

某些事情自动确定比较困难,例如表或列重命名。您应该使用当迁移需要执行时通过的条件将此类更改包裹在IF块中,否则将失败。

根据您的数据库供应商,可能需要将这些包裹在“废弃”过程(例如MySQL仅支持过程内的IF)中。例如,dbMover的供应商特定类会为您处理此问题。废弃过程以tmp_前缀。

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

插入默认数据

为了防止重复插入,这些应该被包裹在如下的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 将尽可能重新排序语句。特别是

  1. 所有以 IF 开头的语句都会被提升。
  2. 所有 ALTER TABLE 语句也会被提升。
  3. 上述两个语句将单独运行。
  4. 索引和外键约束将被删除。
  5. 所有 CREATE TABLE 语句都会被提升并分析。
    1. 如果需要创建表,则直接发出该语句。
    2. 如果需要更新表,则发出所需的更新语句。
  6. 删除现有的视图、例程和触发器。
  7. 运行所有其他语句(例如 CREATE PROCEDURECREATE VIEW 等)。
  8. 重新运行步骤 3。请注意,ALTER TABLE 语句将静默失败,并且预计某些或所有原本为 false 的条件现在将评估为 true,反之亦然。
  9. 尝试删除在模式中不存在或不存在的表。

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

这有时是必要的。在这些情况下,您应使用 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 忽略这些。假设修改这些是风险很高且非常罕见操作,您希望手动进行和/或更密切地监控。

首先测试您的模式

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

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

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

您的应用程序如何处理其 down 状态不由 dbMover 决定。一种简单的方法是将 dbMover 调用包装在您自己的脚本中,例如

touch down
vendor/bin/dbmover
rm down

...

<?php

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

这当然是一个非常简单的示例,但应该能指明正确的方向。

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

如果您已经对实际副本进行了测试并且它运行正常,这通常是不必要的,但最好是小心为上。您可能在迁移过程中遇到电力中断!

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

关于 PostgreSQL 的说明

PostgreSQL的INFORMATION_SCHEMA别名包含比你在模式文件中定义的更多数据,尤其是在程序(它的本地函数也在这里公开)方面。由于这些本地函数通常属于postgresql用户,dbMover会尝试删除它们,但会静默失败。因此,始终以实际数据库用户身份运行dbMover,而不是以主用户身份(这也适用于MySQL,尽管上述问题在那里不适用)。

“主”或“root”用户拥有普通用户没有的权限,所以理论上你可以在模式文件中包含类似DROP unrelated_database;的操作。当然,你只能怪自己,但最好避免这种风险。

PostgreSQL中的序列

PostgreSQL将所谓的“序列”中的“auto_increment”唯一ID存储起来(在所有意图和目的上,它是一个“特殊”的单独表)。为了使迁移正确运行,你应该按照以下方式编写这些

CREATE SEQUENCE IF NOT EXISTS mytable_id_seq;
CREATE TABLE mytable (
    id BIGINT NOT NULL PRIMARY KEY DEFAULT NEXTVAL('auth_id_seq'::regclass),
    -- ...other table info...
);

也可以使用简写的SERIALBIGSERIAL类型来自动化此过程,但它们与DbMover的分析和表修改语句不兼容。

贡献

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

MSSQL和Oracle是有效的选择,但我们无法访问它们。如果你可以并且想移植dbMover的特定于数据库的部分,请随意分叉存储库并向我们发送拉取请求!

没有正式的风格指南,但请查看现有代码,并请尽量保持你的编码风格与之一致。如果你正在处理我不能/不愿意支持的供应商,请确保也为它们添加单元测试。

0.4版本的一个待办事项是,如果某些语句在表迁移前后都产生了错误,则发出警告,因为这意味着它们可能没有按照你的意图执行。