首页
/ Ghost 数据库迁移实战指南:从 pnpm migrate:create 到 knex-migrator 的完整工作流

Ghost 数据库迁移实战指南:从 pnpm migrate:create 到 knex-migrator 的完整工作流

2026-09-05 22:48:59作者:郜逊炳

本篇基于 Ghost 仓库中的技能文档 .agents/skills/create-database-migration/SKILL.md 及其关联的 docs/practices/database-migrations.md,完整讲解在 Ghost 中修改 MySQL 数据库 schema 的标准流程:如何用 pnpm migrate:create 生成迁移文件、如何基于 migrations/utils 工具函数编写幂等且可回滚的迁移、如何用 knex-migrator 本地迭代与回滚验证,以及如何通过迁移集成测试、导出器一致性断言与 schema 完整性哈希测试确保迁移安全。读完后你可以独立完成一次包含建表、加列、加 setting、数据修改在内的数据库 schema 变更,并理解其底层实现依据。

一、为什么需要迁移,以及它何时被触发

Ghost 的数据库迁移(migration)会在 Ghost 启动时转换数据库状态。根据 docs/practices/database-migrations.md,一次迁移通常做以下事情之一:

  • 新增一张表;
  • 给已有表加列;
  • 添加用户权限(user permissions);
  • 修改已有数据。

文档特别警告:迁移是高风险区域,一个失误可能造成大范围损坏。因此迁移应当被仔细规划,并作为功能开发的最早步骤之一提交,以便在代码评审中留出足够的反馈时间。在写迁移之前,先明确期望的 schema——需要的表、列及其命名;如果是数据迁移(DML),还要先弄清现有数据的规模与形态。

技能文档 SKILL.md 把这条流程固化为一份可执行清单:只要任务涉及给 Ghost 的 MySQL 数据库加/删/改列或表、加 setting、建索引、更新数据,或用户提到 schema.jsknex-migrator、migrations 目录、“给某模型/表加个字段”,都应遵循这一流程。

二、第一步:用脚本生成空迁移文件,而不是手写

清单的第 1 步明确要求:

cd ghost/core
pnpm migrate:create <kebab-case-slug>

slug 必须是 kebab-case,例如 add-column-to-posts。并且严禁手工创建迁移文件,必须通过该脚本生成初始的空文件。

这条命令在 ghost/core/package.json 中映射到 node bin/create-migration.js,脚本实现见 ghost/core/bin/create-migration.js。从源码看,脚本做了四件关键事情:

  1. 校验 slug:用正则 /^[a-z0-9]+(-[a-z0-9]+)*$/ 强制 kebab-case,非法 slug 直接抛错;
  2. 决定目标版本目录getTargetMigrationFolder 通过 git describe 读取最近一个稳定版 tag(排除 prerelease),目标目录固定为“已发布版本的下一个 minor”;如果当前包版本已经提升到该 minor(或更高,即一个周期内的第二个迁移),则留在当前 minor。例如最后发布版本为 6.34.0 时,包版本 6.34.1-rc.06.35.0-rc.0 都会落到 6.35 目录;
  3. 提升版本号为 RC:若当前 package.json 的 minor 与目标目录不一致,脚本会把 Ghost Core 与 Admin 两个包的版本都改为 {target}.0-rc.0。源码注释解释了原因:knex-migrator 会过滤掉版本号高于 package.json major.minor 的目录,不做提升的话新迁移在开发环境会被静默跳过;
  4. 写入迁移模板:以 YYYY-MM-DD-HH-MM-<slug>.js 命名,写入一个带注释的模板,引导开发者选择 DDL 用的 createNonTransactionalMigration、DML 用的 createTransactionalMigration,或更具体的 helper(如 addTablecreateAddColumnMigration),并导出 module.exports = /**/; 占位。

脚本还做了防御处理:文件已存在时以 EEXIST 判定并给出清晰报错,避免覆盖已有迁移。

生成后,文件会落在 ghost/core/core/server/data/migrations/versions/<版本目录>/ 下——目录不存在时脚本会自动创建。

三、编写迁移本体:DDL 与 DML 的选型,以及 utils 工具函数

