演示

如何为插件创建数据库迁移

通过 IPlugin.getMigrations() 返回实现 getName、getSource、up、down 的 IDatabaseMigration 类交付 schema 变更。预发与生产升级请优先用迁移而非 synchronize。

目标与何时使用

实体注册仍会为运行时声明表,但真实 schema 演进应走迁移。用于增删改 DDL、重命名、索引、约束,以及必须随插件升级只运行一次的数据回填。

  • 预发 / 生产 — 不要依赖 synchronize
  • 可回滚变更 — 保持 up() 与 down() 成对
  • 已发布文件 — 不要修改;新增带编号的迁移

1. 前置条件

需要在 backend/src/index.ts 中有默认导出的 IPlugin,并明确目标数据源(通常是共享默认)。

  • backend/src/index.ts 导出实现 IPlugin 的插件类
  • 明确变更所属连接 — 通常为 { plugin: "default", name: "default" }
  • 实体定义将更新为与最终迁移 schema 一致
  • 编号约定 — backend/src/migrations/ 下的 001、002、…

2. 目录布局

将迁移类放在 backend/src/migrations/,并导出以便 getMigrations() 返回它们。

  • 每文件一类;保持名称 / 编号有序
  • 若插件使用 ESM,从 index 用 .js 扩展名导入
TSlayout
1backend/ 2 src/ 3 index.ts # IPlugin — getMigrations() 4 migrations/ 5 001-initial.migration.ts 6 002-add-status.migration.ts 7 feature/ 8 my.module.ts

3. 实现 IDatabaseMigration

实现 getName()、getSource()、up(QueryRunner)、down(QueryRunner)。用 metadata.name 为 getName() 加前缀以跨插件唯一。getSource() 通常返回 { plugin: "default", name: "default" }。

  • getName() — 唯一 id;以 metadata.name 为前缀
  • getSource() — 执行 SQL 的 DataSource(多数插件用共享默认库)
  • up() — 应用变更;本地重跑时可优先 IF EXISTS / IF NOT EXISTS
  • down() — 撤销同一变更
TSXmigration class
1import type { IDatabaseMigration } from "@quan-erp/shared-types"; 2import type { QueryRunner } from "typeorm"; 3import metadata from "../../module.metadata.json" with { type: "json" }; 4 5export class AddStatusMigration implements IDatabaseMigration { 6 getName() { 7 return `${metadata.name}_002_add_status`; 8 } 9 10 getSource() { 11 return { plugin: "default", name: "default" }; 12 } 13 14 async up(queryRunner: QueryRunner): Promise<void> { 15 await queryRunner.query( 16 `ALTER TABLE ${metadata.name}_order ADD COLUMN IF NOT EXISTS status varchar(32)`, 17 ); 18 } 19 20 async down(queryRunner: QueryRunner): Promise<void> { 21 await queryRunner.query( 22 `ALTER TABLE ${metadata.name}_order DROP COLUMN IF EXISTS status`, 23 ); 24 } 25}

4. 通过 getMigrations() 注册

在 backend/src/index.ts 的默认导出插件类中,由 getMigrations() 返回迁移类。可用可选的 onMigrate() 做迁移后种子或修复。

  • 数组顺序应与预期应用顺序一致
  • onMigrate() 在待执行迁移批次完成后运行
  • 自定义数据源 — getSource() 须匹配该连接的 plugin + name
TSXbackend/src/index.ts
1import type { GetMigrationsType, IPlugin } from "@quan-erp/shared-types"; 2import { InitialMigration } from "./migrations/001-initial.migration.js"; 3import { AddStatusMigration } from "./migrations/002-add-status.migration.js"; 4 5export default class MyPlugin implements IPlugin { 6 getMigrations(): GetMigrationsType { 7 return [InitialMigration, AddStatusMigration]; 8 } 9 10 async onMigrate(): Promise<void> { 11 // optional: seed / repair after migrations apply 12 } 13 14 // … getRootModule, getName, getVersion, getMetadata, … 15}

5. 验证与规则

安装或升级插件时,核心读取 getMigrations()、应用未执行的名称,并在模块初始化前调用 onMigrate()。

  • 未执行的 getName() 会在 getSource() 上运行 up()
  • 实体列 / 索引与最终迁移 schema 一致
  • 需要回滚时在本地 / 预发测试 down()
  • 不要修改已发布迁移 — 新增 003-…
  • 真实升级优先迁移而非 synchronize
  1. 01

    安装 / 升级插件

    核心加载 module.js 并读取 getMigrations()。

  2. 02

    应用未执行迁移

    每个尚未用过的 getName() 会在 getSource() 上执行 up(queryRunner)。

  3. 03

    onMigrate()

    迁移批次完成后的可选插件钩子。

  4. 04

    模块初始化

    实体、DI 与 @OnInit 照常继续。

常见错误

  • 在 IDatabaseMigration 上用普通 name 属性而不是 getName() / getSource()
  • 忘记用 metadata.name 为 getName() 加前缀(跨插件冲突)
  • getSource() 错误 — 迁移跑在与实体不同的 DataSource 上
  • 修改已发布迁移而不是新增编号文件
  • 只改实体不写迁移(或相反)
  • 生产升级依赖 synchronize: true