数据库迁移
加表 / 加列 / 建索引不要让插件代码自己建表,也不要手工执行 SQL。 把 .sql 放进插件目录的 migrations/,打包时自动进包,安装/升级时自动执行。
plugins/ruoyi-<name>/migrations/01-create-<name>-tables.sql # 文件名排序即执行顺序为什么表结构必须随应用包走
图床就是被这件事打回来的:它原本是宿主编译期模块,5 张表建在平台初始化 SQL 里。 改成插件应用之后,客户那台只导入平台 SQL 的部署装上图床会所有页面报表不存在, 而且卸载时平台也无从知道该删哪些表。
一句话:建表脚本有两个用途 —— 安装时建表,卸载时被平台读出来当删表清单。
执行链路
| 环节 | 谁做 | 说明 |
|---|---|---|
| 打包 | package.ps1 | 默认取插件目录的 migrations/*.sql;-MigrationPath 可指定 |
| 落位 | 落位服务 | 解到 <pluginDir>/migrations/<appCode>/(不清理,历史脚本留着可审计) |
| 执行 | 迁移服务 | 扫描该目录 → 按文件名排序 → 逐个执行 |
| 记账 | app_schema_history | 键 (app_code, script),含 checksum / success / execution_ms |
| 删表依据 | 表名解析 | 从脚本里解析 CREATE TABLE 的表名,供"彻底删除"用 |
四条硬规矩
1. 只做 up,不回滚
app.json 的 migrations(含 down)是预留字段,当前不参与执行。 要回退表结构,请自己写一个"反向"的新迁移脚本。
2. 脚本必须幂等
MySQL 的 DDL 是隐式提交的,没法整体回滚。重试时已成功的会跳过, 失败的那个必须能重跑。
CREATE TABLE IF NOT EXISTS `gallery_image` ( ... );MySQL 8 没有 ADD COLUMN IF NOT EXISTS
加列前要用 information_schema 判断,再动态执行。
3. 已执行过的脚本内容不能改
同一 script 名再来时比对 checksum,不一致直接报错拒绝执行:
迁移脚本 <name> 的内容已被修改(摘要与首次执行时不一致),
为避免同一脚本产生两种结果,拒绝执行。请改用新的脚本名。要变更就新增一个脚本。这是刻意的保护,不要绕过它。
摘要按字节算,所以换行差异要单独容忍
工作区是 CRLF、Linux 上是 LF,语义相同但摘要不同。
平台的比对同时认 LF / CRLF 两种摘要(记账统一存 LF 归一化摘要),真正的改动照旧拒绝。
不要给 plugins/*/migrations/*.sql 加 eol 归一化 —— 本目录的换行本来就是混合的(有的应用 LF、有的 CRLF),"统一"反而会让所有存量记录失效一次。
4. 去重判据不带租户
表结构在所有租户间共享,带上租户会让第二个租户重复执行 DDL。 读写账本一律忽略租户上下文;tenant_id 只记"谁最先应用"。
账本唯一键必须正好是 (app_code, script)
曾经 DDL 写的是 (tenant_id, app_code, version),后果是 一个版本里只要有第二个迁移脚本就装不上。详见 数据模型。
为什么是"目录驱动"而不是读清单
执行方式是扫描目录,而不是解析 app.json 的 migrations 字段。这样:
- 「清单声明了脚本却忘了打包」这类错误根本不会发生;
- 手工补一个修数据脚本,扔进目录即可,不必重新打包上架。
顺序保证
落位 → 变更表结构 → 切换代码这是刻意做出来的(把"落位"与"加载"拆成两步)。 所以插件在启动时机直接读新加的列是安全的。
改过已执行脚本怎么办
改了 01-*.sql(例如把里面的示例数据拆出去)之后,存量部署会因摘要不一致被挡住。 这是刻意的保护,正确做法是:
- 先跑对应的修复脚本(删掉那几行账本记录);
- 再升级(或重新部署)该应用 —— 迁移脚本只在 install / upgrade 里跑。
光把包传上去不会有任何变化
迁移只在 install / upgrade 触发。存量部署拿到新种子数据的唯一路径就是 「先跑修复脚本 → 再升级」。