打开生成的迁移文件后,按 rules.md 的要求“尽可能使用 ghost/core/core/server/data/migrations/utils/ 中的工具函数”。这些工具函数已经过测试,内置了幂等性保护与调试日志。utils 的入口 index.js 聚合了五个模块,各自导出的核心函数为:

  • migrations.jscreateTransactionalMigration(事务型,DML 首选)、createNonTransactionalMigration(非事务,很多 DDL 操作需要)、createIrreversibleMigrationdown() 直接 reject,用于不可回滚操作)、combineTransactionalMigrations / combineNonTransactionalMigrations(组合多个迁移,down() 按相反顺序执行)、createFinalMigration
  • tables.jsaddTabledropTablesrecreateTable
  • settings.jsaddSettingremoveSetting
  • schema.jscreateAddColumnMigrationcreateDropColumnMigrationcreateSetNullableMigrationcreateDropNullableMigrationcreateRenameColumnMigrationcreateAddIndexMigration
  • permissions.js:权限相关工具。

官方指南给出的选型原则是:DML 迁移通常用 createTransactionalMigration;许多 DDL 操作需要 createNonTransactionalMigrationaddTablecreateAddColumnMigration 这类聚焦的 schema helper。不要假设每种操作的事务行为相同,最佳起点是复制一个做类似变更的近期迁移。

四类典型变更在 examples.md 中各给了一个仓库内的真实范例,可直接对照阅读:

变更类型 参考迁移
建表(DDL) add mentions table
给已有表加列(DDL) add source columns to emails table
添加 setting(DML) add member track source setting
数据修改(DML) update newsletter subscriptions

四、本地迭代:用 knex-migrator 前进与回滚

写完迁移后,进入迭代循环。Ghost 使用定制的 knex-migrator(对应 package.json 中的 knex-migrator 依赖),清单给出两条本地命令:

cd ghost/core
# 前向执行,调用迁移的 up()
pnpm knex-migrator migrate --v {version directory} --force

# 回滚以验证 down(),然后再次前向迁移
pnpm knex-migrator rollback --v {previous version} --force

migrate 调用迁移的 up() 方法,rollback 调用 down() 方法;--v 指定要针对的版本目录,--force 用于本地强制执行。官方指南建议的迭代节奏就是:在 down() 能够恢复到 up() 之前的同一状态的前提下,反复前进/回滚验证。

五、迁移之外的强制配套修改

这是容易被漏掉、但对 Ghost 整体一致性至关重要的部分:

  1. 同步 schema 定义:必须更新 ghost/core/core/server/data/schema/schema.js,确保与迁移带来的最新 schema 变化对齐;
  2. 建表/删表时更新导出清单:若新增或删除了表,需要相应更新 ghost/core/core/server/data/exporter/table-lists.js。一致性由 test/unit/server/data/exporter/index.test.js 中的断言保证——它会检查 schema 中的每一张表都在导出器清单中归类过;
  3. 更新 schema 完整性哈希:修改 schema.js、fixtures、默认 settings 或默认路由之后,要运行完整性测试并只更新你自己这次改动对应的那个期望哈希:
cd ghost/core
pnpm test:single test/unit/server/data/schema/integrity.test.js

