跨大版本升级需拆分结构与数据迁移:先用mysqldump+手动重写sql完成结构迁移,再通过db::raw()字段映射执行数据迁移,否则易致字段丢失、时间错乱、主键冲突或乱码。

跨大版本升级(如 TP5.x → TP6.x 或 TP3.2 → TP6.x)时,数据库迁移不是“执行 php think migrate:run”就能完事的——它必须拆成「结构迁移」和「数据迁移」两件事分别处理,否则极易出现字段丢失、时间格式错乱、主键冲突或中文乱码。
为什么不能直接用 migrate 命令同步旧库结构
ThinkPHP 自身不内置迁移能力,topthink/think-migration 是第三方扩展,只负责从零建表或增量改结构,**不会读取已有数据库反向生成迁移文件**。你手头那个 TP5.x 项目跑着三年的 users 表,字段含 create_time(int)、status(tinyint)、无软删除标记——这些历史设计不会自动映射到 TP6 的 created_at(datetime)、is_active(boolean)、deleted_at(datetime)新约定。
常见错误现象:php think migrate:run 执行后报 SQLSTATE[42S01]: Base table or view already exists,或者新表建出来了但老数据全丢了。
- TP5.x 的模型默认用
createTime/updateTime字段名 + 时间戳整型,TP6 要求created_at/updated_at+ datetime 类型 - TP5.x 的
softDelete是手动加字段 + 查询过滤,TP6 的use SoftDelete会强依赖deleted_at字段存在且为 NULLable datetime - TP5.x 的
validate规则写在模型属性里,TP6 禁用闭包规则,且验证失败返回格式从 array 变成 ValidateException
先做结构迁移:用 mysqldump + 手动重写 SQL
别指望自动生成。你得把老库结构导出来,按 TP6 规范重写建表语句,再封装进迁移文件。
实操建议:
- 在旧环境执行:
mysqldump --no-create-info --skip-triggers --compact your_db users > users_old.sql,只导数据,不导建表语句 - 用
SHOW CREATE TABLE users拿到原始 DDL,人工改成 TP6 兼容版:把create_time int(10)改成created_at datetime DEFAULT CURRENT_TIMESTAMP,补上deleted_at datetime NULL,索引名统一用小写(KEY idx_email→INDEX idx_email) - 用
php think migrate:create UsersTable生成空文件,删掉change(),在up()里用$this->execute()直接执行重写后的建表 SQL;down()里写DROP TABLE IF EXISTS users - 如果表有外键,务必在
up()中按依赖顺序建表(先建roles,再建users),否则报错Can't create table
再做数据迁移:用 Db::raw() + 手动字段映射
TP6 的 Db 查询返回对象,不能直接 insertAll() 老数组;且 created_at 需要转换时间戳,status 要映射成布尔值。
示例(写在命令行类或临时控制器里):
$oldUsers = Db::connect('old')->name('users')->select();
$newUsers = [];
foreach ($oldUsers as $u) {
$newUsers[] = [
'username' => $u['user_name'],
'email' => $u['email'],
'password' => $u['password'],
'status' => (bool) $u['status'], // tinyint → boolean
'created_at' => date('Y-m-d H:i:s', $u['create_time']),
'updated_at' => date('Y-m-d H:i:s', $u['update_time']),
'deleted_at' => $u['delete_time'] ? date('Y-m-d H:i:s', $u['delete_time']) : null,
];
}
Db::name('users')->insertAll($newUsers);
- 别用模型批量插入(
UserModel::insertAll()),TP6 模型在迁移阶段可能未加载验证器或事件监听,导致静默截断字段 - 大表(>10w 行)务必分批:用
limit(1000)->page($i)控制每次处理量,避免内存溢出 - 中文字段若出现乱码,检查连接字符集:在
config/database.php的default连接中加'charset' => 'utf8mb4',并确认 MySQL 服务端已启用innodb_large_prefix=ON
上线前必须验证的三个硬点
迁移完成不等于可用。以下三点漏一个,上线后就可能丢订单、登不上后台、日志全错时区。
-
runtime/目录必须清空:缓存里存着 TP5 的路由规则、配置快照、模板编译结果,不清就混用,轻则 404,重则路由指向错误控制器 -
.env文件里的DB_DATABASE和DB_PREFIX必须和新迁移后的表名一致;若旧库用了前缀tp5_,新表建的是users,就得把DB_PREFIX改为空或同步改表名 - 所有模型类的
protected $table属性要显式声明,TP6 不再自动推导表名;若旧模型写了protected $tableName = 'tp5_users',得改成protected $table = 'users'并删掉$tableName
最易被忽略的是时间字段类型变更带来的 ORM 行为差异:TP5 的 create_time 是 int,$user->create_time 返回数字;TP6 的 created_at 是 datetime,$user->created_at 返回 Carbon 实例——模板里直接 echo 会输出完整日期字符串,而非时间戳,前端 JS 处理可能炸掉。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











