Skip to content

数据库 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,那是给"大规模多副本"准备的。本项目的实际情况让它不划算:

  1. 多副本不会打架。 Flyway 在 MySQL 上用 GET_LOCK 抢分布式锁,N 个 Pod 同时启动只有一个 真正执行,其余等它跑完再继续。
  2. 迁移失败不会中断服务。 services.yaml 走 K8s 默认 RollingUpdate,新 Pod ready 之前旧 Pod 不下线 —— 迁移失败的后果是升级卡住、旧版本继续服务,不是服务中断。
  3. 建库/建表已经天然分成两段。 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-serviceShardingConfig#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 = 02026-08-10 真实事故:生成器照抄 mysqldump 产出 291 条 DROP TABLE 并推送

③ 只比影响表结构的部分(@Table / @Column / 非 @Transient 字段声明), 改注释、加方法、调字段顺序都不报。②有豁免名单 ci/schema-baseline.txt, 每行必须写清客户库要执行什么,且只能变少

⑤ 分两档:db/schema/ 零容忍(它是生成物,只该有 CREATE TABLE); db/migration/DROP TABLE / TRUNCATE / 无 WHEREDELETE 不接受豁免, 而 DROP COLUMN、带 WHEREDELETEUPDATE 有正当用途(expand-contract 收尾就必须删列),要在同一行或上一行写明理由:

sql
-- hi-allow-destructive: 收尾 expand-contract,该列已停写两个版本
ALTER TABLE hiapi_xxx DROP COLUMN yyy;

check-widget.shhi-allow-color 是同一套约定。已冻结改不动的老脚本走 schema-baseline.txtdestructive: 名单(当前 4 行,都是 FOREIGN_KEY_CHECKS = 0, 只能变少)。


6. 五条纪律

① 已发布的脚本永久冻结

改一个字符 checksum 就变,客户库启动即失败。写错了加一条新脚本修正,绝不回头改。

.sql 脚本写裸 DDL,不要写守卫;真需要幂等就写 Java 迁移

守卫(先查 information_schema 再决定要不要执行).sql 脚本里跑不通 —— 只要它经过 ShardingSphere 的 SQL 解析器就会被拒。三种写法 2026-08-09/10 实测:

写法报错
SET @ddl = (SELECT ...) + PREPARE/EXECUTEFlyway NullPointerException: resultSet is null
SELECT IF(...) INTO @ddl FROM ...SQLFederationUnsupportedSQLException
存储过程 CREATE PROCEDURE + CALLUnsupportedShardingOperationException: 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 审计结果

结果处置
public0 缺表 0 缺列 ✓已切 validate
user0 缺表 0 缺列 ✓已切 validate
finance0 缺表 0 缺列 ✓已切 validate
vem0 缺表 0 缺列 ✓已切 validate
sy缺 2 张表退回 update
task-worker缺 2 张表 + 2 列退回 update
store15 个实体的表一张都不存在退回 update,基线已删
shop库里一张表都没有,无法生成基线保持 update

后 4 个仓的具体缺口和补齐步骤写在各自 db/migration/README.md 顶部。

store 的问题最值得记一笔:它的开发库 hiapi_cloud_storenuwa(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 留下的漂移 一起固化。实测出来的差异不是理论问题:

照库生成会错在哪
syhiapi_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,建表定义仍然只写一次。