ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Convex 数据迁移模式实战指南:widen-migrate-narrow 工作流与零停机策略

Convex 数据迁移模式实战指南:widen-migrate-narrow 工作流与零停机策略 数据库后端【免费下载链接】convex-backendThe open-source reactive database for app developers项目地址https://gitcode.com/gh_mirrors/co/convex-backend点击查看免费下载导读本指南以convex-migration-helper技能的迁移模式参考文档migration-patterns.md为核心系统讲解 Convex 在遇到破坏性 Schema 变更时的标准迁移方法论从添加必填字段、删除字段、修改字段类型、拆分嵌套表到清理孤儿文档再到双写Dual Write与双读Dual Read两种零停机策略最后覆盖小型表快捷迁移、迁移结果验证等实操细节。读完本文你将掌握一套可复制的「放宽 Schema → 迁移数据 → 收窄 Schema」多部署工作流并能结合convex-dev/migrations组件在生产环境安全执行批量、可断点续跑的在线数据迁移。一、迁移的底层约束Schema 验证驱动整个工作流在动手写迁移代码之前必须先理解一个贯穿 Convex 迁移所有模式的根本约束Convex 不允许部署与库中存量数据不一致的 Schema。从 schema.ts 的源码可以看出schemaValidation默认开启options?.schemaValidation undefined ? true : options.schemaValidation。这意味着 Convex 在部署时会校验新 Schema 与数据实际形态是否匹配由此推导出三条铁律不能给已有文档添加必填字段——存量文档没有该字段校验直接失败不能直接修改字段类型——存量文档仍持有旧类型校验失败不能从 Schema 中删除仍被存量数据使用的字段——同样校验失败。这正是参考文档与 SKILL 文件SKILL.md反复强调的「Schema Validation Drives the Workflow」所有破坏性迁移都必须遵循widen the schema → migrate the data → narrow the schema放宽 → 迁移 → 收窄的可预测模式。另一个关键背景是在线迁移Online MigrationConvex 迁移在应用持续对外服务的同时以异步批次方式更新数据。因此迁移窗口期内你的业务代码必须能够同时处理新旧两种数据格式。二、多部署工作流破坏性变更的标准流程任何破坏性迁移都遵循同一个多部署节奏部署 1 —— 放宽 Schema更新 Schema使其同时兼容新旧两种格式例如新增一个 optional 字段更新读取逻辑处理两种格式更新写入逻辑让新文档写入新格式部署。两次部署之间 —— 迁移数据运行迁移回填存量文档验证所有文档均已迁移。部署 2 —— 收窄 Schema更新 Schema只保留新格式必填删除处理旧格式的代码部署。注意如果只是安全变更则无需迁移——添加 optional 字段、新增全新表、新增索引都不会触碰校验约束。以本仓库 waitlist 示例工程schema.ts为例其waitlist表通过.index(by_position, [position])新增索引就属于安全变更。三、五种常见迁移模式附完整代码以下模式均以convex-dev/migrations组件的migrations.define为核心 APImigrateOne负责处理单条文档组件自动完成分批、基于游标的分页、状态追踪、失败续跑、dry run 与进度监控。3.1 添加必填字段Adding a Required Field三步走先以 optional 形式放开回填数据再收窄为必填。// 部署 1Schema 同时允许两种状态 users: defineTable({ name: v.string(), role: v.optional(v.union(v.literal(user), v.literal(admin))), }); // 迁移回填字段 export const addDefaultRole migrations.define({ table: users, migrateOne: async (ctx, user) { if (user.role undefined) { await ctx.db.patch(user._id, { role: user }); } }, }); // 部署 2迁移完成后将字段改为必填 users: defineTable({ name: v.string(), role: v.union(v.literal(user), v.literal(admin)), });关键点migrateOne中的if (user.role undefined)守卫保证迁移幂等——重复运行不会产生副作用ctx.db.patch只做增量更新不会覆盖文档的其他字段。3.2 删除字段Deleting a Field删除比添加更谨慎先标记为 optional迁移清空数据最后才从 Schema 移除。SKILL 文档还强调一个原则——除非确定不再需要否则优先用v.optional 注释来废弃字段而非物理删除。// 部署 1改为可选 // isPro: v.boolean() -- isPro: v.optional(v.boolean()) // 迁移 export const removeIsPro migrations.define({ table: teams, migrateOne: async (ctx, team) { if (team.isPro ! undefined) { await ctx.db.patch(team._id, { isPro: undefined }); } }, }); // 部署 2从 Schema 中彻底移除 isPro3.3 修改字段类型Changing a Field Type参考文档给出的核心建议是优先新增字段而不是原地改类型。可以「加新删旧」一步完成// 部署 1新增字段旧字段保持 optional // isPro: v.boolean() -- isPro: v.optional(v.boolean()), plan: v.optional(...) // 迁移旧字段值转换到新字段 export const convertToEnum migrations.define({ table: teams, migrateOne: async (ctx, team) { if (team.plan undefined) { await ctx.db.patch(team._id, { plan: team.isPro ? pro : basic, isPro: undefined, }); } }, }); // 部署 2移除 isProplan 改为必填这个模式的优越性在于迁移期间新旧字段并存任何时刻都能安全回滚且存量数据的转换逻辑集中在一个幂等函数里。3.4 将嵌套数据拆分为独立表Splitting Nested Data Into a Separate Table把用户内嵌的preferences对象抽到独立的userPreferences表同时保持幂等查询已存在记录则跳过插入export const extractPreferences migrations.define({ table: users, migrateOne: async (ctx, user) { if (user.preferences undefined) return; const existing await ctx.db .query(userPreferences) .withIndex(by_user, (q) q.eq(userId, user._id)) .first(); if (!existing) { await ctx.db.insert(userPreferences, { userId: user._id, ...user.preferences, }); } await ctx.db.patch(user._id, { preferences: undefined }); }, });参考文档特别提醒了一个迁移窗口竞态运行此迁移之前必须确保业务代码已经开始为新用户写入userPreferences表否则迁移窗口期内新建的文档会被漏掉导致迁移「完成」后仍有未迁移数据。这与 SKILL 文档的常见陷阱第 3 条完全对应Not writing the new format before migrating。3.5 清理孤儿文档Cleaning Up Orphaned Documents删除不再被引用的数据例如找不到对应chunks记录的孤立embeddingsexport const deleteOrphanedEmbeddings migrations.define({ table: embeddings, migrateOne: async (ctx, doc) { const chunk await ctx.db .query(chunks) .withIndex(by_embedding, (q) q.eq(embeddingId, doc._id)) .first(); if (!chunk) { await ctx.db.delete(doc._id); } }, });四、零停机策略Dual Write 与 Dual Read迁移窗口期内应用必须同时兼容新旧两种数据格式。参考文档给出了两种主策略。4.1 双写Dual Write推荐写双份、读旧格式四步完成切换部署同时写新旧两种格式、但只读旧格式的代码对存量数据运行迁移部署读取新格式、仍然双写的代码部署只读写新格式的代码。之所以优先推荐是因为任何时刻都可以安全回滚且旧格式始终是最新的。反例与正例对比// 坏迁移完成前只写新结构 export const createTeam mutation({ args: { name: v.string(), isPro: v.boolean() }, handler: async (ctx, args) { await ctx.db.insert(teams, { name: args.name, plan: args.isPro ? pro : basic, }); }, }); // 好迁移期间同时写两种结构 export const createTeam mutation({ args: { name: v.string(), isPro: v.boolean() }, handler: async (ctx, args) { const plan args.isPro ? pro : basic; await ctx.db.insert(teams, { name: args.name, isPro: args.isPro, plan, }); }, });4.2 双读Dual Read读两种格式优先新格式、只写新格式三步完成切换部署读两种格式偏好新、只写新格式的代码对存量数据运行迁移部署只读写新格式的代码。// 好读两种格式偏好新格式 function getTeamPlan(team: Docteams): basic | pro { if (team.plan ! undefined) return team.plan; return team.isPro ? pro : basic; }该策略避免重复写入适合双份数据可能引发不一致的场景代价是回滚到第 1 步之前更困难——因为新文档只持有新格式。五、小型表快捷迁移一个 internalMutation 搞定对于最多几千条文档的小表可以不用 migrations 组件直接用一个internalMutation完成全量回填import { internalMutation } from ./_generated/server; export const backfillSmallTable internalMutation({ handler: async (ctx) { const docs await ctx.db.query(smallConfig).collect(); for (const doc of docs) { if (doc.newField undefined) { await ctx.db.patch(doc._id, { newField: default }); } } }, });通过 CLI 运行npx convex run migrations:backfillSmallTable参考文档给出的硬性边界是只有在确定表很小的情况下才能用.collect()——它会把整张表一次性加载进事务表稍大就会触发事务限制或超时对应 SKILL 文档常见陷阱第 2 条。任何更大的表都应该交给 migrations 组件做分批分页处理。CLI 命令npx convex run由 cli/run.ts 实现支持传入 JSON 参数如{body: hello, author: me}、--prod选择生产部署、--watch实时刷新查询结果等选项。六、验证迁移是否完成6.1 自定义验证查询写一个只读 query检查还剩多少未迁移文档import { query } from ./_generated/server; export const verifyMigration query({ handler: async (ctx) { const remaining await ctx.db .query(users) .filter((q) q.eq(q.field(role), undefined)) .take(10); return { complete: remaining.length 0, sampleRemaining: remaining.map((u) u._id), }; }, });.take(10)只取样前 10 条避免全表扫描返回complete布尔值与剩余样本 ID方便在 dashboard 或 CI 中人工确认。6.2 组件内置状态监控使用 migrations 组件自带的getStatus函数--watch模式可持续观察进度npx convex run --component migrations lib:getStatus --watch更完整的配套命令详见 migrations-component.md还包括# Dry run跑一个批次后回滚只预览不落数据 npx convex run migrations:runIt {dryRun: true} # 取消正在运行的迁移 npx convex run --component migrations lib:cancel {name: migrations:addDefaultRole} # 部署后链式执行全部迁移 npx convex deploy --cmd npm run build npx convex run migrations:runAll --prod七、常见陷阱与检查清单7.1 六大常见陷阱迁移数据前就把字段设为必填Schema 校验直接拒绝部署因为存量文档缺字段。必须先放宽 Schema。在大表上使用.collect()触发事务限制或超时。只有确认很小几千条以内的表才安全否则必须用 migrations 组件的分批分页。迁移前不写新格式迁移窗口期新建的文档会被漏掉迁移「完成」后仍有残留旧数据。跳过 dry run用dryRun: true先验证迁移逻辑能在触碰生产数据前发现 bug。过早删除字段优先用v.optional 注释废弃只有确认数据与代码都不再引用后才物理删除。用 cron 调度迁移批次migrations 组件通过内部递归调度自动分批cron 方案需要手动清理且要额外部署一次才能移除。7.2 迁移检查清单识别破坏性变更规划多部署工作流更新 Schema同时兼容新旧格式更新读取代码处理两种格式更新写入代码让新文档写入新格式部署放宽后的 Schema 与代码用convex-dev/migrations组件定义迁移以dryRun: true测试运行迁移并监控状态验证所有文档均已迁移更新 Schema 只保留新格式清理处理旧格式的代码部署最终 Schema 与代码确认稳定后移除迁移代码八、进一步阅读迁移组件的安装、配置、批量大小调优与索引子集迁移migrations-component.md技能总览何时使用、安全变更与破坏性变更判定SKILL.mdSchema 校验开关与配置源码schema.tsnpx convex runCLI 实现cli/run.ts仓库内 waitlist 示例工程的 Schema 与函数schema.ts、waitlist.ts赞分享数据库后端【免费下载链接】convex-backendThe open-source reactive database for app developers项目地址https://gitcode.com/gh_mirrors/co/convex-backend点击查看免费下载相关推荐Convex 数据库零停机 Schema 迁移实战widen-migrate-narrow 工作流与 convex-dev/migrations 组件Convex 数据库零停机 Schema 迁移实战widen migrate narrow 工作流与 convex dev/migrations 组件 Co数据库后端Convex 数据库 schema 与数据迁移实战指南基于 convex-migration-helper 的 widen-migrate-narrow 零停机迁移工作流Convex 数据库 schema 与数据迁移实战指南基于 convex migration helper 的 widen migrate narrow 零停数据库后端Convex 迁移助手基于 widen-migrate-narrow 工作流的零停机 Schema 与数据迁移指南Convex 迁移助手基于 widen migrate narrow 工作流的零停机 Schema 与数据迁移指南 导读 本文围绕 convex backen数据库后端创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表