news 2026/9/14 9:11:37

NocoBase Migration API 深度解析:插件升级时的数据库结构变更与数据迁移

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
NocoBase Migration API 深度解析:插件升级时的数据库结构变更与数据迁移

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 },并在构造函数中注入,暴露dbsequelizecontext.db.sequelize)和queryInterfacecontext.db.sequelize.getQueryInterface())三个 getter,以及空实现的up()/down()方法;
  • 应用层基类@nocobase/serverMigration(见 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); }

两个要点值得注意:

  1. 留空即“每次执行”——!m.appVersion为真时直接放行,这也是基类默认值appVersion = ''的语义。由于不符合范围的 migration 在加载阶段就被过滤掉,所以appVersion是“是否执行”的开关,而不是记录“是否已执行”的状态;已执行过但留空的 migration 会在每次 upgrade 重新运行,务必让up()逻辑幂等。
  2. includePrerelease: true——比较时显式开启预发布版本匹配,因此<0.21.0-alpha.13这类带 pre-release 的范围才能正确生效。

实例属性

基类与子类共同提供了访问应用各模块的 getter,全部迁移逻辑都通过它们完成。

app:Application 实例

get app(): Application

NocoBase Application 实例,通过它可以访问应用的各个模块:

async up() { // 获取应用版本 const version = this.app.version; // 获取日志 this.app.log.info('Migration started'); }

db:Database 实例

get db(): Database

NocoBase 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(): Sequelize

Sequelize 实例,可以直接执行原始 SQL:

async up() { await this.sequelize.query(`UPDATE users SET status = 'active' WHERE status IS NULL`); }

queryInterface:执行 DDL

get queryInterface(): QueryInterface

Sequelize 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 侧各有一层实现,可以对照阅读:

  1. CLI 包装层:packages/core/cli/src/commands/scaffold/migration.ts 定义了nocobase create-migration命令,--pkg为必填,--on仅接受beforeLoad/afterSync/afterLoad三个枚举值;它把参数拼装为['create-migration', name, '--pkg', pkg](可选追加--on)后转调 server 侧命令。
  2. 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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/14 9:11:12

Gitee生态下SCA软件成分分析工具落地指南与选型框架

软件成分分析工具这是一个看着简单、实际特别容易跑偏的选型题目。前阵子帮一个团队做安全体系建设评审&#xff0c;他们公司代码托管从一开始就落在 Gitee 上&#xff0c;开源组件用了上百个&#xff0c;但 SCA&#xff08;软件成分分析&#xff09;一直停留在"听说过、没…

作者头像 李华
网站建设 2026/9/14 9:11:04

统一感知物联网系统:百万设备高并发架构设计与落地实践

1. 什么是“统一感知物联网系统”&#xff1f;它真能扛住百万设备百万QPS吗&#xff1f;“统一感知物联网系统”不是某个厂商的营销话术&#xff0c;也不是PPT里一闪而过的概念图——它是我在过去三年里亲手从0到1搭起来、又在产线连续跑满18个月的一套真实架构。核心就一句话&…

作者头像 李华
网站建设 2026/9/14 9:09:06

膝点迁移动态多目标优化:用历史轨迹加速环境响应

做过多目标优化的朋友都知道&#xff0c;跑完一轮NSGA-II或者MOEA/D之后&#xff0c;屏幕上往往会铺开一大片Pareto非支配解。可等你真要把结果交给业务方、写进系统里的时候&#xff0c;面对几十上百个解&#xff0c;决策者只会拍板选一个。这时候你会发现&#xff0c;前沿上大…

作者头像 李华
网站建设 2026/9/14 9:05:55

ChatGPT智能备考四六级:阅读提速与写作突破方案

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/14 9:05:19

大模型推理优化全解析:KV Cache、PagedAttention与30倍吞吐提升

聊到大模型推理优化&#xff0c;很多人第一反应是"上vLLM就完事了""4bit量化拉满&#xff0c;速度肯定快"。但真正跑到生产环境里&#xff0c;你会发现事情没那么简单&#xff1a;显存莫名其妙涨、并发一高速度就崩、量化之后效果漂移严重。过去一年我把大…

作者头像 李华