NocoBase Migration API 深度解析:插件升级时的数据库结构变更与数据迁移
【免费下载链接】nocobaseNocoBase is an open-source AI + no-code platform for building business systems fast. Instead of generating everything from scratch, AI works on top of production-proven infrastructure and a WYSIWYG no-code interface, so you get both speed and reliability.项目地址: https://gitcode.com/GitHub_Trending/no/nocobase
NocoBase 的 Migration 是@nocobase/server提供的数据迁移基类,用于在应用升级时按版本、按执行时机自动处理数据库结构变更(DDL)和数据订正(DML)。本文基于官方 API 文档与仓库源码,完整讲清 Migration 的on执行时机、appVersion版本控制语义、全部实例属性与方法、三类典型迁移脚本写法,以及nocobase create-migration命令的底层生成逻辑——读完你能够独立编写可运行、可版本控制的迁移脚本,并理解框架在 upgrade 流程中如何调度它们。
Migration 基类与最小示例
Migration 是 NocoBase 的数据迁移基类,用于在插件升级时处理数据库结构变更和数据迁移,从@nocobase/server导入。最小可用示例:
import { Migration } from '@nocobase/server'; export default class extends Migration { on = 'afterLoad'; appVersion = '<1.0.0'; async up() { // 升级逻辑 } }从源码结构看,这一继承链分为两层:
- 底层基类
@nocobase/database导出的Migration(见 packages/core/database/src/migration.ts)持有运行时上下文context: { db, queryInterface, sequelize },并在构造函数中注入,暴露db、sequelize(context.db.sequelize)和queryInterface(context.db.sequelize.getQueryInterface())三个 getter,以及空实现的up()/down()方法; - 应用层基类
@nocobase/server的Migration(见 packages/core/server/src/migration.ts)继承自底层基类,声明了三个类属性默认值:
export class Migration extends DbMigration { appVersion = ''; pluginVersion = ''; on = 'afterLoad'; get app() { return this.context.app as Application; } get pm() { return this.context.app.pm as PluginManager; } get plugin() { return this.context.plugin as Plugin; } }可以看到on的默认值'afterLoad'正是由这个类属性给出的;同时源码中还存在一个文档未展开的pluginVersion = ''属性,用于插件级版本判断。
框架如何发现并实例化迁移文件,可以看 packages/core/server/src/application.ts 中的loadMigrations():它用 glob 扫描指定目录下*.{js,ts}文件(忽略.d.ts),逐个importModule动态导入,然后以new Migration({ app: this, db: this.db, ...context })构造实例,并按m.on || 'afterLoad'分桶到beforeLoad/afterSync/afterLoad三个数组中,migration 的name被赋值为${文件名去扩展名}/${namespace}。这解释了两个细节:on不写时框架不会报错而是落入默认桶;migration 名称由“文件名 + 命名空间”构成,而 core migration 的命名空间为@nocobase/server(由loadCoreMigrations()指定目录src/migrations传入)。
类属性:on 与 appVersion
on:控制执行时机
on: 'beforeLoad' | 'afterSync' | 'afterLoad';on控制 migration 在 upgrade 流程中的执行时机,默认'afterLoad'。
| 值 | 执行时机 | 适用场景 |
|---|---|---|
'beforeLoad' | 插件加载之前 | 底层 DDL 操作(比如添加列、添加约束),此时不能使用 Repository API |
'afterSync' | db.sync()之后、插件 upgrade 之前 | 需要新表结构但不依赖插件逻辑的数据迁移 |
'afterLoad' | 所有插件加载完成之后 | 默认值,大多数 migration 用这个。可以使用完整的 Repository API |
从源码看,三种时机对应loadCoreMigrations()返回的三个 up 钩子:每个桶各自调用this.db.createMigrator({ migrations: migrations[时机] })并await migrator.up(),三者分别挂到应用启动流程的 beforeLoad、afterSync、afterLoad 阶段。换言之,同一次 upgrade 中你的多个 migration 会按on被切分到三条流水线中执行,同桶内再按文件名排序依次执行。
appVersion:版本门控
appVersion: string;appVersion是 semver 范围字符串,决定该 migration 在哪些版本的应用上执行。框架用semver.satisfies()判断:只有当前应用版本满足该范围时,migration 才会执行。
// 只有从低于 1.0.0 的版本升级时才执行 appVersion = '<1.0.0'; // 只有从低于 0.21.0-alpha.13 的版本升级时才执行 appVersion = '<0.21.0-alpha.13'; // 留空则每次 upgrade 都执行 appVersion = '';这个判断在源码中逐字对应(packages/core/server/src/application.ts 的loadMigrations()):
const appVersion = await this.version.get(); // ... if (!m.appVersion || semver.satisfies(appVersion, m.appVersion, { includePrerelease: true })) { m.name = `${filename}/${namespace}`; migrations[m.on || 'afterLoad'].push(m); }两个要点值得注意:
- 留空即“每次执行”——
!m.appVersion为真时直接放行,这也是基类默认值appVersion = ''的语义。由于不符合范围的 migration 在加载阶段就被过滤掉,所以appVersion是“是否执行”的开关,而不是记录“是否已执行”的状态;已执行过但留空的 migration 会在每次 upgrade 重新运行,务必让up()逻辑幂等。 includePrerelease: true——比较时显式开启预发布版本匹配,因此<0.21.0-alpha.13这类带 pre-release 的范围才能正确生效。
实例属性
基类与子类共同提供了访问应用各模块的 getter,全部迁移逻辑都通过它们完成。
app:Application 实例
get app(): ApplicationNocoBase Application 实例,通过它可以访问应用的各个模块:
async up() { // 获取应用版本 const version = this.app.version; // 获取日志 this.app.log.info('Migration started'); }db:Database 实例
get db(): DatabaseNocoBase Database 实例,可以用来获取 Repository、执行查询等:
async up() { const repo = this.db.getRepository('users'); await repo.update({ filter: { status: 'inactive' }, values: { status: 'disabled' }, }); }plugin:当前插件实例
get plugin(): Plugin当前插件实例。仅在插件级 migration 中可用(core migration 中为undefined)。
async up() { const pluginName = this.plugin.name; }源码中该 getter 返回this.context.plugin——只有插件加载流程在构造上下文时注入了plugin,core migration(如@nocobase/server自带的src/migrations/)上下文里没有这个键,因此为undefined。
sequelize:执行原始 SQL
get sequelize(): SequelizeSequelize 实例,可以直接执行原始 SQL:
async up() { await this.sequelize.query(`UPDATE users SET status = 'active' WHERE status IS NULL`); }queryInterface:执行 DDL
get queryInterface(): QueryInterfaceSequelize QueryInterface,用于执行 DDL 操作(添加/删除列、添加约束、修改列类型等):
async up() { const { DataTypes } = require('@nocobase/database'); // 添加列 await this.queryInterface.addColumn('users', 'nickname', { type: DataTypes.STRING, }); // 添加唯一约束 await this.queryInterface.addConstraint('users', { type: 'unique', fields: ['email'], }); }pm:插件管理器
get pm(): PluginManager插件管理器。通过this.pm.repository可以查询和修改插件元数据:
async up() { const plugins = await this.pm.repository.find(); for (const plugin of plugins) { // 批量修改插件记录 } }实例方法:up() 与 down()
up()
async up(): Promise<void>升级时执行。子类必须 override 此方法,编写迁移逻辑。底层基类中up()是空实现,migrator 遍历分桶后的 migration 实例并逐个 await 调用。
down()
async down(): Promise<void>回滚时执行。大多数 migration 留空。如果需要支持回滚,在这里编写反向操作。数据库层的迁移器同样按实例收集down()回调,用于降级场景。
完整示例:三种时机的迁移脚本
以下三个示例覆盖最常用的迁移场景,全部继承原文档并可直接作为插件src/server/migrations/下的参考模板。
示例一:使用 Repository API 更新数据(afterLoad)
最常见的场景——在所有插件加载完成后,用 Repository API 批量更新数据:
import { Migration } from '@nocobase/server'; export default class extends Migration { appVersion = '<1.0.0'; async up() { const repo = this.db.getRepository('roles'); await repo.update({ filter: { $or: [{ allowConfigure: true }, { name: 'root' }], }, values: { snippets: ['ui.*', 'pm', 'pm.*'], allowConfigure: false, }, }); } async down() {} }注意这里没有显式写on,依赖默认值'afterLoad'——与源码中migrations[m.on || 'afterLoad']的行为一致。
示例二:使用 QueryInterface 修改表结构(beforeLoad)
在插件加载之前执行底层 DDL——比如给表添加新列和唯一约束:
import { DataTypes } from '@nocobase/database'; import { Migration } from '@nocobase/server'; export default class extends Migration { on = 'beforeLoad'; appVersion = '<0.14.0-alpha.2'; async up() { const tableName = this.pm.collection.getTableNameWithSchema(); const field = this.pm.collection.getField('packageName'); // 先检查字段是否已存在 const exists = await field.existsInDb(); if (exists) return; await this.queryInterface.addColumn(tableName, field.columnName(), { type: DataTypes.STRING, }); await this.queryInterface.addConstraint(tableName, { type: 'unique', fields: [field.columnName()], }); } }这个示例展示了两点 best practice:通过pm.collection拿到带 schema 的表名与字段元信息,避免硬编码表名;以及迁移前先field.existsInDb()做幂等检查——因为如前所述,appVersion只负责“是否执行”,不替你记录“是否已执行”。
示例三:使用原始 SQL / 模型批量处理数据(afterSync)
在表结构同步完成后,用原始 SQL 或逐条模型保存做数据迁移:
import { Migration } from '@nocobase/server'; export default class extends Migration { on = 'afterSync'; appVersion = '<1.0.0-alpha.3'; async up() { const items = await this.pm.repository.find(); for (const item of items) { if (item.name.startsWith('@nocobase/plugin-')) { item.set('name', item.name.substring('@nocobase/plugin-'.length)); await item.save(); } } } }该场景把插件名从带 scope 前缀的@nocobase/plugin-xxx改为短名,依赖的是 afterSync 阶段表结构已经就绪这一前提。
创建 Migration 文件:CLI 命令与生成逻辑
通过 CLI 命令创建:
yarn nocobase create-migration my-migration --pkg @my-project/plugin-hello命令会在插件的src/server/migrations/目录下生成带时间戳的文件,模板如下:
import { Migration } from '@nocobase/server'; export default class extends Migration { on = 'afterLoad'; appVersion = '<当前版本>'; async up() { // coding } }命令参数:
| 参数 | 说明 |
|---|---|
<name> | migration 名称,用于生成文件名 |
--pkg <pkg> | 包名,决定文件存放路径 |
--on <on> | 执行时机,默认'afterLoad' |
CLI 侧与 server 侧各有一层实现,可以对照阅读:
- CLI 包装层:packages/core/cli/src/commands/scaffold/migration.ts 定义了
nocobase create-migration命令,--pkg为必填,--on仅接受beforeLoad/afterSync/afterLoad三个枚举值;它把参数拼装为['create-migration', name, '--pkg', pkg](可选追加--on)后转调 server 侧命令。 - Server 实现层:packages/core/server/src/commands/create-migration.ts 真正负责落盘,关键行为包括:
- 文件名使用
dayjs().format('YYYYMMDDHHmmss')时间戳前缀,即YYYYMMDDHHmmss-<name>.ts,保证同目录下多个 migration 可按文件名排序确定执行先后; - 落盘目录:对
@nocobase/server本身是src/migrations/,其余包统一为src/server/migrations/(若目录不存在会mkdir -p); appVersion的“当前版本”由app.getPackageVersion()推算:若版本含alpha/beta后缀则保留原号${major}.${minor}.${patch},否则取下一个 minor 版${major}.${minor + 1}.0,并以<前缀写成 semver 范围;- 模板中的 import 路径:为
@nocobase/server核心包生成时用相对路径'../migration',其余插件包使用'@nocobase/server'。
- 文件名使用
相关链接
- Migration 升级脚本(插件开发) — 插件开发中 migration 的使用教程
- Collections 数据表 — defineCollection 和表结构同步
- Database 数据库操作 — Repository API 和数据库操作
- Plugin 插件 — 插件生命周期中 install() 和 migration 的关系
【免费下载链接】nocobaseNocoBase is an open-source AI + no-code platform for building business systems fast. Instead of generating everything from scratch, AI works on top of production-proven infrastructure and a WYSIWYG no-code interface, so you get both speed and reliability.项目地址: https://gitcode.com/GitHub_Trending/no/nocobase
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考