Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

应用升级

和“发布”相比,“升级”最容易被说得过于理想化。就 Oak 当前仓库里的真实实现来看,升级能力主要由两部分组成:

  • 代码与结构升级;
  • 静态数据、权限数据、i18n 数据的同步升级。

其中第二部分已经有比较成型的工具链,核心就是 oak-backend-base/src/routines/update.ts 中的 createUpdatePlan(...)

一、先区分两类升级

1. 代码 / 结构升级

这部分通常包括:

  • 修改 Entity
  • 重新执行 make:domain
  • 如果 Oak 依赖模块有增删,重新执行 project:init
  • 重新执行 make:dep
  • 重新编译后端和前端
  • 生成并审核 db:upgrade:plan
  • 发布新版本代码
  • 首次部署执行 server:init;已有库按计划执行结构升级 SQL

这仍然属于一套完整的工程发布动作,不能简单地被一个“升级脚本”完全替代。

当前 make:domain 会优先从依赖模块的 es/entities 读取实体产物,其次才是 lib/entitiessrc/entities。升级 Oak 依赖后,即使项目自己的 src/entities 没变,也应重新执行 make:domain,否则 oak-app-domain 可能仍然停留在旧依赖类型上。

1.1 TypeScript 配置布局迁移

旧项目可能仍把 Web、小程序、Native 的 include、aliases 和编译选项堆在根 tsconfig.json。当前 CLI 使用“根构建配置 + 共享源码配置 + workspace 本地配置”的结构,可先查看迁移计划:

oak-cli migrate:tsconfig --dry-run

确认计划后执行:

oak-cli migrate:tsconfig

迁移命令会发现标准平台目录以及 npm scripts 中通过 --subDir 声明的自定义 workspace,并完成这些动作:

  • 把 ES、Lib aliases 分别迁移到 tsconfig/paths.es.jsontsconfig/paths.lib.json
  • 生成或修复 src/tsconfig.json 和各 workspace 的 tsconfig.json
  • 修复配置之间的 extends,并为 make:domain 补充 --configFile ./tsconfig.es.json
  • 用 TypeScript parser、关键 alias 和最小平台配置检查验证迁移结果;
  • 验证通过后才删除已被替代的旧根配置。

写入前,CLI 会把原文件备份到 .oak-tsconfig-migration/<时间戳>;写入或验证失败时会回滚。--dry-run 只输出计划,不创建备份或修改文件。

已有 workspace 配置默认会尽量保留,只修复能够安全判断的旧 extends。只有明确要用当前模板替换 workspace 配置时才使用:

oak-cli migrate:tsconfig --force

--force 可能覆盖 workspace 中的定制编译选项,执行前必须审核 dry-run 和 Git diff。遇到 project references、项目外部 extends、无法归属单一 workspace 的 include 等边界时,命令会拒绝做破坏性删除,应该人工拆分配置后重试。

1.2 数据库结构升级计划

当前 CLI 模板已经内置:

npm run db:upgrade:plan

它实际调用的是 oak-cli upgrade。这个命令会启动 AppLoader,读取当前编译后的 lib/oak-app-domain/Storage 和数据库现状,生成结构升级计划。默认只生成计划,不执行 SQL。

默认输出目录形如:

.oak-upgrade/20260508-153000

里面最重要的文件是:

  • migration.sql:按执行顺序整理后的正向结构升级 SQL;
  • rollback.sql:由 backwardSql 生成的结构回滚 SQL;
  • summary.json:本次计划的统计摘要;
  • table-changes.json:逐表变更详情;
  • warnings.json:需要人工关注的风险;
  • rename-candidates.json:疑似重命名的列或索引。

常用参数如下:

npm run db:upgrade:plan -- -o .oak-upgrade/release-20260508
npm run db:upgrade:plan -- --largeTableRowThreshold 500000
npm run db:upgrade:plan -- --execute

需要注意几件事:

  1. 先执行 make:domainbuild,再生成计划。oak-cli upgrade 读取的是编译后的运行产物,不是直接读 src/entities
  2. 确认 NODE_ENV 和数据库配置指向目标库。命令会按 mysql.${NODE_ENV}.jsonmysql.jsonpostgres.${NODE_ENV}.jsonpostgres.json 的顺序找配置。
  3. 不带 --execute 时只写文件,不会改库。带 --execute 时会执行排序后的结构升级 SQL,包含 prepareSql / manualSql / forwardSql / onlineSql,不会自动跳过人工步骤。
  4. manualSql 代表 planner 认为这一步需要人工审核,不代表 --execute 会自动跳过。只要计划里有 manualSqlwarningsrenameCandidates,就应先人工确认。
  5. largeTableRowThreshold 默认是 100000。大表索引新增、删除、重建可能被转成 manualSql,避免自动 DDL 长时间锁表或造成性能抖动。
  6. rollback.sql 不是数据库备份。它只能表达框架能推导出来的结构回退,不能恢复被删除列里的业务数据,也不能替代上线前备份。
  7. 这个命令只处理数据库结构,不负责静态数据、权限数据和 i18n 数据同步。后者仍然走 createUpdatePlan 相关脚本。

2. 数据升级

这部分是 Oak 当前更擅长抽象的内容,尤其适合处理:

  • path
  • relation
  • actionAuth
  • relationAuth
  • i18n
  • 某些静态业务数据

它们的共同特点是:数据本身通常放在 lib/data 中,可以被视为项目或模块的一部分,并且希望以一种可重复执行的方式同步到数据库。

二、createUpdatePlan 解决了什么问题

