【Kotlin + Spring Boot 4 从零到架构师】第 22 篇:数据库迁移 --- Liquibase
本系列定位:零基础入门,从 Kotlin 语法一路到 Spring Boot 4 高级架构(DDD + Modulith),适合 Java 开发者转型,也适合纯新手系统学习。
本篇你将学到
- 为什么需要数据库迁移工具
- Liquibase 的核心概念:changelog、changeset、precondition
- YAML 格式 changelog 编写
- 常用变更操作:建表、加列、加索引、加外键
- 与 Spring Boot 集成
学完本篇,你将能为 mini-shop 建立可追溯、可回滚、团队协作的数据库版本管理体系。
一、为什么需要数据库迁移
1.1 ddl-auto 的致命问题
前面我们用 ddl-auto: update 让 Hibernate 自动建表,这在开发阶段很方便,但在生产环境有严重隐患:
| 问题 | 说明 |
|---|---|
| 不记录变更历史 | 谁在什么时候改了什么表?无法追溯 |
| 不能删列/改类型 | Hibernate 只会加表加列,不会删或改类型 |
| 无版本管理 | 没法知道数据库当前处于哪个版本 |
| 团队协作困难 | 多人同时改实体,表结构不同步 |
| 生产环境不可控 | 自动执行 DDL 是高危操作 |
1.2 Liquibase 解决方案
Liquibase 是一个数据库版本控制工具,把数据库结构变更当作「代码」来管理:
代码版本管理:Git 管理代码变更
数据库版本管理:Liquibase 管理表结构变更
核心能力:
- 每次变更都记录在文件中,可追溯
- 按顺序执行变更,已执行的不会重复
- 支持回滚
- 支持多环境(开发→测试→生产)
- 团队协作友好------变更文件纳入 Git
下面用一张流程图对比两种方案:
#mermaid-svg-ECchi2GWl2JthlyX{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-ECchi2GWl2JthlyX .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-ECchi2GWl2JthlyX .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-ECchi2GWl2JthlyX .error-icon{fill:#552222;}#mermaid-svg-ECchi2GWl2JthlyX .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-ECchi2GWl2JthlyX .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-ECchi2GWl2JthlyX .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-ECchi2GWl2JthlyX .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-ECchi2GWl2JthlyX .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-ECchi2GWl2JthlyX .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-ECchi2GWl2JthlyX .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-ECchi2GWl2JthlyX .marker{fill:#333333;stroke:#333333;}#mermaid-svg-ECchi2GWl2JthlyX .marker.cross{stroke:#333333;}#mermaid-svg-ECchi2GWl2JthlyX svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-ECchi2GWl2JthlyX p{margin:0;}#mermaid-svg-ECchi2GWl2JthlyX .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-ECchi2GWl2JthlyX .cluster-label text{fill:#333;}#mermaid-svg-ECchi2GWl2JthlyX .cluster-label span{color:#333;}#mermaid-svg-ECchi2GWl2JthlyX .cluster-label span p{background-color:transparent;}#mermaid-svg-ECchi2GWl2JthlyX .label text,#mermaid-svg-ECchi2GWl2JthlyX span{fill:#333;color:#333;}#mermaid-svg-ECchi2GWl2JthlyX .node rect,#mermaid-svg-ECchi2GWl2JthlyX .node circle,#mermaid-svg-ECchi2GWl2JthlyX .node ellipse,#mermaid-svg-ECchi2GWl2JthlyX .node polygon,#mermaid-svg-ECchi2GWl2JthlyX .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-ECchi2GWl2JthlyX .rough-node .label text,#mermaid-svg-ECchi2GWl2JthlyX .node .label text,#mermaid-svg-ECchi2GWl2JthlyX .image-shape .label,#mermaid-svg-ECchi2GWl2JthlyX .icon-shape .label{text-anchor:middle;}#mermaid-svg-ECchi2GWl2JthlyX .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-ECchi2GWl2JthlyX .rough-node .label,#mermaid-svg-ECchi2GWl2JthlyX .node .label,#mermaid-svg-ECchi2GWl2JthlyX .image-shape .label,#mermaid-svg-ECchi2GWl2JthlyX .icon-shape .label{text-align:center;}#mermaid-svg-ECchi2GWl2JthlyX .node.clickable{cursor:pointer;}#mermaid-svg-ECchi2GWl2JthlyX .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-ECchi2GWl2JthlyX .arrowheadPath{fill:#333333;}#mermaid-svg-ECchi2GWl2JthlyX .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-ECchi2GWl2JthlyX .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-ECchi2GWl2JthlyX .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ECchi2GWl2JthlyX .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-ECchi2GWl2JthlyX .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ECchi2GWl2JthlyX .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-ECchi2GWl2JthlyX .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-ECchi2GWl2JthlyX .cluster text{fill:#333;}#mermaid-svg-ECchi2GWl2JthlyX .cluster span{color:#333;}#mermaid-svg-ECchi2GWl2JthlyX div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-ECchi2GWl2JthlyX .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-ECchi2GWl2JthlyX rect.text{fill:none;stroke-width:0;}#mermaid-svg-ECchi2GWl2JthlyX .icon-shape,#mermaid-svg-ECchi2GWl2JthlyX .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ECchi2GWl2JthlyX .icon-shape p,#mermaid-svg-ECchi2GWl2JthlyX .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-ECchi2GWl2JthlyX .icon-shape .label rect,#mermaid-svg-ECchi2GWl2JthlyX .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ECchi2GWl2JthlyX .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-ECchi2GWl2JthlyX .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-ECchi2GWl2JthlyX :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 升级
Liquibase 方案
编写 changelog 文件
按顺序执行 changeset
记录执行历史
可追溯、可回滚
ddl-auto: update
Hibernate 自动建表
无法追溯变更历史
不能删列/改类型
团队协作困难
二、添加 Liquibase
2.1 添加依赖
kotlin
dependencies {
implementation("org.liquibase:liquibase-core")
// 其他依赖...
}
2.2 配置
yaml
# application-dev.yml
spring:
liquibase:
change-log: classpath:db/changelog/db.changelog-master.yaml
enabled: true
jpa:
hibernate:
ddl-auto: validate # ← 改为 validate!Liquibase 管理表结构
关键 :引入 Liquibase 后,
ddl-auto必须改为validate(只验证实体与表结构是否匹配)或none。表结构变更由 Liquibase 全权管理。
三、Liquibase 核心概念
3.1 changelog 文件
changelog 是 Liquibase 的「版本历史」,记录所有数据库变更。主 changelog 可以包含多个子文件:
src/main/resources/db/changelog/
├── db.changelog-master.yaml ← 主文件(引入其他变更)
├── changes/
│ ├── 001-create-products.yaml ← 第一次变更:建商品表
│ ├── 002-create-users.yaml ← 第二次变更:建用户表
│ ├── 003-create-orders.yaml ← 第三次变更:建订单表
│ └── 004-add-product-description.yaml ← 第四次变更:加列
3.2 主 changelog
db.changelog-master.yaml:
yaml
databaseChangeLog:
- include:
file: db/changelog/changes/001-create-products.yaml
- include:
file: db/changelog/changes/002-create-users.yaml
- include:
file: db/changelog/changes/003-create-orders.yaml
- include:
file: db/changelog/changes/004-add-product-description.yaml
3.3 changeset
每个变更单元叫 changeset ,有唯一的 id + author 标识:
yaml
databaseChangeLog:
- changeSet:
id: 001-create-products
author: developer
changes:
# 这里写具体的 DDL 操作
Liquibase 会记录每个 changeset 的执行状态。已经执行过的 changeset 不会重复执行。
下面这张图展示了 changelog、主 changelog 与 changeset 之间的关系:
#mermaid-svg-Gcua97FaxdXAJGv2{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-Gcua97FaxdXAJGv2 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Gcua97FaxdXAJGv2 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Gcua97FaxdXAJGv2 .error-icon{fill:#552222;}#mermaid-svg-Gcua97FaxdXAJGv2 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Gcua97FaxdXAJGv2 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Gcua97FaxdXAJGv2 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Gcua97FaxdXAJGv2 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Gcua97FaxdXAJGv2 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Gcua97FaxdXAJGv2 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Gcua97FaxdXAJGv2 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Gcua97FaxdXAJGv2 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Gcua97FaxdXAJGv2 .marker.cross{stroke:#333333;}#mermaid-svg-Gcua97FaxdXAJGv2 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Gcua97FaxdXAJGv2 p{margin:0;}#mermaid-svg-Gcua97FaxdXAJGv2 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-Gcua97FaxdXAJGv2 .cluster-label text{fill:#333;}#mermaid-svg-Gcua97FaxdXAJGv2 .cluster-label span{color:#333;}#mermaid-svg-Gcua97FaxdXAJGv2 .cluster-label span p{background-color:transparent;}#mermaid-svg-Gcua97FaxdXAJGv2 .label text,#mermaid-svg-Gcua97FaxdXAJGv2 span{fill:#333;color:#333;}#mermaid-svg-Gcua97FaxdXAJGv2 .node rect,#mermaid-svg-Gcua97FaxdXAJGv2 .node circle,#mermaid-svg-Gcua97FaxdXAJGv2 .node ellipse,#mermaid-svg-Gcua97FaxdXAJGv2 .node polygon,#mermaid-svg-Gcua97FaxdXAJGv2 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-Gcua97FaxdXAJGv2 .rough-node .label text,#mermaid-svg-Gcua97FaxdXAJGv2 .node .label text,#mermaid-svg-Gcua97FaxdXAJGv2 .image-shape .label,#mermaid-svg-Gcua97FaxdXAJGv2 .icon-shape .label{text-anchor:middle;}#mermaid-svg-Gcua97FaxdXAJGv2 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-Gcua97FaxdXAJGv2 .rough-node .label,#mermaid-svg-Gcua97FaxdXAJGv2 .node .label,#mermaid-svg-Gcua97FaxdXAJGv2 .image-shape .label,#mermaid-svg-Gcua97FaxdXAJGv2 .icon-shape .label{text-align:center;}#mermaid-svg-Gcua97FaxdXAJGv2 .node.clickable{cursor:pointer;}#mermaid-svg-Gcua97FaxdXAJGv2 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-Gcua97FaxdXAJGv2 .arrowheadPath{fill:#333333;}#mermaid-svg-Gcua97FaxdXAJGv2 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-Gcua97FaxdXAJGv2 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-Gcua97FaxdXAJGv2 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Gcua97FaxdXAJGv2 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-Gcua97FaxdXAJGv2 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Gcua97FaxdXAJGv2 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-Gcua97FaxdXAJGv2 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-Gcua97FaxdXAJGv2 .cluster text{fill:#333;}#mermaid-svg-Gcua97FaxdXAJGv2 .cluster span{color:#333;}#mermaid-svg-Gcua97FaxdXAJGv2 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-Gcua97FaxdXAJGv2 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-Gcua97FaxdXAJGv2 rect.text{fill:none;stroke-width:0;}#mermaid-svg-Gcua97FaxdXAJGv2 .icon-shape,#mermaid-svg-Gcua97FaxdXAJGv2 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Gcua97FaxdXAJGv2 .icon-shape p,#mermaid-svg-Gcua97FaxdXAJGv2 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-Gcua97FaxdXAJGv2 .icon-shape .label rect,#mermaid-svg-Gcua97FaxdXAJGv2 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Gcua97FaxdXAJGv2 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-Gcua97FaxdXAJGv2 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-Gcua97FaxdXAJGv2 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} include
include
include
include
db.changelog-master.yaml
(主 changelog)
001-create-products.yaml
002-create-users.yaml
003-create-orders.yaml
004-add-product-description.yaml
changeset
id: 001-create-products
author: developer
changeset
id: 002-create-users
author: developer
changeset
id: 003-create-orders
author: developer
changeset
id: 004-add-product-description
author: developer
数据库
DATABASECHANGELOG 表
四、常用变更操作
4.1 创建表
yaml
# 001-create-products.yaml
databaseChangeLog:
- changeSet:
id: 001-create-products
author: developer
changes:
- createTable:
tableName: products
columns:
- column:
name: id
type: bigint
autoIncrement: true
constraints:
primaryKey: true
nullable: false
- column:
name: name
type: varchar(100)
constraints:
nullable: false
- column:
name: price
type: numeric(10,2)
constraints:
nullable: false
- column:
name: stock
type: integer
constraints:
nullable: false
- column:
name: category
type: varchar(50)
constraints:
nullable: false
- column:
name: description
type: varchar(500)
- column:
name: is_active
type: boolean
constraints:
nullable: false
- column:
name: created_at
type: timestamp
constraints:
nullable: false
- column:
name: updated_at
type: timestamp
constraints:
nullable: false
4.2 添加列
yaml
# 004-add-product-description.yaml
databaseChangeLog:
- changeSet:
id: 004-add-product-description
author: developer
changes:
- addColumn:
tableName: products
columns:
- column:
name: weight
type: numeric(6,2)
remarks: 商品重量(克)
4.3 添加索引
yaml
databaseChangeLog:
- changeSet:
id: 005-add-product-index
author: developer
changes:
- createIndex:
tableName: products
indexName: idx_products_category
columns:
- column:
name: category
4.4 添加外键
yaml
databaseChangeLog:
- changeSet:
id: 006-add-order-foreign-keys
author: developer
changes:
- addForeignKeyConstraint:
baseTableName: orders
baseColumnNames: user_id
constraintName: fk_orders_user_id
referencedTableName: users
referencedColumnNames: id
onDelete: CASCADE # 删用户时级联删订单
4.5 插入初始数据
yaml
databaseChangeLog:
- changeSet:
id: 007-seed-product-categories
author: developer
changes:
- insert:
tableName: products
columns:
- column:
name: name
value: 机械键盘
- column:
name: price
valueNumeric: 299.00
- column:
name: stock
valueNumeric: 50
- column:
name: category
value: 外设
- column:
name: is_active
valueBoolean: true
- column:
name: created_at
valueComputed: CURRENT_TIMESTAMP
- column:
name: updated_at
valueComputed: CURRENT_TIMESTAMP
下面这张图展示了 Liquibase 从启动到执行完成的完整流程:
#mermaid-svg-oaefwiKJ3HaPkhIa{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-oaefwiKJ3HaPkhIa .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-oaefwiKJ3HaPkhIa .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-oaefwiKJ3HaPkhIa .error-icon{fill:#552222;}#mermaid-svg-oaefwiKJ3HaPkhIa .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-oaefwiKJ3HaPkhIa .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-oaefwiKJ3HaPkhIa .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-oaefwiKJ3HaPkhIa .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-oaefwiKJ3HaPkhIa .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-oaefwiKJ3HaPkhIa .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-oaefwiKJ3HaPkhIa .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-oaefwiKJ3HaPkhIa .marker{fill:#333333;stroke:#333333;}#mermaid-svg-oaefwiKJ3HaPkhIa .marker.cross{stroke:#333333;}#mermaid-svg-oaefwiKJ3HaPkhIa svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-oaefwiKJ3HaPkhIa p{margin:0;}#mermaid-svg-oaefwiKJ3HaPkhIa .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-oaefwiKJ3HaPkhIa .cluster-label text{fill:#333;}#mermaid-svg-oaefwiKJ3HaPkhIa .cluster-label span{color:#333;}#mermaid-svg-oaefwiKJ3HaPkhIa .cluster-label span p{background-color:transparent;}#mermaid-svg-oaefwiKJ3HaPkhIa .label text,#mermaid-svg-oaefwiKJ3HaPkhIa span{fill:#333;color:#333;}#mermaid-svg-oaefwiKJ3HaPkhIa .node rect,#mermaid-svg-oaefwiKJ3HaPkhIa .node circle,#mermaid-svg-oaefwiKJ3HaPkhIa .node ellipse,#mermaid-svg-oaefwiKJ3HaPkhIa .node polygon,#mermaid-svg-oaefwiKJ3HaPkhIa .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-oaefwiKJ3HaPkhIa .rough-node .label text,#mermaid-svg-oaefwiKJ3HaPkhIa .node .label text,#mermaid-svg-oaefwiKJ3HaPkhIa .image-shape .label,#mermaid-svg-oaefwiKJ3HaPkhIa .icon-shape .label{text-anchor:middle;}#mermaid-svg-oaefwiKJ3HaPkhIa .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-oaefwiKJ3HaPkhIa .rough-node .label,#mermaid-svg-oaefwiKJ3HaPkhIa .node .label,#mermaid-svg-oaefwiKJ3HaPkhIa .image-shape .label,#mermaid-svg-oaefwiKJ3HaPkhIa .icon-shape .label{text-align:center;}#mermaid-svg-oaefwiKJ3HaPkhIa .node.clickable{cursor:pointer;}#mermaid-svg-oaefwiKJ3HaPkhIa .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-oaefwiKJ3HaPkhIa .arrowheadPath{fill:#333333;}#mermaid-svg-oaefwiKJ3HaPkhIa .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-oaefwiKJ3HaPkhIa .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-oaefwiKJ3HaPkhIa .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-oaefwiKJ3HaPkhIa .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-oaefwiKJ3HaPkhIa .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-oaefwiKJ3HaPkhIa .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-oaefwiKJ3HaPkhIa .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-oaefwiKJ3HaPkhIa .cluster text{fill:#333;}#mermaid-svg-oaefwiKJ3HaPkhIa .cluster span{color:#333;}#mermaid-svg-oaefwiKJ3HaPkhIa div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-oaefwiKJ3HaPkhIa .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-oaefwiKJ3HaPkhIa rect.text{fill:none;stroke-width:0;}#mermaid-svg-oaefwiKJ3HaPkhIa .icon-shape,#mermaid-svg-oaefwiKJ3HaPkhIa .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-oaefwiKJ3HaPkhIa .icon-shape p,#mermaid-svg-oaefwiKJ3HaPkhIa .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-oaefwiKJ3HaPkhIa .icon-shape .label rect,#mermaid-svg-oaefwiKJ3HaPkhIa .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-oaefwiKJ3HaPkhIa .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-oaefwiKJ3HaPkhIa .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-oaefwiKJ3HaPkhIa :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 表不存在
表已存在
有未执行的 changeset
全部已执行
Spring Boot 启动
Liquibase 检查
DATABASECHANGELOG 表
自动创建
DATABASECHANGELOG 表
对比主 changelog
与已执行记录
按顺序执行 changeset
记录执行结果
到 DATABASECHANGELOG
应用启动完成
五、Precondition(前置条件)
在执行 changeset 前检查条件,避免错误操作:
yaml
databaseChangeLog:
- changeSet:
id: 008-add-column-if-not-exists
author: developer
preConditions:
- onFail: MARK_RAN # 条件不满足时标记为已执行(跳过)
- not:
columnExists:
tableName: products
columnName: sku
changes:
- addColumn:
tableName: products
columns:
- column:
name: sku
type: varchar(50)
constraints:
nullable: false
unique: true
用途:防止重复执行或保护已有数据。
六、mini-shop 完整 changelog
6.1 建表顺序
由于外键依赖,建表顺序很重要:
1. products (无依赖)
2. users (无依赖)
3. orders (依赖 users)
4. order_items (依赖 orders 和 products)
6.2 主 changelog
yaml
# db.changelog-master.yaml
databaseChangeLog:
- include:
file: db/changelog/changes/001-create-products.yaml
- include:
file: db/changelog/changes/002-create-users.yaml
- include:
file: db/changelog/changes/003-create-orders.yaml
- include:
file: db/changelog/changes/004-create-order-items.yaml
- include:
file: db/changelog/changes/005-add-indexes.yaml
- include:
file: db/changelog/changes/006-seed-data.yaml
6.3 运行流程
- 启动 Spring Boot 项目
- Liquibase 检查数据库中
DATABASECHANGELOG表(自动创建) - 对比主 changelog,执行未执行的 changeset
- 记录执行结果
bash
# 验证
docker exec -it mini-shop-postgres psql -U postgres -d mini_shop
# 查看已执行的变更
SELECT id, author, dateexecuted FROM databasechangelog ORDER BY orderexecuted;
# 查看表
\dt
# products, users, orders, order_items, databasechangelog, databasechangeloglock
下面这张图展示了 mini-shop 的建表依赖关系:
#mermaid-svg-TP4A5QdVwU3fkC3x{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-TP4A5QdVwU3fkC3x .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-TP4A5QdVwU3fkC3x .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-TP4A5QdVwU3fkC3x .error-icon{fill:#552222;}#mermaid-svg-TP4A5QdVwU3fkC3x .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-TP4A5QdVwU3fkC3x .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-TP4A5QdVwU3fkC3x .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-TP4A5QdVwU3fkC3x .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-TP4A5QdVwU3fkC3x .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-TP4A5QdVwU3fkC3x .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-TP4A5QdVwU3fkC3x .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-TP4A5QdVwU3fkC3x .marker{fill:#333333;stroke:#333333;}#mermaid-svg-TP4A5QdVwU3fkC3x .marker.cross{stroke:#333333;}#mermaid-svg-TP4A5QdVwU3fkC3x svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-TP4A5QdVwU3fkC3x p{margin:0;}#mermaid-svg-TP4A5QdVwU3fkC3x .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-TP4A5QdVwU3fkC3x .cluster-label text{fill:#333;}#mermaid-svg-TP4A5QdVwU3fkC3x .cluster-label span{color:#333;}#mermaid-svg-TP4A5QdVwU3fkC3x .cluster-label span p{background-color:transparent;}#mermaid-svg-TP4A5QdVwU3fkC3x .label text,#mermaid-svg-TP4A5QdVwU3fkC3x span{fill:#333;color:#333;}#mermaid-svg-TP4A5QdVwU3fkC3x .node rect,#mermaid-svg-TP4A5QdVwU3fkC3x .node circle,#mermaid-svg-TP4A5QdVwU3fkC3x .node ellipse,#mermaid-svg-TP4A5QdVwU3fkC3x .node polygon,#mermaid-svg-TP4A5QdVwU3fkC3x .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-TP4A5QdVwU3fkC3x .rough-node .label text,#mermaid-svg-TP4A5QdVwU3fkC3x .node .label text,#mermaid-svg-TP4A5QdVwU3fkC3x .image-shape .label,#mermaid-svg-TP4A5QdVwU3fkC3x .icon-shape .label{text-anchor:middle;}#mermaid-svg-TP4A5QdVwU3fkC3x .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-TP4A5QdVwU3fkC3x .rough-node .label,#mermaid-svg-TP4A5QdVwU3fkC3x .node .label,#mermaid-svg-TP4A5QdVwU3fkC3x .image-shape .label,#mermaid-svg-TP4A5QdVwU3fkC3x .icon-shape .label{text-align:center;}#mermaid-svg-TP4A5QdVwU3fkC3x .node.clickable{cursor:pointer;}#mermaid-svg-TP4A5QdVwU3fkC3x .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-TP4A5QdVwU3fkC3x .arrowheadPath{fill:#333333;}#mermaid-svg-TP4A5QdVwU3fkC3x .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-TP4A5QdVwU3fkC3x .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-TP4A5QdVwU3fkC3x .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-TP4A5QdVwU3fkC3x .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-TP4A5QdVwU3fkC3x .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-TP4A5QdVwU3fkC3x .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-TP4A5QdVwU3fkC3x .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-TP4A5QdVwU3fkC3x .cluster text{fill:#333;}#mermaid-svg-TP4A5QdVwU3fkC3x .cluster span{color:#333;}#mermaid-svg-TP4A5QdVwU3fkC3x div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-TP4A5QdVwU3fkC3x .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-TP4A5QdVwU3fkC3x rect.text{fill:none;stroke-width:0;}#mermaid-svg-TP4A5QdVwU3fkC3x .icon-shape,#mermaid-svg-TP4A5QdVwU3fkC3x .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-TP4A5QdVwU3fkC3x .icon-shape p,#mermaid-svg-TP4A5QdVwU3fkC3x .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-TP4A5QdVwU3fkC3x .icon-shape .label rect,#mermaid-svg-TP4A5QdVwU3fkC3x .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-TP4A5QdVwU3fkC3x .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-TP4A5QdVwU3fkC3x .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-TP4A5QdVwU3fkC3x :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} products
(无依赖)
order_items
users
(无依赖)
orders
七、团队协作工作流
开发者 A 需要加一个新字段:
1. 在 changes/ 目录新建 007-add-product-sku.yaml
2. 在 db.changelog-master.yaml 末尾添加 include
3. 提交 Git
4. 其他开发者 pull 后启动项目,Liquibase 自动执行新 changeset
原则:
- 已提交的 changeset 永远不要修改(已被执行过的 changeset 修改会导致校验失败)
- 需要变更就新增 changeset,不要改旧的
- changeset 命名有编号前缀(001、002、003...),保持执行顺序清晰
下面这张图展示了团队协作中新增变更的标准流程:
#mermaid-svg-58rhEUFDwCdBICLG{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-58rhEUFDwCdBICLG .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-58rhEUFDwCdBICLG .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-58rhEUFDwCdBICLG .error-icon{fill:#552222;}#mermaid-svg-58rhEUFDwCdBICLG .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-58rhEUFDwCdBICLG .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-58rhEUFDwCdBICLG .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-58rhEUFDwCdBICLG .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-58rhEUFDwCdBICLG .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-58rhEUFDwCdBICLG .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-58rhEUFDwCdBICLG .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-58rhEUFDwCdBICLG .marker{fill:#333333;stroke:#333333;}#mermaid-svg-58rhEUFDwCdBICLG .marker.cross{stroke:#333333;}#mermaid-svg-58rhEUFDwCdBICLG svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-58rhEUFDwCdBICLG p{margin:0;}#mermaid-svg-58rhEUFDwCdBICLG .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-58rhEUFDwCdBICLG .cluster-label text{fill:#333;}#mermaid-svg-58rhEUFDwCdBICLG .cluster-label span{color:#333;}#mermaid-svg-58rhEUFDwCdBICLG .cluster-label span p{background-color:transparent;}#mermaid-svg-58rhEUFDwCdBICLG .label text,#mermaid-svg-58rhEUFDwCdBICLG span{fill:#333;color:#333;}#mermaid-svg-58rhEUFDwCdBICLG .node rect,#mermaid-svg-58rhEUFDwCdBICLG .node circle,#mermaid-svg-58rhEUFDwCdBICLG .node ellipse,#mermaid-svg-58rhEUFDwCdBICLG .node polygon,#mermaid-svg-58rhEUFDwCdBICLG .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-58rhEUFDwCdBICLG .rough-node .label text,#mermaid-svg-58rhEUFDwCdBICLG .node .label text,#mermaid-svg-58rhEUFDwCdBICLG .image-shape .label,#mermaid-svg-58rhEUFDwCdBICLG .icon-shape .label{text-anchor:middle;}#mermaid-svg-58rhEUFDwCdBICLG .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-58rhEUFDwCdBICLG .rough-node .label,#mermaid-svg-58rhEUFDwCdBICLG .node .label,#mermaid-svg-58rhEUFDwCdBICLG .image-shape .label,#mermaid-svg-58rhEUFDwCdBICLG .icon-shape .label{text-align:center;}#mermaid-svg-58rhEUFDwCdBICLG .node.clickable{cursor:pointer;}#mermaid-svg-58rhEUFDwCdBICLG .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-58rhEUFDwCdBICLG .arrowheadPath{fill:#333333;}#mermaid-svg-58rhEUFDwCdBICLG .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-58rhEUFDwCdBICLG .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-58rhEUFDwCdBICLG .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-58rhEUFDwCdBICLG .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-58rhEUFDwCdBICLG .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-58rhEUFDwCdBICLG .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-58rhEUFDwCdBICLG .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-58rhEUFDwCdBICLG .cluster text{fill:#333;}#mermaid-svg-58rhEUFDwCdBICLG .cluster span{color:#333;}#mermaid-svg-58rhEUFDwCdBICLG div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-58rhEUFDwCdBICLG .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-58rhEUFDwCdBICLG rect.text{fill:none;stroke-width:0;}#mermaid-svg-58rhEUFDwCdBICLG .icon-shape,#mermaid-svg-58rhEUFDwCdBICLG .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-58rhEUFDwCdBICLG .icon-shape p,#mermaid-svg-58rhEUFDwCdBICLG .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-58rhEUFDwCdBICLG .icon-shape .label rect,#mermaid-svg-58rhEUFDwCdBICLG .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-58rhEUFDwCdBICLG .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-58rhEUFDwCdBICLG .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-58rhEUFDwCdBICLG :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 开发者 A 需要加新字段
在 changes/ 目录
新建变更文件
在 db.changelog-master.yaml
末尾添加 include
提交 Git
其他开发者 pull 代码
启动项目
Liquibase 自动执行
新 changeset
所有环境同步
本篇小结
| 知识点 | 核心内容 |
|---|---|
| Liquibase | 数据库版本控制工具 |
| changelog | 变更历史文件 |
| changeset | 最小变更单元(id + author 唯一) |
createTable |
创建表 |
addColumn |
添加列 |
createIndex |
创建索引 |
addForeignKeyConstraint |
外键约束 |
insert |
插入初始数据 |
| precondition | 执行前检查条件 |
ddl-auto: validate |
引入 Liquibase 后改为 validate |
| 团队协作 | 新变更只新增 changeset,不改旧的 |
下篇预告
下单要扣库存 + 创建订单 + 记录日志,如果中间失败了怎么办?下一篇深入 Spring 的事务管理,学习传播行为、隔离级别和 Kotlin 中的事务陷阱。
如果本篇内容对你有帮助,欢迎点赞收藏!有任何疑问,欢迎在评论区交流。