拓冰建站拓冰建站
首页 / 资讯中心 / 正文

Ghost 数据库迁移实战指南:七条规则与工具函数背后的源码解析

Ghost 数据库迁移实战指南七条规则与工具函数背后的源码解析【免费下载链接】GhostIndependent technology for modern publishing, memberships, subscriptions and newsletters.项目地址: https://gitcode.com/GitHub_Trending/gh/GhostGhost 在启动时通过数据库迁移migration来演进数据库结构这是发布流程中最容易一处出错、全线崩溃的环节。本文以 Ghost 仓库内置的迁移规则文档.agents/skills/create-database-migration/rules.md为主体逐条深入讲解每条规则的设计动机并结合ghost/core下的迁移工具函数源码ghost/core/core/server/data/migrations/utils/与真实迁移文件展示规则是如何被代码机制兜住的。读完本文你能够掌握 Ghost 迁移的完整规范并理解幂等性、防御式编程与日志策略在源码层面的具体实现方式。一、迁移规则全景为什么这些规则存在Ghost 的迁移规则文档列出了七条硬性规则可以归纳为三个层次正确性保障迁移必须幂等idempotent、必须防御式defensive、禁止使用模型层model layer工程协作保障迁移文件一旦进入main分支就不可变immutable、迁移 PR 必须尽可能小可维护性保障优先使用工具函数、每条代码路径都要有日志。这些规则不是空谈。Ghost 的启动流程会阻塞等待迁移执行完毕一条崩溃的迁移意味着站点无法启动而一条不可重跑的迁移意味着生产事故后无法安全恢复。下面的章节逐一拆解每条规则并在ghost/core源码中找到其落地证据。二、规则一迁移必须幂等Migrations must be idempotent规则原文的核心论断是迁移必须可以安全地运行两次。迁移完全可能因为外部因素停止执行因此必须保证重新运行迁移能够成功完成。也就是说迁移在执行到一半失败后重试的场景下必须依然正确不能因为表已存在而报Table already exists不能因为列已添加而重复插入不能因为记录已存在而触发唯一约束冲突。工具函数如何把幂等性变成默认行为Ghost 没有靠开发者自觉来保证幂等而是在 ghost/core/core/server/data/migrations/utils/tables.js 中以先检查状态、不满足则跳过的模式统一实现。以addTable为例tables.js#L12-L35function addTable(name, tableSpec) { return createNonTransactionalMigration( async function up(connection) { const tableExists await connection.schema.hasTable(name); if (tableExists) { logging.warn(Skipping adding table: ${name} - table already exists); return; // 幂等表已存在则直接跳过 } logging.info(Adding table: ${name}); return commands.createTable(name, connection, tableSpec); }, async function down(connection) { const tableExists await connection.schema.hasTable(name); if (!tableExists) { logging.warn(Skipping dropping table: ${name} - table does not exist); return; } logging.info(Dropping table: ${name}); return commands.deleteTable(name, connection); }, ); }up与down两个方向都先查询数据库当前状态hasTable状态已符合预期就记录一条 warn 日志并提前返回。同样的检查—跳过模式还出现在addSetting/removeSettingsettings.js#L17-L119插入设置前先where(key, , key).first()查重已存在则Skipping adding setting: ... already existsaddPermissionHelperpermissions.js#L18-L54按name action_type object_type查重权限已存在则Permission for ... already added并返回createAddColumnMigration/createDropColumnMigrationschema.js#L44-L99通过commands.createColumnMigration传入dbIsInCorrectState谓词例如hasColumn true列已处于目标状态时跳过操作createSetNullableMigrationschema.js#L158-L211先用isColumnNullable探测列的可空性已为目标状态则跳过。事务包装器失败时的原子性兜底对 DML 类迁移ghost/core/core/server/data/migrations/utils/migrations.js 提供了createTransactionalMigrationmigrations.js#L48-L60把整个up包进数据库事务。当迁移中途抛出异常时事务回滚使数据库保持原状下一次重跑等价于第一次运行——这也是幂等性的另一层保障function createTransactionalMigration(up, down) { return { config: { transaction: true }, async up(config) { await up(config.transacting); // 传入 knex 事务对象 }, async down(config) { await down(config.transacting); }, }; }而 DDL 类操作建表、加列等在部分数据库上不支持事务回滚因此对应的createNonTransactionalMigrationmigrations.js#L9-L21显式声明transaction: false把幂等责任下放给先检查、后执行的状态判断逻辑。三、规则二迁移禁止使用模型层Migrations must NOT use the model layer这条规则解释了迁移开发中最隐蔽的陷阱迁移是为某个特定版本编写的使用模型层时默认假设模型属于那个版本。但实际运行时模型属于正在迁移到的目标版本而不是迁移的起始版本。模型中的破坏性变更breaking changes会不自觉地弄坏旧迁移。从源码结构看这正是 Ghost 迁移体系的核心矛盾迁移文件位于core/server/data/migrations/versions/版本目录/按发布 minor 版本归档当新版本启动、执行从旧数据库状态到新的迁移时进程里加载的模型Bookshelf models却是新版本代码——模型字段、序列化行为都可能已经改变。如果迁移依赖模型 API 去读写数据等于在旧数据状态上运行了新语义的代码行为不可预测。规则给出的替代方案是直接使用迁移携带的数据库事务对象和迁移工具函数操作数据。这一点在settings.js、permissions.js中体现得很彻底——它们全部使用裸 knex 查询connection(settings).where(...)、connection(permissions).insert(...)不require任何 Bookshelf 模型甚至时间戳都用connection.raw(CURRENT_TIMESTAMP)交给数据库生成settings.js#L27。四、规则三迁移文件不可变Migrations are Immutable规则原文一旦迁移进入main分支它就是最终版本。如果在合并到 main 之后还需要变更请创建一条新的迁移。从源码结构看这条规则的破坏后果非常具体Ghost 用 knex-migrator 追踪哪些迁移已执行过各环境的数据库都记录着自己的迁移执行历史。如果合并后修改或删除某条迁移已执行过该迁移的环境其执行记录与新文件不再对应不同环境的数据库将处于不同的结构状态迁移追踪migration tracking会直接失序。Ghost 官方文档docs/practices/database-migrations.md对这条规则给出了一个可操作的范例如果某列在迁移进入main之后需要重命名不要改旧迁移而是让旧迁移在必要时变为 no-op再新增迁移来完成创建正确列 删除旧列若存在。若存在这个限定词本身就是幂等规则在不可变场景下的应用。五、规则四优先使用工具函数Use utility functions规则要求尽可能使用ghost/core/core/server/data/migrations/utils中的工具函数如addTable、createTransactionalMigration、addSetting。这些工具函数经过测试已内置幂等保护并在合适的位置包含日志语句方便调试迁移。ghost/core/core/server/data/migrations/utils/index.js 将五个模块统一导出形成完整的工具面模块主要导出适用场景migrations.jscreateTransactionalMigration、createNonTransactionalMigration、createIrreversibleMigration、combineTransactionalMigrations、combineNonTransactionalMigrations、createFinalMigration选择事务语义、组合多个迁移tables.jsaddTable、dropTables、recreateTable建表、删表、重建表schema.jscreateAddColumnMigration、createDropColumnMigration、createRenameColumnMigration、createAddIndexMigration、createSetNullableMigration、createDropNullableMigration加/删/重命名列、加索引、改可空性settings.jsaddSetting、removeSetting增删系统设置permissions.jsaddPermission、addPermissionToRole、addPermissionWithRoles、createRemovePermissionMigration等权限与角色授权选对事务包装器DDL 与 DML 的分工migrations.js中的三个基础包装器对应三种语义createTransactionalMigrationmigrations.js#L48-L60config.transaction trueup/down拿到的是config.transacting事务对象。DML 数据迁移的默认选择失败整体回滚createNonTransactionalMigrationmigrations.js#L9-L21config.transaction falseup/down拿到的是config.connection裸连接。多数 DDL 操作建表、加列使用它createIrreversibleMigrationmigrations.js#L28-L40声明irreversible: truedown直接Promise.reject()。用于不可回滚的操作如dropTablestables.js#L42-L55——删表后无法还原不如显式拒绝回滚。此外还有两个值得注意的设计组合器combineTransactionalMigrations/combineNonTransactionalMigrationsmigrations.js#L67-L110把多个迁移串成一条且down时按逆序执行源码注释特意标注// Down migrations must be run backwards!!。一个真实的组合示例见 2024-10-31-15-27-42-add-jobs-queue-columns.jsconst { combineNonTransactionalMigrations, createAddColumnMigration } require(../../utils); module.exports combineNonTransactionalMigrations( createAddColumnMigration(jobs, metadata, { type: string, maxlength: 2000, nullable: true, }), createAddColumnMigration(jobs, queue_entry, { type: integer, nullable: true, unsigned: true, }), );createFinalMigration(major)migrations.js#L115-L128一个刻意让up抛错的终态迁移报错信息提示用户必须先升级到该主版本的最新补丁版才能跨主版本升级。这是版本更新策略的硬门禁也说明迁移文件不仅承担数据变更还承担升级路径管控。一个真实的列迁移示例2022-12-13-16-15-add-usage-colums-to-tokens.js 展示了createAddColumnMigration的典型用法——给tokens表一次性加三列每列声明type、nullable、unsigned、defaultTo等属性const { createAddColumnMigration, combineNonTransactionalMigrations } require(../../utils); module.exports combineNonTransactionalMigrations( createAddColumnMigration(tokens, updated_at, { type: dateTime, nullable: true, }), createAddColumnMigration(tokens, used_count, { type: integer, nullable: false, unsigned: true, defaultTo: 0, }), );createAddColumnMigration内部schema.js#L44-L67把 up 与 down 分别映射为commands.addColumn/commands.dropColumn并以dbIsInCorrectState谓词实现幂等。注意它返回的是非事务迁移——这与加列属于 DDL的常识一致也印证了官方文档的提示并非所有操作都有相同的事务行为抄一份近期相似迁移比臆断更安全。方言差异如何被封装schema.js还展示了工具函数对 MySQL / SQLite 双方言差异的封装。例如createNullableMigrationschema.js#L21-L35的注释解释了SQLite 修改列可空性要重建整张表而重建所需的PRAGMA foreign_keys开关在事务打开后无效所以同一逻辑在 SQLite 上必须以非事务方式运行applyNullableChangeschema.js#L125-L149则针对两个引擎分别切换PRAGMA foreign_keys与SET FOREIGN_KEY_CHECKS。开发者写迁移时无需感知这些细节——这正是用工具函数的价值所在。六、规则五迁移 PR 应尽可能小Minimal migration PRs规则明确了最小 PR的构成清单新的迁移文件schema.js的同步更新ghost/core/core/server/data/schema/schema.js更新后的 schema 完整性哈希测试涉及增删表时更新导出器exporter的表清单。配套的实操流程见 docs/practices/database-migrations.md迁移文件统一用pnpm migrate:create slug生成而非手工命名CI 再用 scripts/check-migration-integrity.cjs 复核版本与落位。生成脚本 ghost/core/bin/create-migration.js 的做法值得注意slug 强制 kebab-case校验正则见 create-migration.js#L23文件固定放入core/server/data/migrations/versions/下一个 minor 版本/文件名带时间戳前缀必要时把 Core 与 Admin 包的package.json版本号提升到目标 minor 的-rc.0——脚本注释说明这是必需的因为 knex-migrator 会过滤版本号高于package.jsonmajor.minor 的目录不提升的话新迁移在开发环境会被静默跳过create-migration.js#L68-L71。这些机制共同把最小 PR从口号变成可检查的工程约束迁移 PR 里不该出现无关的 schema 改动、不该手动移动版本目录。七、规则六迁移必须防御式Defensive migrations规则原文只有一句话却道出了最高风险场景对缺失数据做好防护。如果迁移崩溃Ghost 无法启动。防御式在工具函数中表现为两类模式状态探测失败不阻断。以createSetNullableMigration为例schema.js#L160-L179如果isColumnNullable探测本身抛错代码不会让迁移失败而是记录 warn 日志后继续执行} catch (error) { // 无法检查列状态时继续迁移保持与早期实现的向后兼容 logging.warn( Could not check nullable status for ${table}.${column}, proceeding with migration: ${error.message}, ); }结构探测兼容历史差异。addSetting在插入前先探测settings表是否有created_by/updated_by列有则写入迁移用户 IDMIGRATION_USER 1无则跳过settings.js#L39-L45。这是对不同数据库停留在不同历史状态的直接防御同一条迁移可能在缺列的旧库与不缺列的新库上分别运行代码必须两种情况都接受。引用缺失时的取舍差异。对比permissions.jsaddPermissionToRoleHelper在权限或角色缺失时抛错数据关系断裂必须暴露而removePermissionFromRoleHelper在资源缺失时只记 warn 后跳过删除是收敛操作目标状态不存在已经达成。这种加法严格、减法宽容的不对称是防御式设计里很实用的一条经验。八、规则七每条代码路径都要打日志Log every code path规则的逻辑很直接如果需要调试迁移就必须知道它实际做了什么。没有日志这不可能做到。所以确保所有代码路径与提前返回early return都包含日志。注意使用工具函数时日志通常由工具函数自身处理无需额外打日志。回头看前文源码这条规则已被内建为工具函数的标准写法——每个分支都有对应的logging.warn/logging.info跳过分支Skipping adding table: X - table already exists执行分支Adding table: X回滚分支Dropping table: X/Skipping dropping table: X - table does not exist。以addSetting为例settings.js#L18-L58up 的已存在跳过 / 新增、down 的不存在跳过 / 删除四个分支全部有日志事后只需翻启动日志即可还原迁移到底走了哪条路。这也正是规则第七与规则四的衔接点既然要求每条路径有日志而工具函数已经做到了那么优先用工具函数就同时满足了日志要求——两条规则在实现层面是闭环的。九、把规则落到实操一次完整迁移的开发路径综合规则文档、docs/practices/database-migrations.md 与仓库脚本在 Ghost 中新增一条迁移的标准路径是规划先行先明确目标 schema表、列、命名数据迁移先评估数据规模生成文件在ghost/core下运行pnpm migrate:create add-column-to-postskebab-case slug由 bin/create-migration.js 负责落位到下一个 minor 版本目录并处理版本号提升选择包装器DML 用createTransactionalMigrationDDL 用createNonTransactionalMigration或专用 helperaddTable、createAddColumnMigration等拿不准就抄一份近似的既有迁移versions/目录中有大量真实样例本地迭代pnpm knex-migrator migrate --v version-directory --force跑up()pnpm knex-migrator rollback --v previous-version --force跑down()让down还原up之前的状态同步收尾schema 变更同步 schema.js重跑 schema 完整性测试pnpm test:single test/unit/server/data/schema/integrity.test.js跑迁移集成测试pnpm test:single test/integration/migrations/migration.test.js覆盖初始化、回滚、前向迁移与幂等性自查七条规则是否幂等、是否绕开模型层、是否不可变、是否用了工具函数、PR 是否最小、是否防御缺失数据、是否每条路径都有日志。十、小结规则与代码的对应关系规则源码落点机制幂等utils/tables.js、utils/schema.js状态探测 提前返回事务回滚兜底禁用模型层utils/settings.js、utils/permissions.js全部走裸 knex 查询不依赖 Bookshelf不可变docs/practices/database-migrations.md已合入 main 的迁移不改追加新迁移用工具函数utils/index.js五个模块统一导出覆盖 DDL/DML/设置/权限最小 PRbin/create-migration.js、scripts/check-migration-integrity.cjs脚本生成 CI 复核版本落位防御式utils/schema.js#L170-L176、utils/settings.js#L39-L45探测失败降级、结构差异兼容全路径日志各 utils 文件每个分支内置logging.warn/logging.info这套体系的核心思想可以概括为一句话迁移不是普通业务代码而是运行在不可控历史状态上的一次性基建作业——所以幂等是底线、防御是本能、日志是生命线而把这些素质固化进工具函数就是 Ghost 迁移规则能长期被遵守的原因。【免费下载链接】GhostIndependent technology for modern publishing, memberships, subscriptions and newsletters.项目地址: https://gitcode.com/GitHub_Trending/gh/Ghost创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门