迁移
您项目的迁移是在跟踪项目中涉及的所有数据库更改。 byjg/php-migration 是一个非常有用的核心库,可以帮助您入门。
安装
PHP 库
如果您只想在项目中使用 PHP 库:
composer require "byjg/migration"
命令行界面
命令行界面是独立的,不需要您与项目一起安装。
您可以全局安装并创建符号链接
composer require "byjg/migration-cli"
请访问 byjg/migration-cli 以获取有关迁移 CLI 的更多信息。
支持的数据库
数据库 | 驱动人 | 连接字符串 |
---|---|---|
Sqlite | pdo_sqlite | sqlite:///path/to/file |
MySql/MariaDb | pdo_mysql | mysql://username:password@hostname:port/database |
Postgres | pdo_pgsql | pgsql://username:password@hostname:port/database |
Sql Server | pdo_dblib, pdo_sysbase Linux | dblib://username:password@hostname:port/database |
Sql Server | pdo_sqlsrv Windows | sqlsrv://username:password@hostname:port/database |
它是如何工作的?
数据库迁移使用纯 SQL 来管理数据库版本。 为了使其正常工作,您需要:
- 创建 SQL 脚本
- 使用命令行或 API 进行管理。
SQL 脚本
脚本分为三组脚本:
- BASE 脚本包含创建新数据库的所有 SQL 命令;
- UP 脚本包含所有数据库版本 "up" 的 SQL 迁移命令;
- DOWN 脚本包含所有数据库版本 "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 对象。
- 使用适当的命令进行 "reset"、"up" 或 "down" 迁移脚本。
查看一个示例:
<?php
// 创建连接 URI
// 了解更多: https://github.com/byjg/anydataset#connection-based-on-uri
$connectionUri = new \ByJG\Util\Uri('mysql://migrateuser:migratepwd@localhost/migratedatabase');
// 注册可处理该 URI 的数据库:
\ByJG\DbMigration\Migration::registerDatabase(\ByJG\DbMigration\Database\MySqlDatabase::class);
// 创建 Migration 实例
$migration = new \ByJG\DbMigration\Migration($connectionUri, '.');
// 添加一个回调进度函数以接收执行信息
$migration->addCallbackProgress(function ($action, $currentVersion, $fileInfo) {
echo "$action, $currentVersion, ${fileInfo['description']}\n";
});
// 使用 "base.sql" 脚本恢复数据库
// 并运行所有现有脚本以将数据库版本提升到最新版本
$migration->reset();
// 运行所有现有脚本以向上或向下迁移数据库版本
// 从当前版本到 $version 编号;
// 如果未指定版本编号,则迁移到最后的数据库版本
$migration->update($version = null);
迁移对象控制数据库版本。
在您的项目中创建版本控制
<?php
// 注册可处理该 URI 的数据库:
\ByJG\DbMigration\Migration::registerDatabase(\ByJG\DbMigration\Database\MySqlDatabase::class);
// 创建 Migration 实例
$migration = new \ByJG\DbMigration\Migration($connectionUri, '.');
// 此命令将在您的数据库中创建版本表
$migration->createVersion();
获取当前版本
<?php
$migration->getCurrentVersion();
添加回调以控制进度
<?php
$migration->addCallbackProgress(function ($command, $version, $fileInfo) {
echo "执行命令:$command 在版本 $version - ${fileInfo['description']}, ${fileInfo['exists']}, ${fileInfo['file']}, ${fileInfo['checksum']}\n";
});
避免部分迁移(不适用于 MySQL)
部分迁移是指因为错误或手动中断而在过程中中断迁移脚本。
迁移表将处于状态 partial up
或 partial down
,需要手动修复才能再次迁移。
为了避免这种情况,您可以指定迁移将在事务上下文中运行。
如果迁移脚本失败,事务将被回滚,迁移表将标记为 complete
,版本将是导致错误的脚本之前的立即前一个版本。
要启用此功能,您需要调用方法 withTransactionEnabled
,并传递 true
作为参数:
<?php
$migration->withTransactionEnabled(true);
注意:此功能在 MySQL 中不可用,因为它不支持事务中的 DDL 命令。 如果您在 MySQL 中使用此方法,迁移将悄默忽略它。 更多信息:https://dev.mysql.com/doc/refman/8.0/en/cannot-roll-back.html
编写 Postgres SQL 迁移的提示
创建触发器和 SQL 函数
-- DO
CREATE FUNCTION emp_stamp() RETURNS trigger AS $emp_stamp$
BEGIN
-- 检查 empname 和薪水是否已给出
IF NEW.empname IS NULL THEN
RAISE EXCEPTION 'empname 不能为空'; -- 这些注释是否为空并不重要
END IF; --
IF NEW.salary IS NULL THEN
RAISE EXCEPTION '% 不能有空薪水', NEW.empname; --
END IF; --
-- 谁为我们工作时必须为此支付?
IF NEW.salary < 0 THEN
RAISE EXCEPTION '% 不能有负薪水', NEW.empname; --
END IF; --
-- 记住谁在何时更改工资单
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
-- 检查 empname 和薪水是否已给出
IF NEW.empname IS NULL THEN
RAISE EXCEPTION 'empname 不能为空';
END IF;
IF NEW.salary IS NULL THEN
RAISE EXCEPTION '% 不能有空薪水', NEW.empname;
END IF;
-- 谁为我们工作时必须为此支付?
IF NEW.salary < 0 THEN
RAISE EXCEPTION '% 不能有负薪水', NEW.empname;
END IF;
-- 记住谁在何时更改工资单
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 # 默认为 localhost
export MYSQL_PASSWORD=newpassword # 如果想有一个空密码,请使用 '.'
export PSQL_TEST_HOST=localhost # 默认为 localhost
export PSQL_PASSWORD=newpassword # 如果想有一个空密码,请使用 '.'
export MSSQL_TEST_HOST=localhost # 默认为 localhost
export MSSQL_PASSWORD=Pa55word
export SQLITE_TEST_HOST=/tmp/test.db # 默认为 /tmp/test.db