CHARLIE SAYS

查理如是说
DATE 2026-08-24
THEME
SERIES / JHIPSTER / P-222 · JHipster 项目开发

JHipster 开发 07:数据库迁移——Liquibase 不是可选项

第 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 用的抽象类型(bigintvarchar)会由它翻译成目标数据库方言——H2 与 PostgreSQL 共用一份 changelog 的底气就在这。

增量 changeset 的铁律

已执行过的 changeset 永远不改。 Liquibase 以 id+author+路径 指纹识别 changeset,改了已执行文件的内容,轻则校验失败(checkSum 不匹配启动报错),重则环境间结构悄悄分叉。规范动作永远是追加新文件、新 changeset

  1. 模型变了先改 JDL,让生成器产 changelog(结构变更首选来源);
  2. 生成器覆盖不到的(数据订正、手工 DDL)自己写新文件,文件名带时间戳:20260824103000_add_fk_order_item.xml
  3. master.xml 末尾追加 include;
  4. 提交前本地空库验证一遍(见文末习惯清单)。

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.ymlliquibase.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;上线前空库演练兜底。

系列导航

← 集合源码 007:LinkedHashSet&Map源码解析 目录 开发安全 007:DDoS 详解 →
← 返回文章列表