bin/magento setup:upgrade 在自定义 Data Patch 中报错退出。修好一行代码后直接重跑,新的错误却变成 duplicate key。这是因为 Patch 是否完成由 patch_list 登记决定,而业务表里的部分写入不一定随异常回滚。

先停止重复执行并保留现场

记录完整堆栈、Patch 类名、当前 release、数据库备份点和已经修改的行。不要手工向 patch_list 插入类名来“跳过”,除非已经证明 Patch 的所有预期结果完整存在;否则环境会永久处于半升级状态。

SELECT patch_name
FROM patch_list
WHERE patch_name LIKE '%Vendor%';

bin/magento setup:db:status

判断哪些写入已经发生

阅读 Patch 的 apply 方法,列出每一步会新增或更新的实体:CMS 页面、属性、配置、分类或自定义表。对每一步写只读查询,形成“预期—实际”对照。Repository save、外部 API 和多次循环可能不在同一个数据库事务内。

让 Patch 可重复运行

新增记录前按稳定业务键查找;存在则校验并更新,不存在才创建。不要把自动递增 ID 当跨环境固定标识。属性、CMS identifier 等已有唯一业务键时,应利用它们。

$page = $this->pageFactory->create()->load('shipping-help', 'identifier');
if (!$page->getId()) {
    $page->setIdentifier('shipping-help');
}
$page->setTitle('Shipping Help');
$this->pageRepository->save($page);

示例只表达幂等思路,实际还要设置 store、content 与 active 等必需字段。若业务不允许覆盖人工编辑内容,Patch 应仅创建缺失记录,后续变更用新的 Patch。

事务边界要现实

Data Patch 接口不保证所有服务调用都自动处在一个可回滚的大事务里。大量数据迁移可以按批次记录进度,但进度表本身也要设计一致性。外部 API 不应直接放进不可重放的数据库升级;更适合在升级后由可重试任务执行。

恢复选择

如果部分数据可安全回滚,先用可审计脚本撤回,再运行修正版 Patch;如果数据应保留,让修正版识别并补齐缺失项。不要修改已经在其他环境成功执行过的 Patch 逻辑却保持同一类名,否则环境行为会分叉;通常应增加修复 Patch,并明确依赖关系。

在数据库副本演练从原始失败点恢复,连续执行两次 setup:upgrade,第二次应无数据变化。随后检查 patch_list、schema status 和业务页面。只有重复运行安全,才算真正解决中断问题。

部署流程要能识别半完成状态

setup:upgrade 失败后不应继续编译、切换流量或清理旧 release。流水线需要立即停止并保留日志;健康检查不仅看首页 200,还要检查数据库版本状态。回滚代码却保留新数据库结构也可能不兼容,因此每个 Patch 上线前都应明确向前修复方案,而不是假定数据库可以无损回滚。

Patch 依赖必须是有向无环关系

getDependencies() 应只声明真正的数据前置条件。循环依赖会让升级无法排序,漏依赖则可能在不同模块加载顺序下偶发失败。跨模块依赖还要保证 composer/module 依赖存在,否则某环境卸载了被依赖模块,Patch 类本身就无法加载。

Revertable 不等于生产可安全回滚

实现 PatchRevertableInterface 后,模块卸载可能调用 revert,但删除属性、CMS 页面或业务数据可能已经有人工修改和关联记录。只有能明确识别“本 Patch 创建且未被业务使用”的数据才适合自动删除。否则保留向前修复 Patch,并在卸载文档中说明人工步骤。

Patch 中需要特定 area code、store context 或 indexer 时,应显式建立最小上下文并恢复状态;不要依赖 setup:upgrade 当时恰好加载了 adminhtml。测试应从全新数据库和已经运行多个旧版本的数据库两条路径执行,因为升级路径问题往往不会在全新安装出现。