package.jsontest:single 脚本可以看到其路由逻辑:test/unit/* 走普通 vitest,其余(数据库相关的集成/单元用例)走 vitest.config.db.ts 配置,因此 test:single 一条命令即可跑单测、迁移集成测试与导出器测试。

六、测试矩阵:迁移必须过的四道关

按清单顺序,迁移完成后的验证序列是:

cd ghost/core
# 1. 迁移集成测试:覆盖初始化、回滚、前向迁移与幂等性
pnpm test:single test/integration/migrations/migration.test.js

# 2. 仅在导出清单变化时跑导出器一致性单测
pnpm test:single test/unit/server/data/exporter/index.test.js

# 3. schema 完整性测试(同时用于更新哈希)
pnpm test:single test/unit/server/data/schema/integrity.test.js

# 4. Ghost Core 全量单元测试,反复迭代直到通过
pnpm test:unit

其中 test/integration/migrations/migration.test.js 是迁移的“总闸门”——清单明确要求迁移必须对 MySQL 和 SQLite 两种后端都通过 database-backed 套件。官方指南补充说明:MySQL 是 Ghost 支持的生产数据库,数据库相关的迁移套件针对它运行;而涉及保留的 SQLite 兼容路径的改动,应当为该方言补充聚焦的单测。

七、铁律:幂等、不可变、不用模型层、防御式、全路径打日志

.agents/skills/create-database-migration/rules.mddocs/practices/database-migrations.md 的 Rules 部分共同构成了不可妥协的硬约束:

  • 必须幂等:迁移可能因外部因素中断执行,重跑一次必须安全,且不能把数据库留在非法状态。这是最高优先级要求;
  • 禁止使用模型层:迁移是针对特定数据库版本编写的,但实际加载的模型来自“正要迁移到的版本”。模型层的破坏性变更会悄悄打破旧迁移。应直接使用迁移的数据库事务和 migrations/utils 工具函数;
  • 迁移不可变:一旦进入 main 分支即告终结。后续需要再改,新建一个迁移。官方指南还给出了具体例子:若某列的迁移已合入 main 后发现列名要改,只在必要时把原迁移变成 no-op 以修复问题,再用新迁移创建正确的列、删除旧列(如存在)。修改或删除已运行过的迁移会让不同环境落在不同的数据库状态,并可能破坏迁移跟踪;
  • PR 尽量最小:迁移 PR 通常只应包含——新迁移文件、schema.js 的更新、schema 完整性哈希测试的更新、(如涉及表增删时)导出器清单的更新。迁移埋在大型变更里最容易出错;
  • 防御式编写:防止缺失数据。迁移一旦崩溃,Ghost 无法启动;
  • 每个代码路径都要打日志:调试迁移时必须知道它实际做了什么,所有分支和提前返回都要有日志。使用 utils 工具函数时,日志通常由工具函数内部处理,无需重复添加;
  • 性能:大表上的 schema 变更、索引变更和数据更新都很昂贵,且迁移会阻塞 Ghost 启动直到完成。避免无界循环与批量大更新,大数据量改动要分批;不要在同一次迁移里混用 DDL 和 DML;在小本地库上测出的性能不代表大站安全,拿不准的改动要在 PR 中显式标注提醒评审者;
  • 版本策略:迁移必须保持 Ghost 的版本升级策略——用户能够从任一受支持大版本的最后一个补丁版本完成升级(对应 migrations.jscreateFinalMigration 的跨大版本拦截逻辑)。

评审时关注三点:正确性(是否做了预期的变更)、性能(能否在安全时间内完成)、安全性(是否防护了缺失或非法数据)。对大表或高频更新表的变更要额外做性能审视。CI 侧还有 scripts/check-migration-integrity.cjs 重复执行版本与目录放置的检查,防止迁移文件被放到已发布的版本目录中。

八、完整工作流速查

把以上步骤串起来,就是一份可照做的清单:

  1. cd ghost/core && pnpm migrate:create <kebab-case-slug> 生成空迁移(自动放置到正确版本目录、必要时提升 core/admin 到 RC);
  2. 按既有模式填充迁移代码,优先使用 migrations/utils 中的工具函数,保证幂等与日志;
  3. 同步更新 ghost/core/core/server/data/schema/schema.js
  4. pnpm knex-migrator migrate --v {version directory} --force 手动验证前向迁移;
  5. pnpm knex-migrator rollback --v {previous version} --force 验证 down(),再前向迁移一次;
  6. pnpm test:single test/integration/migrations/migration.test.js 跑迁移集成测试(MySQL/SQLite 双后端);
  7. 若增删了表:更新 ghost/core/core/server/data/exporter/table-lists.js 并跑 pnpm test:single test/unit/server/data/exporter/index.test.js
  8. pnpm test:single test/unit/server/data/schema/integrity.test.js 更新 schema 哈希;
  9. pnpm test:unit 全量单测通过;
  10. 提交最小化 PR(迁移文件 + schema.js + 哈希测试 + 导出清单),等待评审(关注正确性、性能、安全性)。

只要遵循“脚本生成、工具函数封装、双方向验证、全路径日志”这条主线,一次 Ghost 数据库迁移就能在可评审、可回滚、可重跑的前提下安全落地。

登录后查看全文
热门项目推荐
相关项目推荐