Appearance
数据库 Schema 管理规范
定稿 2026-08-09。适用于全部 8 个 Java 服务仓。 决策背景见 ADR-0010。
一句话
Flyway 是 schema 的唯一真相源;Hibernate 只做启动校验;迁移在业务 Pod 内执行(不加独立 Job)、 走 ShardingSphere 数据源(脚本只写逻辑表名);全量 SQL 是构建产物,不是维护对象。
1. 运行时形态
db-init-job (Helm post-install hook) → 只建库,不建表
↓
业务 Pod 启动
├─ Flyway migrate → 建表 / 加列(新客户从空库建全套,老客户跑增量)
└─ Hibernate ddl-auto: validate → 核对实体与表结构,对不上拒绝启动新客户装完 Helm 直接就有完整的表,老客户升级镜像自动跑增量。不需要额外的迁移 Job。
为什么迁移放在 Pod 里而不是独立 Job
教科书答案是独立 Job,那是给"大规模多副本"准备的。本项目的实际情况让它不划算:
- 多副本不会打架。 Flyway 在 MySQL 上用
GET_LOCK抢分布式锁,N 个 Pod 同时启动只有一个 真正执行,其余等它跑完再继续。 - 迁移失败不会中断服务。
services.yaml走 K8s 默认 RollingUpdate,新 Pod ready 之前旧 Pod 不下线 —— 迁移失败的后果是升级卡住、旧版本继续服务,不是服务中断。 - 建库/建表已经天然分成两段。
db-init-job只建库(Flyway 连的 URL 里带库名, 而且create-schemas: false),应用负责建表。再加一个 Job 是第三段,纯属多余。
Job 唯一真正买到的是"故障表现为一个 Job 红了,而不是新 Pod 起不来" —— 诊断体验的差别, 不是可用性的差别。为这一条在私有化交付里多塞一个组件(多一份镜像配置、多一个 Helm hook、 多一种客户现场没见过的失败形态),不值。
什么时候回头加 Job
只有一个触发条件:某条迁移的执行时间逼近 startupProbe 预算(当前 30 × 10s = 300 秒), 通常是几百万行的大表 ALTER。那时 Pod 会在迁移跑到一半被探针判死重启,反复执行半截脚本。
真到那天,做法也不是加 Job,而是那一条迁移走线下人工执行(gh-ost / pt-online-schema-change), Flyway 里只留一条标记。所以 Job 大概率永远不需要。
2. 必须的配置
各服务 application.yaml
yaml
spring:
flyway:
enabled: true
clean-disabled: true # 防止 flyway clean 把客户库清空
out-of-order: false # 不允许补跑漏掉的旧版本
validate-migration-naming: true
create-schemas: false # 见下
jpa:
hibernate:
ddl-auto: validate # Hibernate 一个字都不改库Flyway 用主数据源(ShardingSphere-JDBC 代理层),不要另配 spring.flyway.url。 这样迁移脚本只写逻辑表名,由 ShardingSphere 展开到全部物理分片 —— 见 §4。
2026-08-02 曾经因为一个空指针把 Flyway 绕成单独直连:
NullPointerException: ShardingSphereDatabase.getRuleMetaData() ... database is null。 当时判断是"Flyway 不能走 ShardingSphere"。2026-08-09 查清根因其实是框架ShardingConfig建数据源时没传 databaseName —— 逻辑库名取默认值logic_db, 而 Flyway 按连接的 catalog(真实库名)去找,当然找不到。传上真实库名后一切正常。 修复见hiapi-core-sharding-service的ShardingConfig#resolveDatabaseName。
create-schemas 仍必须是 false:Flyway 判断 schema 是否存在的元数据查询经过代理层 会失真(库真实存在也返回"不存在"),于是去 CREATE SCHEMA 撞 MySQL 1007 启动失败。 业务库一律由运维侧或 db-init-job 预先建好。
hiapi-chart/templates/services.yaml
yaml
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 0必须显式写死。 现在不写也是 0,靠的是"默认 25% 向下取整正好等于 0"这个巧合 —— replicas 调到 4 时 25% 就等于 1,K8s 会先干掉一个旧 Pod 再起新的,迁移一失败就真的少一个副本。
3. 迁移脚本命名:时间戳,不用版本号
V20260812143000__add_ice_selector_enabled.sql
V20260815091200__widen_order_no.sql格式 V<yyyyMMddHHmmss>__<小写下划线描述>.sql。
为什么不把应用版本号编进去(这条讨论过并明确否掉):
- 版本号会撞车。 两个分支各写一个
V7,合并时 Flyway 直接拒绝启动;手动改成V8后 执行顺序就悄悄错了。时间戳永远不撞。 - 迁移和发版不是 1:1。 计划进 1.2.0 的改动延到 1.3.0,版本号方案要改文件名 —— 而文件名一旦发出去就再也不能改。
- "客户在哪个版本"不该靠迁移号回答。
flyway_schema_history里有每条迁移的名字和执行时间, 信息量比一个版本号大得多;想知道对应哪次发版,用 git tag 反查。
Rails、Django、Liquibase 全是时间戳/序列号,没有一个把应用版本编进迁移 ID。
可重复迁移
视图、存储过程、基础字典这类"最终状态"的东西用 R__ 前缀(如 R__seed_config.sql), 内容变了 Flyway 自动重跑。不要写成一堆增量。
4. 基线
过去 schema 一直由 ddl-auto: update 首次启动时顺手创建,全仓从来没有过建表 DDL。 切 validate 之后 Hibernate 不再建表,所以每个仓都要补一条基线脚本: 全量 CREATE TABLE IF NOT EXISTS,把当时的完整表结构建出来。
V20260809000000__baseline.sql,一次性生成、之后永久冻结。
从实体生成,不要照库生成 —— 实体才是 validate 的判据,照库生成会把 ddl-auto: update 留下的漂移(缺表、孤儿表、孤儿列)一起固化。工具是 hiapi-chart/ci/lib/EntityDdl.java(不连库、不启服务),各仓的实测差异见 §8。 public / user / finance / vem 四仓的基线是 2026-08-09 照库生成的,已知带着少量 孤儿表/孤儿列 —— 已发布不再回头改,新增的仓一律从实体来。
| 行为 | |
|---|---|
| 新客户 | 空库靠它建出全部表 |
| 老客户 | 全部 IF NOT EXISTS 命中已存在 → 空跑,不改任何东西 |
分片表:写逻辑表名,不写物理分片
取模分片只写一条,由 ShardingSphere 展开:
sql
CREATE TABLE IF NOT EXISTS `hiapi_vem_device_slot` ( ... ); -- 实际建出 _0 _1 _2 _3依据必须是实体的 ShardingOption,不能按表名猜 —— 库里长得像分片的 _N 表未必 真有分片规则(vem 的 hiapi_vem_device_cargo_lane_0..3 就是删掉实体之后 ddl-auto: update 留下的孤儿表)。没有规则的展不开,只能保留物理表名,或者干脆不建。
按月分表只写裸表:月表由框架 ShardingTableTaskImpl 启动后 + 每天零点 CREATE TABLE ... LIKE 裸表 自动补,基线只提供那张模板。
不要写显式索引名。 ShardingSphere 改写 DDL 时会给约束名追加表名(而且会追加两遍), Hibernate 自动生成的 UKxxx 加完直接超 MySQL 64 字符上限: Identifier name 'UKk748...._hiapi_vem_app_version_hiapi_vem_app_version' is too long。
⚠️ 代价:基线不能再用 mysql < 直接跑 —— 直接跑只会建出一张 hiapi_vem_device_slot,不是 4 张分片。要给客户 DBA 看的完整物理结构, 用 db/schema/ 下 CI 生成的那份(它是跑完迁移之后 dump 出来的真实结果)。
分片表:_yyyyMM 月表不进基线(框架 ShardingTableTaskImpl 启动后 CREATE TABLE ... LIKE 裸表自动创建),只建裸的逻辑表当模板; 取模分片(_0.._N)没有自动建表逻辑,必须全部显式建。
⚠️ 手工列表会漏。vem 第一版基线是人工枚举实体列出来的 30 张表, 自动生成后是 37 张 —— 漏掉的 7 张里有
hiapi_vem_device_cargo_lane_0..3, 正是没有自动建表逻辑的取模分片,漏了新客户装机必炸。基线只能生成,不能手写。
存量客户:已发布脚本的处置
截至 2026-08-09 有 2 个客户环境在跑。原方案是加 V0.1__baseline.sql 排在旧脚本之前 配临时 out-of-order: true,以完全不碰客户库。业主决定不做这层兼容 —— 理由是只有 2 个客户,真出问题手工处理花不了多少时间,不值得让脚本结构长期背一个兼容包袱。
于是采用干净方案:每仓只有一条时间戳基线,hiapi-cloud-public 原有的 V1~V4 (以及 vem 的 V1)已删除,内容已折叠进基线。
代价是明确的:这 2 个客户库的 flyway_schema_history 里留着 V1~V4 的记录, 而文件已经不存在,Flyway 校验会报 Detected applied migration not resolved locally 拒绝启动。 升级前对 public 的 2 个客户库各执行一次:
sql
DELETE FROM flyway_schema_history WHERE version IN ('1','2','3','4');之后基线会以 IF NOT EXISTS 空跑一遍并记上账。其余仓不受影响 (它们此前没有任何迁移脚本,客户库里只有 baseline-on-migrate 打的 version 0 那一行, 新基线版本号更大,顺序执行、正常空跑)。vem 视其客户库里有无 version=1 的行而定 —— 那条脚本是 2026-08-06 那次语法失败的 V1,大概率从未发到客户。
5. 全量 SQL:生成物,不是安装主路径
<svc>-app/src/main/resources/db/
migration/ ← 增量脚本,唯一真相源,进 jar 参与运行时
V20260809000000__baseline.sql
V20260812143000__xxx.sql
R__seed_xxx.sql
schema/
hiapi_cloud_vem.sql ← CI 生成,勿手改;含建库 + 建表 + flyway 历史行用途是给客户 DBA 审阅("你要在我库里建什么,拿来看")、灾备参考、人肉排查时的对照基准。 新客户的标准安装路径仍然是"建空库 → 应用启动跑迁移",因为那条路径 CI 每次都在验证, 是唯一被持续测试的路径;dump 路径没人天天验证,属于"看起来省事的未测试路径"。
不保存历史版本的全量 SQL。 git checkout <tag> 出来的 schema/ 就是那个版本的全量, git tag 本身就是版本归档。
生成与门禁(2026-08-10 已建)
bash
hiapi-chart/ci/gen-schema.sh <服务目录> # 生成/更新
hiapi-chart/ci/gen-schema.sh --check <服务目录> # 重新生成并 diff,不一致就失败做的事:建临时库 <库名>_schemagen → 用服务自己的 ShardingConfig 建数据源 → 跑完 db/migration 全部迁移 → 逐表 SHOW CREATE TABLE 导出 → 删临时库。 引擎是 hiapi-chart/ci/lib/SchemaGen.java。
为什么必须走 ShardingConfig 而不是直连 + mysqldump:迁移脚本里分片表只写 逻辑表名,靠 SS 的 DDL 改写展开成 _0..3。直连跑同一份脚本只会建出一张同名表, 而且不报任何错 —— 导出来的"全量 SQL"是错的,还看不出来。
不用 mysqldump 的两个原因:免掉一个外部依赖(开发机普遍没装 mysql 客户端); SHOW CREATE TABLE 的输出天然干净,没有 dump 头。生成器另外剥掉了 AUTO_INCREMENT=<n> 和 flyway_schema_history —— 这两样每次都在变, 留着 diff 门禁就天天误报,等于废掉。
🔴 这份文件里不许出现 DROP TABLE,一条都不行。
2026-08-10 第一版生成器照抄了 mysqldump 的习惯,每张表前面加了 DROP TABLE IF EXISTS,还配了 SET FOREIGN_KEY_CHECKS = 0 —— 四个仓一共 291 条,已经提交并推上去了(业主发现后回滚)。
为什么这是严重错误,而不是风格问题:这个文件会被打进 jar 发到客户环境, 名字又叫"全量 schema",在私有化现场早晚有人手工执行它 —— 执行一次就是把客户那个库的数据全删了,而且 FOREIGN_KEY_CHECKS = 0 保证它一路删到底。 mysqldump 默认带 DROP 是因为它的用途是整库还原(先清空再灌); 这份文件的用途是给人看结构,两回事。
同理也不加 IF NOT EXISTS:在非空库上就该报 Table already exists 停下, 而不是建一半、或者让人误以为跑成功了。失败要响。
(唯一的运气是 Flyway 只扫 classpath:db/migration,db/schema/ 不在扫描路径里, 所以没有任何东西会自动执行它 —— 但"没被自动触发"不是安全,只是没赶上。)
"全量 SQL 也要跟着更新"必须是一条机器规则,不能是一条纪律。 靠人记得同步两份文件,第一次忘了就会让新客户装出一个和老客户不一样的库, 而且没有任何机制能发现。
⚠️ 它和 build.sh 里那 6 个门禁不一样:要连一个真 MySQL,不是纯静态零依赖。 所以没有挂进 build.sh。放夜间任务,或者改完迁移之后手动跑一次 --check。
另一个门禁:check-schema.sh(纯静态,已挂 build.sh)
和上面那个不是一回事,别混。它不连库,拦四类事:
| 规则 | 拦什么 | 为什么必须自动查 |
|---|---|---|
| ① 命名 | 不是 V<14位时间戳>__x.sql | 两个分支各写一个 V7,合并后 Flyway 拒绝启动 |
| ② 冻结 | 改/删已提交的迁移脚本 | 只有已经装过那个版本的客户会 checksum mismatch,本地和新客户全绿,测不出来 |
| ③ 配对 | 实体表结构变了却没新增迁移 | validate 模式下最常见的漏项,表现是服务起不来 |
| ④ 残留 | target/classes/db/migration/ 下有源码里已删的脚本 | 2026-08-09 真实事故,见 §7 |
| ⑤ 破坏性语句 | DROP / DELETE / TRUNCATE / FOREIGN_KEY_CHECKS = 0 | 2026-08-10 真实事故:生成器照抄 mysqldump 产出 291 条 DROP TABLE 并推送 |
③ 只比影响表结构的部分(@Table / @Column / 非 @Transient 字段声明), 改注释、加方法、调字段顺序都不报。②有豁免名单 ci/schema-baseline.txt, 每行必须写清客户库要执行什么,且只能变少。
⑤ 分两档:db/schema/ 零容忍(它是生成物,只该有 CREATE TABLE); db/migration/ 里 DROP TABLE / TRUNCATE / 无 WHERE 的 DELETE 不接受豁免, 而 DROP COLUMN、带 WHERE 的 DELETE、UPDATE 有正当用途(expand-contract 收尾就必须删列),要在同一行或上一行写明理由:
sql
-- hi-allow-destructive: 收尾 expand-contract,该列已停写两个版本
ALTER TABLE hiapi_xxx DROP COLUMN yyy;和 check-widget.sh 的 hi-allow-color 是同一套约定。已冻结改不动的老脚本走 schema-baseline.txt 的 destructive: 名单(当前 4 行,都是 FOREIGN_KEY_CHECKS = 0, 只能变少)。
6. 五条纪律
① 已发布的脚本永久冻结
改一个字符 checksum 就变,客户库启动即失败。写错了加一条新脚本修正,绝不回头改。
② .sql 脚本写裸 DDL,不要写守卫;真需要幂等就写 Java 迁移
守卫(先查 information_schema 再决定要不要执行)在 .sql 脚本里跑不通 —— 只要它经过 ShardingSphere 的 SQL 解析器就会被拒。三种写法 2026-08-09/10 实测:
| 写法 | 报错 |
|---|---|
SET @ddl = (SELECT ...) + PREPARE/EXECUTE | Flyway NullPointerException: resultSet is null |
SELECT IF(...) INTO @ddl FROM ... | SQLFederationUnsupportedSQLException |
存储过程 CREATE PROCEDURE + CALL | UnsupportedShardingOperationException: Can not support operation 'CREATE PROCEDURE' with sharding table 'xxx' |
根因:框架 ShardingConfig 给每个实体都注册了表规则(非分片表走 else 分支也注册), 所以几乎所有表都在 ShardingSphere 管辖内。
所以 .sql 脚本的幂等只靠 Flyway 自身的「每条脚本只执行一次」。 直接后果:目标库里如果已经有那一列,手工重跑脚本会以 Duplicate column name 失败 —— 写加列脚本前要确认目标库没有该列(开发库常因排障手工加过)。
确实需要"可重复执行"时:用 Flyway 的 Java 迁移
上面三种写法的共同点是把判断交给 MySQL 去做,于是必然经过 SS 的解析器。 Java 迁移把判断挪到 JVM 里,只把最终的 DDL 交给 SS —— 两个目标就都能满足了。 2026-08-10 实测通过:
java
// <app模块>/src/main/java/.../migration/V20260810120000__add_price.java
public class V20260810120000__add_price extends BaseJavaMigration {
@Override public boolean canExecuteInTransaction() { return false; } // MySQL DDL 非事务
@Override public void migrate(Context ctx) throws Exception {
if (hasColumn(ctx, "hiapi_vem_device_slot", "price")) return;
try (Statement s = ctx.getConnection().createStatement()) {
// 逻辑表名,由 SS 展开到全部分片 —— 和 .sql 脚本里一样
s.execute("ALTER TABLE hiapi_vem_device_slot ADD COLUMN price INT NOT NULL DEFAULT 0");
}
}
/** 关键:用 SHOW COLUMNS,它能穿过 SS(路由到 0 号分片),不用另开裸连接、不用凭据。 */
private boolean hasColumn(Context ctx, String table, String column) throws SQLException {
try (Statement s = ctx.getConnection().createStatement();
ResultSet r = s.executeQuery("SHOW COLUMNS FROM " + table)) {
while (r.next()) if (column.equalsIgnoreCase(r.getString(1))) return true;
return false;
}
}
}实测结论(ss_jm3 / ss_show_test 两个 scratch 库):
- 一条逻辑
CREATE TABLE→ 建出 4 张物理分片 ✓ - 带守卫的
ALTER重复执行 → 第二次正确跳过,不报错、列不重复 ✓ SHOW COLUMNS FROM <逻辑表名>穿过 SS ✓(SHOW INDEX也可以)- ⚠️
SHOW TABLES LIKE '<逻辑表名>'返回 0 行 —— 物理表叫_0..3,判断建表要用LIKE 'xxx%' - ⚠️
CREATE TABLE IF NOT EXISTS穿过 SS 不幂等:SS 元数据里已有这张表时直接抛TableExistsException,IF NOT EXISTS不起作用。建表也要自己先判断。 - 代价:每条 DDL 之后 SS 要刷新元数据,单条约 30 秒。迁移场景可以接受,但别写几十条。
默认仍然写 .sql —— DBA 能读、能审、能对着客户库比对,这是私有化交付里的硬需求。 只有确实需要重复执行的场景(改客户库时不确定当前状态)才换 Java 迁移;两者可以在 同一个 flyway_schema_history 里混用。
2026-08-10:业主在知道"
.sql写不了守卫"这个代价的情况下选择保留 ShardingSphere 路线(换来"分片表建表只写一条")。当时判断的备选是 Flyway 直连 + 存储过程 (两个目标都满足,但失去分片自动展开)。Java 迁移这条路当时没试过 —— 它比存储过程那条更好:分片展开和守卫都不丢。
③ 一个脚本只做一件事
MySQL 的 DDL 不是事务性的,Flyway 回滚不了。一个脚本里塞三条 ALTER,第二条挂了就留下 一个"改了一半、说不清是什么状态"的库。拆开写,失败时至少能一眼看出停在哪。
④ 提交前必须在两种库上跑通
全新空库 + 当前生产结构的副本。
这条是五条里最重要的。因为迁移在 Pod 里跑,最痛的失败模式不是"这次升级失败",而是 失败记录留在 flyway_schema_history 里,之后每次启动都被拒绝 (Detected failed migration)—— 2026-08-06 vem 加 ice_selector_enabled 就是这个, 一次语法错被放大成"服务再也起不来",得手工 DELETE FROM flyway_schema_history WHERE version=... AND success=0 才能恢复。
这个风险独立 Job 方案也免疫不了,唯一的解就是别让坏脚本出门。
⑤ 破坏性变更走 expand-contract,拆两版
加列/加索引这类只增不减的改动,老代码在新 schema 上照常跑,滚动升级期间新旧 Pod 并存没问题。 删列、改类型、改名不行,必须拆开:
版本 N : 加新列 + 双写 + 回填历史数据(老列还在,老代码还能跑)
版本 N+1 : 代码只读新列
版本 N+2 : 删掉老列这样任何一步失败都能直接回滚上一版镜像 —— 因为老版本代码在新 schema 上仍然能跑。 一步到位的 rename 一旦出问题,只能停机手工救。
Flyway 社区版没有 undo,而且就算有也不该用:生产 schema 从不回滚,只往前修。
7. 切 validate 前必须做漂移审计
ddl-auto: update 只加不减,不同服务、不同时间点跑出来的结构可能和实体对不上。 所以切 validate 之前必须逐表核对实体要求的表/列与库里实际有的表/列。
方法:用 Hibernate 自己的 MetadataSources 构建实体模型(不连库),再和 information_schema 比对。分片实体的逻辑表在直连库里不存在,要按 裸表 → _0 → 最近的 _yyyyMM 依次找物理表再比列名。 框架 jar 里的实体也要扫(hiapi_core_* 那些共享表),只审计仓内实体会漏。
不直接用
hbm2ddl.auto=validate:它遇到第一个不一致就抛异常, 而分片实体的逻辑表会淹没在误报里,看不到真正的问题。
2026-08-09 审计结果
| 仓 | 结果 | 处置 |
|---|---|---|
| public | 0 缺表 0 缺列 ✓ | 已切 validate |
| user | 0 缺表 0 缺列 ✓ | 已切 validate |
| finance | 0 缺表 0 缺列 ✓ | 已切 validate |
| vem | 0 缺表 0 缺列 ✓ | 已切 validate |
| sy | 缺 2 张表 | 退回 update |
| task-worker | 缺 2 张表 + 2 列 | 退回 update |
| store | 15 个实体的表一张都不存在 | 退回 update,基线已删 |
| shop | 库里一张表都没有,无法生成基线 | 保持 update |
后 4 个仓的具体缺口和补齐步骤写在各自 db/migration/README.md 顶部。
store 的问题最值得记一笔:它的开发库 hiapi_cloud_store 被 nuwa(Go 应用市场 / 交付中心)占用了 —— 库里是 apps / lic_* / dep_* / developers, 不是 store 实体要的 hiapi_core_store_*。按其余仓同样方式生成出来的那份"基线" 其实是 nuwa 的表结构,已删除。留着的话新客户装 store 会建出一堆 nuwa 的表, 而 store 自己的表一张都没有 —— 而且不报任何错。补齐前要先解决库名冲突。
sy / task-worker 的缺口性质相同:实体改过之后服务一直没重启,update 还没来得及补建。 补齐方式是启一次服务让 update 补上,再重新生成基线、重跑审计。
8. 进度
已完成(2026-08-09 ~ 08-10)
- public / user / finance / vem 四仓:基线生成 → 漂移审计 0 缺口 → 切
validate - 框架
ShardingConfig传真实库名,Flyway 改走 ShardingSphere 数据源; 四仓基线改用逻辑表名,语句数 54/119/62/37 → 43/58/52/30 - 每仓都做过端到端验证:空库跑完迁移后的物理表集合与现有库逐张一致
- vem 另加
V20260809010000__add_ice_selector_enabled_to_device_type.sql, 并验证过老客户路径(表都在、无该列 → 基线空跑、ALTER 补上) hiapi-chart/templates/services.yaml显式写死maxUnavailable: 0- 8 仓
db/migration/README.md统一 check-schema.sh(纯静态四条规则)+schema-baseline.txt豁免名单,已挂build.sh; 四条规则各做过正反测试,含"只改注释不误报"gen-schema.sh+lib/SchemaGen.java,四个validate仓都已生成db/schema/<库名>.sql(vem 41 / public 56 / user 125 / finance 69 张物理表);--check模式连跑两次结果一致,可以直接当 diff 门禁用lib/EntityDdl.java:从实体导 DDL,不连库不启服务 —— 这把 sy / task-worker / shop / store 四仓"卡在 Nacos 密码"的阻塞整个消掉了- sy / task-worker / shop 三仓改用实体生成的基线并切
validate, 各自在临时库上跑通 + 实体↔库比对 0 缺口(7 / 3 / 18 张表) - store 基线也生成并验过(8 张表 0 缺口),但仍是
update—— 卡的是库名归属
基线该从库生成还是从实体生成
一开始是照开发库 SHOW CREATE TABLE 生成的。2026-08-10 换成从实体生成, 因为实体才是 validate 的判据,照库生成只会把 ddl-auto: update 留下的漂移 一起固化。实测出来的差异不是理论问题:
| 仓 | 照库生成会错在哪 |
|---|---|
| sy | 少 hiapi_core_sy_app_upgrade / hiapi_core_sender_record(实体改了没重启),多 hiapi_core_thirdparty_access_token(全仓已无对应实体的孤儿表) |
| task-worker | 只有 1 张表;少 hiapi_task_worker_retry / hiapi_cloud_app_install,还少 2 列 |
| shop / store | 库是空的 / 被 nuwa 占着,根本没法生成 |
| vem(已发布的那份) | 多 1 张孤儿表 hiapi_vem_template_version + device_info 的 3 个孤儿列 |
曾经担心"从实体导出会漏掉框架带进来的共享表"。核对方式:把该服务 runtime classpath 上每个 hiapi-*.jar 解开、逐个 class 查 @Entity,和导出的表数对账 —— shop 上核过,18 个实体对 18 张表,一个不差。hiapi-core-upload 那些模块 不在 shop 的依赖里,那些表本来就不该出现在它的库里。
待办
| 事项 | 阻塞在哪 |
|---|---|
vem 部署前开发库执行 ALTER TABLE hiapi_vem_device_type DROP COLUMN ice_selector_enabled | 该列是排障时手工加的,不删会撞 Duplicate column name |
public 的 2 个客户库执行 DELETE FROM flyway_schema_history WHERE version IN ('1','2','3','4') | 需要客户库访问,升级前做 |
| sy 开发库手工补 2 张表、task-worker 手工补 2 张表 + 2 列 | baseline-on-migrate 在非空库上会跳过基线脚本,存量库拿不到 |
store 定库名归属(store 换还是 nuwa 换),改三个 profile 的 jdbc url 后切 validate | 只能由人决定;基线已就绪,不用动 |
gen-schema.sh --check 挂进夜间任务;sy/shop/store/task-worker 生成 db/schema/ | 要一台常驻 MySQL,不能进 build.sh |
框架 hiapi-fast-frame 发版 | ShardingConfig 传 databaseName 的改动只在本机 ~/.m2,CI 和别人的机器拿不到 |
附:为什么没有"幂等 DDL 写法"这一节
其它 MySQL 项目常见的做法是先查 information_schema 再决定要不要执行 (ADD COLUMN IF NOT EXISTS 是 MariaDB 语法,MySQL 不支持)。
本项目在 .sql 脚本里用不了,原因和实测证据见 §6 ②。.sql 加列一律写裸 ALTER TABLE。
真需要幂等的场景改用 Java 迁移(守卫在 JVM 里做、DDL 交给 SS 展开,2026-08-10 实测通过), 写法见 §6 ②。那是目前唯一"分片自动展开 + 可重复执行"两头都占的办法。
如果哪天改回 Flyway 直连,可用的写法是存储过程(已实测通过):
sql
DROP PROCEDURE IF EXISTS hiapi_addcol;
DELIMITER $$
CREATE PROCEDURE hiapi_addcol() BEGIN
IF NOT EXISTS(SELECT * FROM information_schema.columns
WHERE table_schema = DATABASE() AND table_name = 'xxx' AND column_name = 'yyy')
THEN ALTER TABLE `xxx` ADD COLUMN `yyy` ...;
END IF;
END $$
DELIMITER ;
CALL hiapi_addcol();
DROP PROCEDURE hiapi_addcol;分片表用 CURSOR 遍历 information_schema.TABLES 按前缀逐张处理; 建分片表用 WHILE 循环 + CONCAT,建表定义仍然只写一次。