monomelodies / dbmover
基于PHP的数据库版本管理工具
Requires
- dariuszp/cli-progress-bar: ^1.0
- monomelodies/kingconf: ^1.0
Requires (Dev)
- monomelodies/gentry: ^0.8
This package is auto-updated.
Last update: 2024-09-09 20:46:09 UTC
README
一个基于PHP的数据库版本管理工具。
安装
Composer(推荐使用)
composer require monomelodies/dbmover
手动安装
- 下载或克隆仓库;
- 根目录下有一个可执行的
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 IF
、ELSIF
)的确切语法也取决于供应商。确定是否需要重命名表的确切方式也取决于供应商(尽管在当前版本中,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 将尽可能重新排序语句。特别是
- 所有以
IF
开头的语句都会被提升。 - 所有
ALTER TABLE
语句也会被提升。 - 上述两个语句将单独运行。
- 索引和外键约束将被删除。
- 所有
CREATE TABLE
语句都会被提升并分析。- 如果需要创建表,则直接发出该语句。
- 如果需要更新表,则发出所需的更新语句。
- 删除现有的视图、例程和触发器。
- 运行所有其他语句(例如
CREATE PROCEDURE
、CREATE VIEW
等)。 - 重新运行步骤 3。请注意,
ALTER TABLE
语句将静默失败,并且预计某些或所有原本为false
的条件现在将评估为true
,反之亦然。 - 尝试删除在模式中不存在或不存在的表。
将数据从一个表转移到另一个表
这有时是必要的。在这些情况下,您应使用 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... );
也可以使用简写的SERIAL
或BIGSERIAL
类型来自动化此过程,但它们与DbMover的分析和表修改语句不兼容。
贡献
SQLite支持计划在不久的将来实现,但在我优先级列表中并不是非常高(客户偶尔会使用它,但它并不是非常适合Web开发的数据库)。
MSSQL和Oracle是有效的选择,但我们无法访问它们。如果你可以并且想移植dbMover的特定于数据库的部分,请随意分叉存储库并向我们发送拉取请求!
没有正式的风格指南,但请查看现有代码,并请尽量保持你的编码风格与之一致。如果你正在处理我不能/不愿意支持的供应商,请确保也为它们添加单元测试。
0.4版本的一个待办事项是,如果某些语句在表迁移前后都产生了错误,则发出警告,因为这意味着它们可能没有按照你的意图执行。