Appearance
ADR-0010: 数据库 schema 以 Flyway 为唯一真相源,ddl-auto 切 validate,迁移在业务 Pod 内执行
- 日期:2026-08-09 状态:生效
- 主文档:数据库 Schema 管理规范
背景(什么问题)
2026-07-31 之前,全部 8 个服务都是 ddl-auto: update 且没有任何迁移工具。 表结构一直是 Hibernate 首次启动时顺手创建的 —— 全仓从来没有过建表 DDL。
这个状态在 2026-08-06 到 08-09 连续暴露出三个问题:
- 加一列引发服务再也起不来。 vem 加
ice_selector_enabled时写了 MariaDB 语法的ADD COLUMN IF NOT EXISTS,MySQL 直接语法报错,Flyway 把这次迁移记成 failed, 之后每次启动都在校验阶段被拒绝,得手工删 history 行才能恢复。 ddl-auto: update有一类事永远做不到:删列、改类型、改列名、数据订正、 回答"这个客户的库改到哪一步了"。私有化交付有多个客户各自一套库,最后一条是致命的。- 新客户根本装不上。 切 validate 之后 Hibernate 不再建表,而没有任何建表脚本;
hiapi-cloud-public的 V1~V4 全是 ALTER 型脚本,在空库上会因"表不存在"直接失败。
决策(怎么定的)
1. Flyway 是唯一真相源,ddl-auto: validate
Hibernate 从此一个字都不改库,只在启动时核对实体与表结构,对不上拒绝启动。 schema 只能通过 db/migration/ 下的脚本变更。
2. Flyway 走 ShardingSphere 数据源,脚本只写逻辑表名
框架 ShardingConfig 建数据源时补传 databaseName(取 ds_0 的真实库名)。 此前不传、逻辑库名默认 logic_db,Flyway 按连接 catalog 找不到对应库直接空指针 —— 2026-08-02 曾据此判断"Flyway 不能走 ShardingSphere",绕成单独直连; 2026-08-09 实测证明那只是少传了一个参数。
拿到的是:CREATE TABLE hiapi_vem_device_slot 一条建出 4 张取模分片, 一条 ALTER 覆盖按月分表的几十张月表。四仓基线从 54/119/62/37 条精简到 43/58/52/30 条。
代价是加字段不能写守卫(三种写法均实测失败,见主文档 §6 ②), 只能写裸 DDL,幂等靠 Flyway "每条只执行一次"。业主 2026-08-10 知情选择接受 —— 备选方案(Flyway 直连 + 存储过程 WHILE/CURSOR)两个目标都能满足且已实测通过, 但会失去"分片表建表只写一条"这个效果。
3. 迁移在业务 Pod 内执行,不加独立的迁移 Job
db-init-job 保留(只建库),应用启动时跑 Flyway 建表。理由:
- Flyway 用
GET_LOCK抢锁,多副本不会打架 - 默认 RollingUpdate 下新 Pod ready 前旧 Pod 不下线,迁移失败 = 升级卡住,不是服务中断
- 独立 Job 唯一买到的是"故障表现更清晰",是诊断体验差别而非可用性差别, 不值得在私有化交付里多塞一个组件
配套要求:services.yaml 显式写死 maxUnavailable: 0(现在等于 0 靠的是 "25% 向下取整"的巧合,replicas 涨到 4 就不成立了)。
4. 迁移文件用时间戳命名,不编码应用版本号
V<yyyyMMddHHmmss>__<描述>.sql。曾考虑过 V<交付版本>.<序号> 让脚本和发版对应, 否掉的理由见「理由」节。
5. 全量 SQL 由 CI 生成,是审计件不是安装主路径
db/schema/<db>.sql,由 gen-schema.sh 在临时 MySQL 上跑完全部迁移后 dump 得到, check-schema.sh 重新生成并 diff,对不上就构建失败。
用途是给客户 DBA 审阅、灾备参考。新客户的标准安装路径仍是"建空库 → 应用跑迁移"。
6. 基线补录:每仓一条时间戳基线,旧脚本删除
截至 2026-08-09 有 2 个客户环境运行。原方案是给 public / vem 加 V0.1__baseline.sql 排在旧脚本之前、配临时 out-of-order: true,以完全不碰客户库。
业主决定不做这层兼容 —— 只有 2 个客户,真出问题手工处理花不了多少时间, 不值得让脚本结构长期背一个兼容包袱。
于是:每仓只有一条 V20260809000000__baseline.sql,public 原有的 V1~V4 和 vem 的 V1 已删除、内容折叠进基线。代价是 public 的 2 个客户库升级前要各执行一次 DELETE FROM flyway_schema_history WHERE version IN ('1','2','3','4'), 否则 Flyway 会因"已应用的迁移在本地找不到对应文件"拒绝启动。
理由(为什么,包括否掉了哪些备选)
否掉:纯 JPA ddl-auto: update
它在"加一个带默认值的列"这类场景确实够用,而且这次的两次事故用它都不会发生。 但它做不到删列、改类型、改名、数据订正,也答不出"客户库是什么状态" —— 私有化交付里客户越多,最后一条越致命。这是决定性的,不是偏好问题。
否掉:迁移号编码应用版本(V1.2.0.1__xxx.sql)
- 版本号会撞车:两个分支各写一个
V7,合并时 Flyway 拒绝启动;手动改号后顺序悄悄错 - 迁移和发版不是 1:1,计划进 1.2.0 的改动延到 1.3.0 就要改文件名,而文件名发出去就不能改
- "客户在哪个版本"本来就该由
flyway_schema_history回答,信息量比版本号大
Rails / Django / Liquibase 全是时间戳或序列号,无一把应用版本编进迁移 ID。
否掉:全量 SQL 由人维护
第一版方案是另开一份 db/baseline/schema.sql 由 chart 的 db-init-job 导入。 被业主当场否掉:多了一份要手动跟着改表结构同步的文件,跟这次 Flyway 事故的病根 (脚本和实际执行结果对不齐)是同一类问题。改为 CI 生成 + diff 门禁。
否掉:重写客户库的 flyway_schema_history(Rails 式 squash)
更干净(history 形态统一),但要在 2 个生产客户库上做手工数据操作。 V0.1 + 临时 out-of-order 能达到同样效果且完全不碰客户库,收益不值那个风险。
否掉:独立迁移 Job
见「决策」第 3 条。这是教科书答案,但针对的是大规模多副本场景,本项目不是。
影响(约束了什么,谁要遵守)
全部 8 个 Java 服务仓,今后:
- 改实体必须同时写迁移脚本,否则服务启动失败(这是 validate 的设计意图,不是缺陷)
- 已发布的脚本永久冻结,写错了加新脚本修正
- 一个脚本只做一件事(MySQL DDL 非事务,回滚不了)
- 提交前必须在两种库上跑通:全新空库 + 当前生产结构副本
- 删列/改类型/改名必须拆两版走 expand-contract,保证"回滚上一版镜像"始终可行
切 validate 之前必须做漂移审计。 2026-08-09 对 8 个仓跑了一遍,结果是 4 个仓 0 缺口(已切 validate)、4 个仓有实打实的缺口(退回/保持 update)—— 其中 hiapi-cloud-store 的开发库被 nuwa 占用,一度生成出来的"基线"其实是 nuwa 的表结构。 逐仓结果与补齐步骤见主文档 §7 和各仓 db/migration/README.md 顶部。
审计只覆盖了开发库。客户库理论上也该各查一遍(update 只加不减,不同客户可能不同), 业主判断 2 个客户手工处理成本可接受,暂不阻塞发布。