第 6 篇里 JDL 每次增量更新都会产出 Liquibase changelog,这篇专门讲这套体系。标题不是修辞:我们接手过的每一个数据库结构混乱的项目,根因都是”改表不经版本管理”。Liquibase 在 JHipster 里不是可选插件,而是与代码同等地位的版本控制对象。
为什么不能直接改表
先看手工改表的经典死法:
| 场景 | 手工改表 | Liquibase 管理 |
|---|---|---|
| 同事的环境 | “你本地也要跑一下这几条 ALTER”(口口相传) | 启动应用自动同步 |
| 测试/生产环境 | DBA 拿着 Word 文档执行,漏一条没人知道 | changelog 即事实,执行记录在库 |
| 回滚 | 没有,或靠备份碰运气 | changeset 可配 rollback |
| 新环境搭建 | 全量 dump 导入,结构与代码版本对不上 | 空库起步,启动即追平 |
| 审计 | “这张表谁加的列?” | 每个 changeset 有 author 与时间 |
Liquibase 的核心机制是在数据库里维护一张表(JHipster 中是 DATABASECHANGELOG),记录每个 changeset 的 id、author、文件路径与执行状态。应用启动时对比”该执行的”与”已执行的”,只跑增量。这就是全环境一致性的来源。
JHipster 的集成结构
生成的迁移结构:
src/main/resources/config/liquibase/
├── master.xml # 入口,按顺序 include 所有 changelog
├── changelog/
│ ├── 00000000000000_initial.xml # 初始结构:用户/权限/审计表等
│ └── 20260824000000_added_entity_Customer.xml # JDL 增量生成的实体表
└── data/ # 种子数据(用户、权限、测试数据)
master.xml 的组织方式是线性的 include 列表:
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog" ...>
<include file="config/liquibase/changelog/00000000000000_initial.xml" relativeToChangelogFile="false"/>
<include file="config/liquibase/changelog/20260824000000_added_entity_Customer.xml" relativeToChangelogFile="false"/>
<!-- 新 changelog 按时间顺序追加在这里 -->
</databaseChangeLog>
一个 JDL 产出的 changeset 长这样(节选):
<changeSet id="20260824000000-1" author="jhipster">
<createTable tableName="customer">
<column name="id" type="bigint">
<constraints primaryKey="true" nullable="false"/>
</column>
<column name="name" type="varchar(100)">
<constraints nullable="false"/>
</column>
<column name="email" type="varchar(255)">
<constraints nullable="false" unique="true"/>
</column>
<column name="level" type="varchar(255)">
<constraints nullable="false"/>
</column>
</createTable>
</changeSet>
<changeSet id="20260824000000-2" author="jhipster">
<createIndex tableName="customer" indexName="idx_customer_email">
<column name="email"/>
</createIndex>
</changeSet>
注意 Liquibase 用的抽象类型(bigint、varchar)会由它翻译成目标数据库方言——H2 与 PostgreSQL 共用一份 changelog 的底气就在这。
增量 changeset 的铁律
已执行过的 changeset 永远不改。 Liquibase 以 id+author+路径 指纹识别 changeset,改了已执行文件的内容,轻则校验失败(checkSum 不匹配启动报错),重则环境间结构悄悄分叉。规范动作永远是追加新文件、新 changeset:
- 模型变了先改 JDL,让生成器产 changelog(结构变更首选来源);
- 生成器覆盖不到的(数据订正、手工 DDL)自己写新文件,文件名带时间戳:
20260824103000_add_fk_order_item.xml; - 在
master.xml末尾追加 include; - 提交前本地空库验证一遍(见文末习惯清单)。
diff changelog:手改实体后的逃生通道
有时你会先手改了 domain/ 里的实体(快速试验),回头要把结构变更固化。JHipster 集成了 Liquibase Maven 插件的 diff 能力:
# dev 环境:对比 JPA 元模型与 H2 实际结构,产出差异 changelog
./mvnw liquibase:diff
生成的 changelog 落在 target/liquibase/changelog/…,逐条人工审阅(diff 产物可能包含误删列、类型误判)后手工整理进正式 changelog。我们把它定位成”草稿生成器”,从不直接采用原始输出。
常见坑与解法
坑一:字段重命名被当成”删列 + 加列”
JDL 里把 name 改成 fullName 再生成,产出的 changeset 是 dropColumn + addColumn——生产环境一执行,数据没了。解法:重命名必须手工写 changeset:
<changeSet id="20260824110000-1" author="charlie">
<renameColumn tableName="customer"
oldColumnName="name" newColumnName="full_name"
columnDataType="varchar(100)"/>
</changeSet>
同时把 .jhipster/Customer.json 里的字段定义同步(或改 JDL 再生成、丢弃其 changelog 只留实体代码)。经验法则:涉及存量数据的结构变更,一律人工 review changelog 后再上生产。
坑二:加了 NOT NULL 列,老数据没默认值
空表随便加;有数据的表加 nullable=false 的列,先加可空列、回填、再收紧约束,分三个 changeset 走:
<changeSet id="20260824120000-1" author="charlie">
<addColumn tableName="customer">
<column name="level" type="varchar(20)"/>
</addColumn>
</changeSet>
<changeSet id="20260824120000-2" author="charlie">
<update tableName="customer">
<column name="level" value="BASIC"/>
<where>level is null</where>
</update>
</changeSet>
<changeSet id="20260824120000-3" author="charlie">
<addNotNullConstraint tableName="customer" columnName="level"
columnDataType="varchar(20)"/>
</changeSet>
数据回填(backfill)永远用 <update>/<sql> 写进 changeset,不搞”上线后跑个脚本”——脚本会丢,changeset 不会。
坑三:context 与 profile 的关系
changeset 可声明 context="production" 之类的运行环境标记,JHipster 启动时按 Spring profile 匹配(application-dev.yml 里 liquibase.contexts: dev, faker 之类的配置决定哪些 context 激活)。典型用法:种子测试数据只在 dev/faker context 下插入:
<changeSet id="20260824130000-1" author="charlie" context="dev">
<insert tableName="customer">…</insert>
</changeSet>
坑在于:忘了标 context 的测试数据上到了生产。上线前用 ./mvnw -Pprod 干净库做一次演练可提前暴露。
坑四:checkSum 报错
Validation Failed: checksum mismatch 出现,说明有人改了已执行的 changeset。定位到报错里的 changeset id,若改动无实际结构差异(改格式),用 liquibase:clear-checksums 谨慎重置;若有真实分叉,老实写补救 changeset 对齐环境。预防胜于治疗:changelog 文件的 code review 要像业务代码一样严格。
我们的上线习惯清单
# 1. 干净库全量演练(模拟全新环境)
docker run -d -p 5432:5432 -e POSTGRES_PASSWORD=x postgres:17
# 2. 指向空库启动 prod profile,确认全部 changeset 顺序执行无错
SPRING_DATASOURCE_URL=jdbc:postgresql://localhost:5432/fresh ./mvnw -Pprod
# 3. 检查执行记录条数与 master.xml include 数一致(psql 查 DATABASECHANGELOG)
# 4. 生产发布后再 tail 启动日志里的 Liquibase 段,确认 "successfully acquired change log lock"
小结
- Liquibase 靠
DATABASECHANGELOG表做增量执行,是全环境结构一致性的唯一保证,JHipster 里它不是可选项。 - 结构是
master.xml顺序 include changelog 文件;铁律是已执行的 changeset 永不修改,只追加。 ./mvnw liquibase:diff生成差异草稿,人工审阅后采用;JDL 生成的 changelog 也要过 review,尤其存量表。- 大坑三条:重命名要
renameColumn、非空列三步走、测试数据标 context;上线前空库演练兜底。
系列导航
- 上一篇:JDL 从入门到驯服
- 下一篇:安全体系(上)——认证
- 延伸阅读:技术栈全景图 · 安全体系(下)——授权