为跨新旧读取方的存储字段迁移编写任务说明
本文目录
请智能体重命名一个存储字段之前,先说明哪些读取方和写入方还可能使用旧名称。然后定义现有记录如何迁移、冲突值如何处理,以及从哪个阶段开始,回到旧版应用就不再只是一次代码回退。
字段重命名的影响会比源代码重命名持续得更久。旧记录、后台任务和读取方可能在代码变更后继续存在。Danilo Sato 的 Parallel Change 介绍了先扩展接口、再迁移使用方、最后移除旧接口的方法。Prisma ORM v7 迁移指南 则提供了一个具体的列迁移示例。两份来源都没有替你的应用确定写入协调策略。下面的完整契约示例会明确选择一种策略。
从完整记录开始,包括棘手的情况
这个虚构应用用 old_name 存储一个可为 null 的名称。替代字段 display_name 的含义和允许值完全相同:字符串,包括空字符串,或者 null。字段不存在,与字段存在但值为 null,是两种不同的状态。
以下八条记录是原创教学数据。它们表示一次中断的扩展之后的混合快照,既不是数据库导出,也不是迁移成功的结果。本指南后续的所有状态都是人工指定的预期。
[
{"id":"m1","revision":1,"old_name":"Ari"},
{"id":"m2","revision":1,"old_name":null},
{"id":"m3","revision":3,"old_name":"Bo","display_name":"Bo"},
{"id":"m4","revision":5,"old_name":"Cora","display_name":"Coral"},
{"id":"m5","revision":2,"old_name":null,"display_name":"Dee"},
{"id":"m6","revision":4,"display_name":"Eli"},
{"id":"m7","revision":1,"old_name":""},
{"id":"m8","revision":2,"old_name":null,"display_name":null}
]
示例中的旧读取方在 old_name 存在时返回其值,在该字段不存在时返回 null。新读取方则要求 display_name 必须存在。revision 数字描述的是这里给定的历史,并不是在规定数据库的并发控制机制。
| 记录 | 复制前需要作出的决定 |
|---|---|
| m1、m2、m7 | 新字段不存在:确认写入暂停之后,才可以进行精确复制 |
| m3、m8 | 两个字段已经一致:保持值和修订号不变 |
| m4 | 两个字符串冲突:停止自动处理,取得明确的解决决定 |
| m5 | 已存在的 null 与字符串冲突:这同样是冲突,不能把 null 理解为“缺失” |
| m6 | 只有新字段有值:旧读取方会读到 null;宣布整批数据已就绪之前,先解决这个兼容缺口 |
只复制容易处理的记录,并不能让整个数据集达到兼容状态。尤其是,“优先使用新字段”的兜底规则,会在没有需求依据的情况下,悄悄替 m4 和 m5 作出决定。
选择如何阻止写入方与回填交错执行
下面是一个缺乏协调的复制操作的反例:
| 事件 | 记录值 |
|---|---|
| 复制器读取修订号为 1 的 m1 | old_name="Ari";新字段不存在 |
| 旧写入方 W0 更新 m1 | 修订号为 2,old_name="Aria";新字段仍不存在 |
| 复制器无条件使用此前读到的旧值 | 新字段变成 "Ari",旧字段仍为 "Aria" |
这些行描述的是假设的历史,不是实际观察到的竞态。它们说明,仅仅要求“把旧字段复制到新字段”,并不能证明这种操作可以与只写旧字段的写入方安全共存。
接下来的练习选择暂停对待迁移记录的写入。读取可以继续,但应用、任务、管理操作和迁移相关的所有写入,都必须纳入协调范围。取得迁移快照之前,先等待已开始的工作结束,或以其他方式妥善处理。如果无法确认这些条件成立,就停在调查方案阶段。
这个选择用写入可用性换取更简单的过渡过程。它不声称实现了零停机迁移。如果真实系统必须持续写入,就需要针对该系统制定有依据的协调和冲突处理设计;本指南不会因为写了两条字段赋值语句,就推断双写是安全的。
先解决冲突,再只填充尚不存在的新字段
在这个虚构练习中,一份给定的解决说明作出了三项决定:
- D1:m4 的预期名称是
Coral;将两个字段都设为该值,并把修订号从 5 改为 6。 - D2:m5 确实应该没有名称;两个字段都保留为
null,并把修订号从 2 改为 3。 - D3:m6 的预期名称是
Eli;为旧读取方补上缺失的旧字段,并把修订号从 4 改为 5。
这些是为示例编造的决定,并不是授权你按相同方向处理自己的记录。请在自己的迁移记录中保留冲突前的值和决策理由。
在写入仍然暂停时,从已存在的 old_name 填充尚不存在的 display_name。精确复制 null 和 ""。不要覆盖任何已经存在的新字段值。如果一对已存在的字段发生冲突,或者所需的源字段不存在,就停止。
预期结果如下:
| ID | 修订号 | old_name | display_name |
|---|---|---|---|
| m1 | 2 | "Ari" |
"Ari" |
| m2 | 2 | null |
null |
| m3 | 3 | "Bo" |
"Bo" |
| m4 | 6 | "Coral" |
"Coral" |
| m5 | 3 | null |
null |
| m6 | 5 | "Eli" |
"Eli" |
| m7 | 2 | "" |
"" |
| m8 | 2 | null |
null |
在同一个暂停写入阶段,再执行一遍回填,任何记录都不应改变,修订号也不应增加。筛选条件是“新字段不存在”;如果改成“新字段为 null”,就会错误地再次选中 m2、m5 和 m8。
如果出现预期外的写入或修订号变化,就必须停止并核对处理。只完成一部分不等于完成迁移;在决定如何继续之前,记录哪些行已经发生变化。这里不假定存在任何自动回退或备份恢复行为。
切换使用方之前,先写好兼容性表
| 阶段 | 记录预期 | 读取方 | 写入方 | 进入下一阶段的条件 |
|---|---|---|---|---|
| 扩展(Expand) | 旧字段保留;新字段可能不存在 | 旧读取方 R0 读取旧字段 | 迁移前暂停并核清 W0 及所有其他写入方 | 快照和未完成的工作已核清 |
| 迁移(Migrate) | 先处理 D1–D3,再填充不存在的新字段;八条记录的字段对全部一致 | R0 仍可读取旧字段值 | 对这些记录的所有写入仍暂停 | 没有未解决的记录;重复执行不产生变化 |
| 切换读取 | 完整且相等的字段对保持不变 | 将使用方迁移到读取新字段的 R1 | 仍暂停写入 | 所有旧读取方和旧写入方都已停用,或已用其他方式排除 |
| 切换写入 | 新字段成为权威值来源 | 仅 R1 | 恢复只写新字段的 W1;W0 保持禁用 | 新编辑和新记录遵循新契约 |
| 收缩(Contract) | 在依赖清单满足条件后移除旧字段 | 仅 R1 | 仅 W1 | 没有旧使用方、报表、任务或回退假设仍依赖旧数据 |
这个方案有意在读取方迁移期间持续暂停写入,从而避免需要一段双写期。存在未知的旧客户端或后台写入方,是继续保留兼容阶段的理由,不能因此认为它“大概已经没人用了”。
W1 启用之后,假设它作出以下两项变更:
[
{"id":"m1","revision":3,"old_name":"Ari","display_name":"Aria"},
{"id":"m9","revision":1,"display_name":"Faye"}
]
在只写新字段的策略下,m1 的旧值已经过期、m9 没有旧字段,都是预期状态。旧读取方会分别读到 Ari 和 null,而不是 Aria 和 Faye。因此,必须先停用它,再恢复 W1。
旧回填只是在其定义的阶段内可以重复执行,并不是永远都能重跑。不要在只写新字段的操作开始之后重新启用它,也不要把这些有意形成的差异称为数据损坏。
明确回退含义发生变化的时点
在恢复只写新字段的操作之前,这个示例中的新旧字段值仍然相等,可以考虑在保持写入暂停时切回旧读取方。但这仍然只是一条有待验证的兼容路径。
W1 写入之后,仅把应用二进制切回旧版,就会暴露过期或缺失的旧字段值。收缩阶段移除 old_name 之后,这个示例中的旧读取方会对每条记录返回 null。根据当前新字段值重新引入旧字段,属于另一项向前修复工作,需要已验证的映射和协调;恢复更早的快照,也可能丢弃此后产生的工作。
删除兼容字段之前,先把这些限制写进方案。“我们有备份”并不能证明恢复已经检查过,也不能证明迁移后的编辑能够被妥善合并处理。
复制这份调查请求
为给定虚构记录规划从 old_name 到 display_name 的迁移。
不要连接数据库,也不要改变真实数据。
含义:
两个名称表示同一个可为 null 的字符串。空字符串、null
和字段不存在是不同状态。必须保留 ID 和无关字段。
盘点新旧读取方和写入方,包括任务与正在执行的工作。
选定策略是在快照前暂停对待迁移记录的所有写入,
并持续暂停至读取方切换完成。
如果不能确认暂停条件成立,返回缺失的证据和方案;
不要把未经协调的复制称为安全双写。
使用八条完整输入记录,以及明确的 D1–D3 解决决定。
仅填充不存在的 display_name。不要覆盖已经存在的
新字段,也不要仅因字段值为 null 就选中该记录。
给出扩展、冲突解决与回填、读取切换、只写新字段后的编辑
与新记录,以及旧字段移除各阶段的预期记录。展示在写入
持续暂停时,第二遍回填不会产生变化。
恢复只写新字段的 W1 之前,必须停用旧读取方和旧写入方。
保留 m1 编辑和新记录 m9 的示例。解释从这一时点起,
为什么旧复制器和切回旧读取方的回退方式不再适用。
返回有明确范围的方案、各阶段停止条件、未决的存储
假设和回退限制。将人工示例与实际执行的检查分开。
这里不授权数据库操作、安装、删除、生产请求、
发布或备份恢复。
有用的交付,是记录、使用方和停止条件彼此一致的过渡方案。成功复制一个字段,只是这份契约的一部分。