真正的数据库迁移必须具备版本控制、可回滚性与环境一致性保障;仅靠手动执行sql文件无法满足,因其缺乏统一调度、事务封装、逆序执行校验及执行状态记录等核心机制。

为什么直接写 SQL 文件 + 手动执行不是真正的迁移
因为缺少版本控制、回滚能力、环境一致性保障。PHP 项目里常见做法是把 up() 和 down() 封装成类,但若不统一调度,就会出现 php migrate.php up 2023_01_01_add_users_table 这种硬编码路径调用——一旦文件名拼错或顺序乱了,down() 就可能跳过某些步骤,数据库状态和代码实际不一致。
命令模式的核心:把每个迁移封装成可执行对象
不是让 Migration 类自己决定“该做什么”,而是让它只定义“能做什么”,由统一的 CommandInvoker 按顺序调用 execute()。这样迁移逻辑和执行流程解耦,也方便加日志、事务、dry-run 支持。
关键点:
-
MigrationInterface必须声明up(Connection $conn)和down(Connection $conn),不能只写run()—— 否则无法安全回滚 - 每个迁移类名需带时间前缀(如
class M20230101120000_AddUsersTable implements MigrationInterface),便于按字典序排序 - 不要在
up()里写CREATE TABLE IF NOT EXISTS—— 迁移应假设环境干净,冲突应暴露为异常,而不是静默忽略
如何避免 migrate:rollback 时破坏依赖关系
典型错误是把 down() 当作“删表”,结果先执行了删除外键表的迁移,再执行删除主键表的迁移,触发 SQLSTATE[HY000]: General error: 1025 Error on rename。
正确做法:
- 所有
down()必须严格逆序于up(),且迁移类本身不负责判断是否“该删”,只做确定性操作 - 记录已执行迁移的表(如
migration_log)必须包含version(时间戳字符串)、name(类名)、executed_at、status(success/failed)字段,不能只存文件名 - 执行
rollback前,先查migration_log中最近一条成功记录,然后按ORDER BY version DESC取出对应类,调用其down()—— 不依赖文件系统顺序
CLI 入口怎么做到既灵活又不绕弯子
别用 symfony/console 大包起步,小项目直接用 $_SERVER['argv'] 解析更可控。重点在于参数校验和错误反馈是否直给。
示例入口逻辑片段:
if (!isset($argv[1])) {
die("Usage: php migrate.php [up|down|status] [version?]\n");
}
$command = $argv[1];
if ($command === 'up' && !isset($argv[2])) {
die("migrate.php up requires a version, e.g. '20230101120000'\n");
}
// 后续根据 $command 分发到 CommandInvoker::run()
注意:version 参数必须是完整类名前缀(如 20230101120000),不能接受模糊匹配(如 add_users),否则会掩盖命名不规范问题。
真正容易被忽略的是:所有数据库操作必须包裹在事务中,但 SQLite 不支持 DDL 事务,所以 down() 对 SQLite 要提前检查并报错提示,而不是静默失败。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











