第22篇-数据库迁移-Liquibase

【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 运行流程

  1. 启动 Spring Boot 项目
  2. Liquibase 检查数据库中 DATABASECHANGELOG 表(自动创建)
  3. 对比主 changelog,执行未执行的 changeset
  4. 记录执行结果
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,不改旧的

下篇预告

第 23 篇:事务管理

下单要扣库存 + 创建订单 + 记录日志,如果中间失败了怎么办?下一篇深入 Spring 的事务管理,学习传播行为、隔离级别和 Kotlin 中的事务陷阱。


如果本篇内容对你有帮助,欢迎点赞收藏!有任何疑问,欢迎在评论区交流。

相关推荐
刘名喜2 小时前
第30篇-Spring-Security-7核心概念
后端·kotlin·springboot
我命由我123454 小时前
Jetpack Compose - Material Design 断点范围、WindowSizeClass、针对不同屏幕尺寸创建预览、四类导航栏
android·java·开发语言·java-ee·kotlin·android jetpack·android runtime
刘名喜17 小时前
第24篇-全局异常处理
kotlin·springboot
我命由我1234521 小时前
Jetpack Compose - MaterialExpressiveTheme 与 MaterialTheme、ColorScheme
android·java·开发语言·java-ee·kotlin·android jetpack·android runtime
深海呐1 天前
仓颉语言是ArkTs的上层语言吗?就像kotlin和Java
java·开发语言·kotlin·仓颉
YDS8291 天前
大营销平台 —— 架构解析和抽奖流程串联
java·springboot·ddd
Kapaseker1 天前
Boolean 变量到底该怎么命名?
android·kotlin
刘名喜1 天前
第21篇-N+1问题与fetch-join-EntityGraph
kotlin·springboot
我命由我123451 天前
Kotlin 面向对象 - Kotlin 类变量与类方法
java·服务器·后端·java-ee·kotlin·android jetpack·android runtime