byjg / migration
PHP数据库版本控制简单库。支持Sqlite、MySql、Sql Server和Postgres。
Requires
- ext-pdo: *
- byjg/anydataset-db: 4.9.*
Requires (Dev)
- phpunit/phpunit: 5.7.*|7.4.*|^9.6
README
功能
这是一个用于数据库版本控制的PHP简单库。目前支持Sqlite、MySql、Sql Server和Postgres。
数据库迁移可以用作
- 命令行界面
- PHP库,可集成到您的功能测试中
- 无论您的编程语言或框架如何,都可以集成到您的CI/CD中。
数据库迁移仅使用SQL命令来对数据库进行版本控制。
为什么使用纯SQL命令?
大多数框架倾向于使用编程语句来对数据库进行版本控制,而不是使用纯SQL。
使用框架的本地编程语言来维护数据库有一些优点
- 框架命令有一些用于执行复杂任务的技巧代码;
- 您可以一次性编码并将代码部署到不同的数据库系统中;
- 还有其他
但是,尽管这些功能很好,在大项目中,有人最终会使用MySQL Workbench来更改数据库,然后花费数小时将代码翻译成PHP。那么,为什么不用MySQL Workbench、JetBrains DataGrip等现有的功能,这些功能提供了更新数据库所需的SQL命令,并将其直接放入数据库版本控制系统中呢?
因为这个项目是中立的(独立于框架和编程语言),并使用纯SQL命令来迁移数据库。
安装
PHP库
如果您只想在项目中使用PHP库
composer require "byjg/migration"
命令行界面
命令行界面是独立的,不需要您与项目一起安装。
您可以全局安装并创建一个符号链接
composer require "byjg/migration-cli"
请访问 byjg/migration-cli 以获取有关Migration CLI的更多信息。
支持的数据库
它是如何工作的?
数据库迁移使用纯SQL来管理数据库版本。为了使其工作,您需要
- 创建SQL脚本
- 使用命令行或API进行管理。
SQL脚本
脚本分为三组
- BASE脚本包含创建全新数据库的所有SQL命令;
- UP脚本包含所有将数据库版本升级的SQL迁移命令;
- DOWN脚本包含所有将数据库版本降级或回滚的SQL迁移命令;
脚本目录是
<root dir>
|
+-- base.sql
|
+-- /migrations
|
+-- /up
|
+-- 00001.sql
+-- 00002.sql
+-- /down
|
+-- 00000.sql
+-- 00001.sql
- "base.sql" 是基础脚本
- "up" 文件夹包含将版本升级的脚本。例如:00002.sql 是将数据库从版本 '1' 升级到 '2' 的脚本。
- "down" 文件夹包含将版本降级的脚本。例如:00001.sql 是将数据库从版本 '2' 降级到 '1' 的脚本。 "down" 文件夹是可选的。
多开发环境
如果您与多个开发人员和多个分支一起工作,那么确定下一个数字就非常困难。
在这种情况下,版本号后面应添加 "-dev" 后缀。
请看以下场景
- 开发人员1创建了一个分支,最新的版本号是例如42。
- 开发人员2同时创建了一个分支,并且具有相同的数据库版本号。
在这种情况下,两个开发人员都会创建一个名为 43-dev.sql 的文件。两个开发人员都可以毫无问题地迁移UP和DOWN,并且您的本地版本将是43。
但是开发者1合并了您的更改并创建了最终版本43.sql(git mv 43-dev.sql 43.sql
)。如果开发者2更新您的本地分支,他将会有一个文件43.sql(来自开发者1)和您的文件43-dev.sql。如果他在尝试迁移UP或DOWN时,迁移脚本将会失败并提醒他存在两个版本43。在这种情况下,开发者2将不得不更新您的文件到44-dev.sql并继续工作,直到合并您的更改并生成最终版本。
使用PHP API并将其集成到您的项目中
基本用法是
- 创建一个连接,一个ConnectionManagement对象。更多信息请参阅“byjg/anydataset”组件
- 使用此连接以及存放sql脚本的文件夹创建一个Migration对象。
- 使用适当的命令“重置”、“UP”或“DOWN”迁移脚本。
请看一个例子
<?php // Create the Connection URI // See more: https://github.com/byjg/anydataset#connection-based-on-uri $connectionUri = new \ByJG\Util\Uri('mysql://migrateuser:migratepwd@localhost/migratedatabase'); // Register the Database or Databases can handle that URI: \ByJG\DbMigration\Migration::registerDatabase(\ByJG\DbMigration\Database\MySqlDatabase::class); // Create the Migration instance $migration = new \ByJG\DbMigration\Migration($connectionUri, '.'); // Add a callback progress function to receive info from the execution $migration->addCallbackProgress(function ($action, $currentVersion, $fileInfo) { echo "$action, $currentVersion, ${fileInfo['description']}\n"; }); // Restore the database using the "base.sql" script // and run ALL existing scripts for up the database version to the latest version $migration->reset(); // Run ALL existing scripts for up or down the database version // from the current version until the $version number; // If the version number is not specified migrate until the last database version $migration->update($version = null);
Migration对象控制数据库版本。
在您的项目中创建版本控制
<?php // Register the Database or Databases can handle that URI: \ByJG\DbMigration\Migration::registerDatabase(\ByJG\DbMigration\Database\MySqlDatabase::class); // Create the Migration instance $migration = new \ByJG\DbMigration\Migration($connectionUri, '.'); // This command will create the version table in your database $migration->createVersion();
获取当前版本
<?php $migration->getCurrentVersion();
添加回调以控制进度
<?php $migration->addCallbackProgress(function ($command, $version, $fileInfo) { echo "Doing Command: $command at version $version - ${fileInfo['description']}, ${fileInfo['exists']}, ${fileInfo['file']}, ${fileInfo['checksum']}\n"; });
获取Db Driver实例
<?php $migration->getDbDriver();
要使用它,请访问:https://github.com/byjg/anydataset-db
避免部分迁移(MySQL不支持)
部分迁移是指迁移脚本在过程中由于错误或手动中断而中断。
迁移表将处于部分UP
或部分DOWN
状态,需要手动修复后才能再次迁移。
为了避免这种情况,您可以指定迁移将在事务性上下文中运行。如果迁移脚本失败,事务将回滚,迁移表将标记为完成
,版本将是导致错误的脚本之前的立即前一个版本。
要启用此功能,您需要调用方法withTransactionEnabled
并将true
作为参数传递
<?php $migration->withTransactionEnabled(true);
注意:此功能在MySQL中不可用,因为它不支持事务中的DDL命令。如果您使用此方法与MySQL,迁移将静默忽略它。更多信息:https://dev.mysqlserver.cn/doc/refman/8.0/en/cannot-roll-back.html
Postgres SQL迁移的技巧
关于创建触发器和SQL函数
-- DO CREATE FUNCTION emp_stamp() RETURNS trigger AS $emp_stamp$ BEGIN -- Check that empname and salary are given IF NEW.empname IS NULL THEN RAISE EXCEPTION 'empname cannot be null'; -- it doesn't matter if these comments are blank or not END IF; -- IF NEW.salary IS NULL THEN RAISE EXCEPTION '% cannot have null salary', NEW.empname; -- END IF; -- -- Who works for us when they must pay for it? IF NEW.salary < 0 THEN RAISE EXCEPTION '% cannot have a negative salary', NEW.empname; -- END IF; -- -- Remember who changed the payroll when NEW.last_date := current_timestamp; -- NEW.last_user := current_user; -- RETURN NEW; -- END; -- $emp_stamp$ LANGUAGE plpgsql; -- DON'T CREATE FUNCTION emp_stamp() RETURNS trigger AS $emp_stamp$ BEGIN -- Check that empname and salary are given IF NEW.empname IS NULL THEN RAISE EXCEPTION 'empname cannot be null'; END IF; IF NEW.salary IS NULL THEN RAISE EXCEPTION '% cannot have null salary', NEW.empname; END IF; -- Who works for us when they must pay for it? IF NEW.salary < 0 THEN RAISE EXCEPTION '% cannot have a negative salary', NEW.empname; END IF; -- Remember who changed the payroll when NEW.last_date := current_timestamp; NEW.last_user := current_user; RETURN NEW; END; $emp_stamp$ LANGUAGE plpgsql;
由于PDO数据库抽象层无法运行SQL语句的批处理,当byjg/migration读取迁移文件时,必须将整个SQL文件的全部内容在分号处分割,并逐条执行语句。然而,有一种语句可以在其主体中包含多个分号:函数。
为了能够正确解析函数,byjg/migration 2.1.0开始根据分号 + EOL
序列分割迁移文件,而不是仅仅分号。这样,如果您在每个函数定义的内部分号后添加一个空注释,byjg/migration将能够解析它。
不幸的是,如果您忘记添加任何这些注释,库将把CREATE FUNCTION
语句分割成多个部分,迁移将失败。
避免冒号字符(:
)
-- DO CREATE TABLE bookings ( booking_id UUID PRIMARY KEY, booked_at TIMESTAMPTZ NOT NULL CHECK (CAST(booked_at AS DATE) <= check_in), check_in DATE NOT NULL ); -- DON'T CREATE TABLE bookings ( booking_id UUID PRIMARY KEY, booked_at TIMESTAMPTZ NOT NULL CHECK (booked_at::DATE <= check_in), check_in DATE NOT NULL );
由于PDO使用冒号字符作为预处理语句中命名参数的前缀,其在其他上下文中的使用将导致它出错。
例如,PostgreSQL语句可以使用::
在类型之间进行值转换。另一方面,PDO将读取这作为在无效上下文中的无效命名参数,并在尝试运行时失败。
唯一修复这种不一致的方法是完全避免冒号(在这种情况下,PostgreSQL还有另一种语法:CAST(value AS type)
)。
使用SQL编辑器
最后,编写手动SQL迁移可能很繁琐,但使用能够理解SQL语法、提供自动完成、检查当前数据库架构或自动格式化代码的编辑器会容易得多。
处理一个模式内部的多种迁移
如果您需要在同一模式内创建不同的迁移脚本和版本,虽然技术上可行,但风险极高,我 强烈不建议 使用此方法。
为此,您需要通过传递参数给构造函数来创建不同的“迁移表”。
<?php $migration = new \ByJG\DbMigration\Migration("db:/uri", "/path", true, "NEW_MIGRATION_TABLE_NAME");
出于安全考虑,此功能在命令行不可用,但您可以使用环境变量 MIGRATION_VERSION
来存储名称。
我们真心建议不要使用此功能。建议每个模式使用一个迁移。
运行单元测试
基本单元测试可以通过以下方式运行
vendor/bin/phpunit
运行数据库测试
运行集成测试需要确保数据库已启动并运行。我们提供了一个基本的 docker-compose.yml
文件,您可以使用它来启动数据库进行测试。
运行数据库
docker-compose up -d postgres mysql mssql
运行测试
vendor/bin/phpunit vendor/bin/phpunit tests/SqliteDatabase* vendor/bin/phpunit tests/MysqlDatabase* vendor/bin/phpunit tests/PostgresDatabase* vendor/bin/phpunit tests/SqlServerDblibDatabase* vendor/bin/phpunit tests/SqlServerSqlsrvDatabase*
可选地,您可以设置单元测试使用的服务器和密码
export MYSQL_TEST_HOST=localhost # defaults to localhost export MYSQL_PASSWORD=newpassword # use '.' if want have a null password export PSQL_TEST_HOST=localhost # defaults to localhost export PSQL_PASSWORD=newpassword # use '.' if want have a null password export MSSQL_TEST_HOST=localhost # defaults to localhost export MSSQL_PASSWORD=Pa55word export SQLITE_TEST_HOST=/tmp/test.db # defaults to /tmp/test.db