Liquibase 使用说明
sz-admin 使用 Liquibase 管理数据库表结构和初始化数据。v2.0.0 起,数据库脚本随模块拆分:启动服务只保留聚合入口,具体表结构和初始化数据由
sz-module-admin、sz-module-audit、sz-module-generator等业务模块维护。
为什么使用 Liquibase
- 版本可追溯:每个
changeSet都有唯一id,Liquibase 通过DATABASECHANGELOG表记录执行历史,不会重复执行。 - 模块化维护:服务入口聚合模块级 changelog,模块内部再按
framework、demo、generator等能力分层。 - 多数据库兼容:需要同时兼容 MySQL 和 PostgreSQL 的模块,可通过
<property dbms="...">和changeSet dbms="..."维护差异。 - 环境幂等:通过
<preConditions onFail="MARK_RAN">避免已有对象重复创建。 - 可选执行:通过
spring.liquibase.enabled控制是否在启动时自动执行,生产环境可关闭后人工审核脚本。
当前目录结构
v2.0.0 当前实际入口如下:
sz-service/sz-service-admin/
└── src/main/resources/
└── db/changelog/
└── changelog-master.xml # 服务级聚合入口
sz-module/sz-module-admin/
└── src/main/resources/
└── db/changelog/
├── module-admin-changelog.xml # admin 模块入口
├── framework/
│ ├── changelog-master.xml # framework 分层入口
│ └── 2.0.0/
│ ├── 001_framework_init.xml # framework 2.0.0 清单
│ └── tables/
│ ├── 000_public_type_def.xml
│ ├── 001_sys_client.xml
│ ├── 002_sys_config.xml
│ ├── ...
│ └── 025_sys_dict_source.xml
└── demo/
├── changelog-master.xml # demo 分层入口
└── 2.0.0/
├── 001_demo_init.xml # demo 2.0.0 清单
└── tables/
├── 001_sys_user_dept.xml
├── ...
└── 015_teacher_statistics.xml
sz-module/sz-module-generator/
└── src/main/resources/
└── db/changelog/
├── module-generator-changelog.xml # generator 模块入口
└── generator/
├── changelog-master.xml # generator 分层入口
└── 2.0.0/
├── 001_generator_init.xml # generator 2.0.0 清单
└── tables/
├── 001_generator_table.xml
└── 002_generator_table_column.xml入口关系
服务级入口位于 sz-service-admin:
<!-- sz-service/sz-service-admin/src/main/resources/db/changelog/changelog-master.xml -->
<databaseChangeLog ...>
<include file="db/changelog/module-admin-changelog.xml"/>
<include file="db/changelog/module-generator-changelog.xml"/>
</databaseChangeLog>module-admin-changelog.xml 聚合 admin 模块内的 framework 与 demo:
<databaseChangeLog ...>
<include file="framework/2.0.0/001_framework_init.xml" relativeToChangelogFile="true"/>
<include file="demo/2.0.0/001_demo_init.xml" relativeToChangelogFile="true"/>
</databaseChangeLog>module-generator-changelog.xml 聚合代码生成器模块:
<databaseChangeLog ...>
<include file="generator/2.0.0/001_generator_init.xml" relativeToChangelogFile="true"/>
</databaseChangeLog>NOTE
framework/changelog-master.xml、demo/changelog-master.xml、generator/changelog-master.xml 仍保留为分层入口,便于独立维护或后续按模块拆分引用;当前服务主链路直接通过模块级 changelog 引入具体版本清单。
分层说明
| 层级 | 所在模块 | 用途 | 是否必须 |
|---|---|---|---|
framework/ | sz-module-admin | 框架核心表结构与必要初始化数据,例如客户端、配置、字典、菜单、角色、用户等 | 必须 |
demo/ | sz-module-admin | 演示/测试数据,例如示例用户、示例部门、教师统计数据等 | 可选,生产环境可按需注释 |
generator/ | sz-module-generator | 代码生成器表结构,例如 generator_table、generator_table_column | 可选,不使用生成器时可注释模块入口 |
可选模块关闭
生产环境通常至少保留 framework。如果不需要 demo 或 generator,可在服务级或模块级入口注释对应 include。
<!-- 服务级入口:不需要代码生成器时可注释 -->
<include file="db/changelog/module-admin-changelog.xml"/>
<!-- <include file="db/changelog/module-generator-changelog.xml"/> --><!-- admin 模块入口:生产环境不需要演示数据时可注释 -->
<include file="framework/2.0.0/001_framework_init.xml" relativeToChangelogFile="true"/>
<!-- <include file="demo/2.0.0/001_demo_init.xml" relativeToChangelogFile="true"/> -->WARNING
已经在某个数据库执行过的 changeSet 不建议修改 id、author 或文件路径,否则 Liquibase 可能将其识别为新的变更集。需要变更结构时,应追加新的 changeSet。
执行控制
Liquibase 配置在数据库配置文件中,例如 mysql.yml 或 postgresql.yml:
spring:
liquibase:
change-log: classpath:db/changelog/changelog-master.xml
enabled: true
database-change-log-lock-table: databasechangeloglock
database-change-log-table: databasechangelog| 配置项 | 说明 |
|---|---|
change-log | 服务级入口,当前为 classpath:db/changelog/changelog-master.xml |
enabled: true | 应用启动时自动检测并执行未执行的 changeSet |
enabled: false | 不自动执行,适合生产环境人工审核后手动执行 |
database-change-log-table | Liquibase 执行记录表 |
database-change-log-lock-table | Liquibase 执行锁表 |
类型变量
需要同时兼容 MySQL 和 PostgreSQL 的 changelog,可在 XML 头部声明类型变量,Liquibase 会按当前数据库自动选择:
<property name="bool.type" value="CHAR(1)" dbms="mysql"/>
<property name="bool.type" value="VARCHAR(1)" dbms="postgresql"/>
<property name="datetime.type" value="DATETIME" dbms="mysql"/>
<property name="datetime.type" value="TIMESTAMP" dbms="postgresql"/>使用示例:
<column name="del_flag" type="${bool.type}" defaultValue="F" remarks="删除标志"/>
<column name="create_time" type="${datetime.type}" remarks="创建时间"/>这类字段可以用一个 changeSet 同时覆盖 MySQL 和 PostgreSQL。若项目已固定目标数据库,也可以直接写目标库类型。
changeSet 命名规范
推荐格式:
{scope}-{operation}-{table-name}-{yyyyMMddHHmm}| 部分 | 说明 | 示例 |
|---|---|---|
scope | 数据层级或模块,如 framework、demo、generator | framework |
operation | struct、init、data、alter 等 | struct |
table-name | 表名,用连字符分隔 | sys-client |
| 时间 | 精确到分钟,同分钟多个 changeSet 可追加序号 | 202605100838 |
示例:
framework-struct-sys-client-202605100838
framework-init-sys-client-202605100838
generator-struct-generator-table-202605100838
demo-data-teacher-statistics-202605100838preConditions 用法
所有结构类 changeSet 建议添加 <preConditions onFail="MARK_RAN">,避免已有数据库重复执行时报错。
建表前检查表是否存在:
<changeSet id="framework-struct-sys-dept-202605100838" author="sz">
<preConditions onFail="MARK_RAN">
<not><tableExists tableName="sys_dept"/></not>
</preConditions>
<createTable tableName="sys_dept" remarks="部门表">
...
</createTable>
</changeSet>插入数据前检查记录是否存在:
<changeSet id="framework-init-sys-client-202605100838" author="sz">
<preConditions onFail="MARK_RAN">
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM sys_client</sqlCheck>
</preConditions>
<insert tableName="sys_client">
...
</insert>
</changeSet>onFail 值 | 行为 |
|---|---|
MARK_RAN | 条件不满足时跳过该 changeSet,并在 DATABASECHANGELOG 中标记为已执行 |
HALT | 条件不满足时抛出异常,停止后续执行 |
CONTINUE | 条件不满足时跳过该 changeSet,但不标记为已执行 |
双 dbms 写法
当某个字段类型在两种数据库中存在无法通过变量统一的差异时,例如 dept_scope 或大文本,是否双写取决于兼容目标:
- 固定只支持 MySQL 的业务项目:直接使用 MySQL 类型即可,不需要 PostgreSQL 版 changeSet。
- 固定只支持 PostgreSQL 的业务项目:直接使用 PostgreSQL 类型即可,不需要 MySQL 版 changeSet。
- 需要同时兼容 MySQL + PostgreSQL 的官方模块、插件模块或可复用模块:使用
dbms="mysql"/dbms="postgresql"分别维护两个 changeSet。
双库兼容示例:
<!-- MySQL 版:dept_scope=JSON,大文本=MEDIUMTEXT -->
<changeSet id="demo-struct-your-table-202606010900" author="sz" dbms="mysql">
<preConditions onFail="MARK_RAN">
<not><tableExists tableName="your_table"/></not>
</preConditions>
<createTable tableName="your_table" remarks="你的表">
<column name="id" type="BIGINT" remarks="ID"><constraints primaryKey="true"/></column>
<column name="name" type="VARCHAR(64)" remarks="名称"/>
<column name="del_flag" type="${bool.type}" defaultValue="F" remarks="删除标志"/>
<column name="create_time" type="${datetime.type}" remarks="创建时间"/>
<column name="dept_scope" type="JSON" remarks="部门范围(数据权限)"/>
<column name="content" type="MEDIUMTEXT" remarks="正文内容"/>
</createTable>
</changeSet>
<!-- PostgreSQL 版:dept_scope=BIGINT[],大文本=TEXT -->
<changeSet id="demo-struct-your-table-pg-202606010900" author="sz" dbms="postgresql">
<preConditions onFail="MARK_RAN">
<not><tableExists tableName="your_table"/></not>
</preConditions>
<createTable tableName="your_table" remarks="你的表">
<column name="id" type="BIGINT" remarks="ID"><constraints primaryKey="true"/></column>
<column name="name" type="VARCHAR(64)" remarks="名称"/>
<column name="del_flag" type="${bool.type}" defaultValue="F" remarks="删除标志"/>
<column name="create_time" type="${datetime.type}" remarks="创建时间"/>
<column name="dept_scope" type="BIGINT[]" remarks="部门范围(数据权限)"/>
<column name="content" type="TEXT" remarks="正文内容"/>
</createTable>
</changeSet>判断原则:
- 固定目标数据库为 MySQL 或 PostgreSQL:按目标库写单个
changeSet即可。 - 字段类型可以用
${bool.type}或${datetime.type}表达:单个changeSet即可兼容双库。 - 同时兼容双库,且字段是
dept_scope或大文本:需要按dbms双写。 VARCHAR、INT、BIGINT、DECIMAL等通用类型:通常单个changeSet即可。
初始化数据写法
初始化数据优先使用 <insert> 标签。数值类型字段用 valueNumeric,字符串字段用 value,日期字段用 valueDate:
<changeSet id="framework-init-sys-client-202605100838" author="sz">
<preConditions onFail="MARK_RAN">
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM sys_client WHERE client_id = '195da9fcce574852b850068771cde034'</sqlCheck>
</preConditions>
<insert tableName="sys_client">
<column name="client_id" value="195da9fcce574852b850068771cde034"/>
<column name="client_key" value="sz-admin"/>
<column name="active_timeout" valueNumeric="86400"/>
<column name="timeout" valueNumeric="604800"/>
<column name="del_flag" value="F"/>
<column name="create_id" valueNumeric="1"/>
<column name="create_time" valueDate="2024-01-22 13:43:51"/>
<column name="is_lock" value="T"/>
</insert>
</changeSet>NOTE
初始化数据本身通常不需要 dbms 区分。只有表结构或 SQL 函数依赖特定数据库能力时,才需要按数据库拆分。
已执行记录查询
Liquibase 执行历史保存在 DATABASECHANGELOG 表中:
SELECT id, author, filename, dateexecuted, exectype
FROM DATABASECHANGELOG
ORDER BY dateexecuted;
SELECT id, author, exectype
FROM DATABASECHANGELOG
WHERE exectype = 'MARK_RAN';exectype | 含义 |
|---|---|
EXECUTED | 正常执行完成 |
MARK_RAN | 因 preConditions 不满足而被标记跳过 |
RERAN | 设置 runAlways="true" 的 changeSet 被重新执行 |
FAILED | 执行失败 |