oak-backend-base/src/routines/update.ts 本质上是一个数据同步计划生成器。它会把当前项目 lib/data 中的数据与数据库里的现有数据做对比,然后按你给定的策略处理差异。

它支持的核心策略包括:

onUniqueViolation

当数据文件里的记录与数据库已有记录发生唯一索引冲突时,如何处理:

  • error
  • skip
  • update

onOnlyExistingInDb

当数据库里有、但数据文件里没有时,如何处理:

  • skip
  • delete
  • physicalDelete

生命周期钩子

你还可以提供:

  • beforeCheck
  • afterUpdate

这样就可以在真正写库前后,插入项目自定义逻辑。

三、真实项目中的升级脚本长什么样

bm-smart 已经给出了很直接的样板。

只升级 i18n

scripts/upgradeI18n.js

startup(pwd, simpleConnector, true, true, createUpdatePlan({
    plan: {
        i18n: { onUniqueViolation: 'update', onOnlyExistingInDb: 'physicalDelete' },
    }
}))

只升级权限相关数据

scripts/upgradeAuth.js

它同步:

  • path
  • actionAuth
  • relation
  • relationAuth

同时还能在 beforeCheck 中对数据做额外修正。

新 CLI 模板已经补充了权限升级脚本入口。项目新增或调整权限模型后,应该把权限数据升级作为发布步骤的一部分,而不是只依赖 make:dep

全量数据升级

scripts/update.js

它把 i18n、权限、以及部分业务静态数据一起纳入同步计划。

也正因为如此,npm run upgrade:all 在真实项目里的含义,不是“框架神奇地帮你升级一切”,而是:

按当前项目自己定义的 update plan,把 lib/data 中指定的实体数据同步进数据库。

四、update plan 执行时会做什么

update.ts 的实现来看,它大致会经历下面这些步骤:

  1. 读取 lib/data/index
  2. 合并 oak-domain 自带的 i18n 数据;
  3. 对每个目标实体做前置校验;
  4. 分析反向引用关系;
  5. 查询数据库中已有的数据;
  6. 比较差异,决定新增、更新、跳过还是删除;
  7. 处理唯一索引冲突;
  8. 必要时更新反向引用;
  9. 按依赖顺序删除多余数据;
  10. 执行 afterUpdate

代码里还明确做了几件对升级非常关键的事情:

  • 使用 forUpdate 锁定记录,减少并发问题;
  • 默认 blockTrigger: true,避免触发器干扰升级过程;
  • 支持逻辑删除和物理删除两种清理策略;
  • 支持对引用关系做拓扑排序后再删除。

这说明 Oak 的数据升级工具,关注的重点是“可重复执行、引用关系正确、差异同步清晰”,而不是简单粗暴地覆盖数据。

五、升级时如何处理旧版本客户端

这是 Oak 里另一个很重要的升级问题。

oak-general-business/src/aspects/application.ts 中的 checkAppVersionSafe(...) 会根据:

  • system.oldestVersion
  • platform.oldestVersion
  • application.dangerousVersions
  • application.warningVersions

来决定当前客户端:

  • 是否必须升级;
  • 是否只给出警告。

如果版本过低,后端会抛出 OakApplicationHasToUpgrade。与此同时,oak-general-business/src/context/BackendRuntimeContext.ts 在异常推导阶段也会继续返回这个异常。

所以 Oak 的“升级”并不只是数据库更新,它还包括:

  • 如何允许旧版本继续访问;
  • 从什么时候开始强制升级;
  • 哪些版本只是提醒,哪些版本必须拦截。

六、一个推荐的升级顺序

在实际项目里,比较稳妥的升级顺序通常是:

  1. 修改代码、实体与静态数据;
  2. 依赖变化时先执行 project:init,再执行 make:domainmake:localemake:depbuild
  3. 执行 db:upgrade:plan,审核 migration.sqlwarnings.jsonrename-candidates.json
  4. 发布新后端代码;
  5. 首次部署执行 server:init;已有库执行审核后的结构升级 SQL 或谨慎使用 db:upgrade:plan -- --execute
  6. 执行 upgrade:locale / upgrade:auth / upgrade:all 这类数据同步脚本;
  7. 再发布前端产物;
  8. 根据版本策略决定是否拦截旧客户端。

如果你的升级中包含不兼容结构变更,就更不能跳过这套顺序。

七、近期框架升级特别注意

近期 Oak 框架有几类变化会影响升级判断:

  • 发布包实体解析改为 es/entities -> lib/entities -> src/entities,模块不应靠发布 src 维持编译;
  • oak-db 新建表不再默认输出数据库物理外键,旧项目应通过 db:upgrade:plan 审核引用关系和生成 SQL;
  • update expression 已支持 MySQL / PostgreSQL,但 JSON 字段中的普通对象按字面量处理;
  • 前后端 context 已带 locale,翻译、通知和多语言内容生成应改用统一上下文;
  • 小程序路由以 Oak canonical route 为准,动态裸 wx.* 跳转应迁移到 Oak navigator。

八、不要把升级理解成“一条命令”

最后强调一点:

Oak 的升级能力是拆开的:db:upgrade:plan 负责结构升级计划,createUpdatePlan 负责静态数据同步,不能把它们理解成“一条命令包办所有升级问题”。

这并不算缺点。恰恰相反,这种拆分反而更符合真实项目:

  • 代码升级归代码发布流程;
  • 数据库结构升级归 db:upgrade:plan
  • 静态数据升级归 update plan;
  • 客户端兼容归 application 版本策略。

把这三者混成一件事,往往才是上线事故的开始。