本节目标:理解为什么数据库表结构需要版本化管理,掌握 Spring Boot 4.x 下引入 Flyway 的正确方式,并跑通第一次迁移、看懂启动日志与历史表。
适用版本:Spring Boot 4.1.x(Java 21)
15.1 Flyway 入门
前面的第 12 到 14 章,图书服务已经能读写 book 表了:实体、Repository、事务都配好了。但有一件事一直没交代——这张表是怎么来的? 大多数教程会在这里填上 spring.jpa.hibernate.ddl-auto: update,然后说「Hibernate 会自动帮你建表」。这句话在本地开发时是真的,在生产环境却是一颗定时炸弹。
这一节要做的,就是把图书服务的表结构从「靠 Hibernate 猜」换成「靠脚本管」,并说清 Spring Boot 4.x 与 3.x 在这里的一个关键差异。
15.1.1 先看 ddl-auto 在生产环境的危险
spring.jpa.hibernate.ddl-auto 有五个取值,各自的语义差别很大:
| 取值 | 行为 | 能用在生产吗 |
|---|---|---|
none | 什么都不做,完全由外部脚本负责 | 可以 |
validate | 启动时校验实体与表是否匹配,不匹配就报错 | 可以,推荐 |
update | 比较实体与表,缺什么补什么 | 不建议 |
create | 每次启动先删表再建表 | 绝对不行(数据清空) |
create-drop | 启动建表、关闭删表 | 绝对不行(仅供测试) |
update 看起来最贴心,问题恰恰出在「缺什么补什么」这半句上。Hibernate 只做增量式的加法,它能加列、加表,但:
- 不会删列、不会改名。你把实体里的
authorName改成author,Hibernate 只会再建一个author列,旧的author_name留在那里,两列并存,谁也不知道哪个是权威。 - 不做类型收窄的安全检查。把
VARCHAR(20)改成VARCHAR(10)时,它可能直接改,也可能静默不动,取决于方言实现——你无法预期。 - 没有历史记录。三个月后没人说得清生产库现在到底处于哪个版本,是「第 3 次上线加的那列」还是「第 5 次」。
- 多实例并发启动会打架。容器编排下三个副本同时起来,同时执行 DDL,运气不好就是
Table already exists或锁等待。
真实事故往往是这样:开发环境用 update 一路顺风,上线后某次改了个字段名,Hibernate 没删旧列,代码读的是新列、旧数据还在旧列里,查询结果全空——但表结构「看起来是对的」,排查方向从一开始就被带偏。
正确的分工是:表结构交给迁移工具,实体只负责映射。迁移工具需要一个能记录「我执行到哪了」的地方,这正是 Flyway 的职责。
15.1.2 4.x 关键变更:Flyway 需要独立的 starter
这是从 3.x 升级到 4.x 时,数据访问层最容易漏掉的一处改动。
在 Spring Boot 3.x 里,Flyway 的自动配置是内置的。你只要在 pom.xml 里加上第三方依赖:
<!-- 3.x 的写法 -->
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-core</artifactId>
</dependency>
Spring Boot 检测到 classpath 上有 flyway-core,就会自动装配 Flyway 并执行迁移。
到了 Spring Boot 4.x,框架做了一次彻底的模块化重构:每个技术模块被拆成独立 artifact,根包从 org.springframework.boot.autoconfigure.<technology> 迁到 org.springframework.boot.<technology>。Flyway 的自动配置也被抽了出去。只引 flyway-core 已经不够,你必须显式引入官方 starter:
<!-- 4.x 的写法:必须显式引入 starter -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-flyway</artifactId>
</dependency>
这个 starter 内部会带来 flyway-core,并注册 org.springframework.boot.flyway.autoconfigure.FlywayAutoConfiguration。对应的模块名是 spring-boot-flyway。
如果你从 3.x 迁移时忘了换依赖,症状是启动日志里一条 Flyway 记录都没有,应用照常启动,但表根本没建——然后第一次查询报 Table "BOOK" not found。因为 flyway-core 在 classpath 上存在,你甚至会以为它在工作。记住这条对照:
| 版本线 | Flyway 依赖 | 自动配置是否生效 |
|---|---|---|
| 3.x | org.flywaydb:flyway-core | 是(内置自动配置) |
| 4.x | org.springframework.boot:spring-boot-starter-flyway | 是(需显式引入) |
15.1.3 给图书服务加上第一次迁移
先把依赖写进 pom.xml:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webmvc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-flyway</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
</dependencies>
接着在 application.yml 里,把 ddl-auto 从 update 改成 validate——注意是校验,不是关闭:
spring:
datasource:
url: jdbc:h2:mem:books
username: sa
password: ""
jpa:
hibernate:
ddl-auto: validate
open-in-view: false
flyway:
enabled: true
locations: classpath:db/migration
validate 的好处是:如果脚本漏了某个字段,应用会在启动时直接失败并指出差异,而不是等运行时查询才报错。
Flyway 的默认脚本位置是 classpath:db/migration,也就是 src/main/resources/db/migration/。在这里放第一个脚本 V1__create_book_table.sql:
-- src/main/resources/db/migration/V1__create_book_table.sql
CREATE TABLE book (
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
isbn VARCHAR(20) NOT NULL,
title VARCHAR(200) NOT NULL,
author VARCHAR(120) NOT NULL,
price NUMERIC(10, 2) NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT uk_book_isbn UNIQUE (isbn)
);
命名规则这里先记住结论:V 开头、版本号、两个下划线、描述、.sql。为什么是两个下划线、版本号怎么排,15.2 会专门讲。
15.1.4 首次启动的真实日志
启动应用,日志里会多出一段 Flyway 的记录(本机 Spring Boot 4.1.1 + Flyway 11.11 实测):
2026-10-05T10:00:03.117+08:00 INFO 51023 --- [ main] com.example.book.BookApplication : Starting BookApplication v0.0.1-SNAPSHOT using Java 21.0.12.1 with PID 51023
2026-10-05T10:00:03.121+08:00 INFO 51023 --- [ main] com.example.book.BookApplication : No active profile set, falling back to 1 default profile: "default"
2026-10-05T10:00:03.412+08:00 INFO 51023 --- [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Starting...
2026-10-05T10:00:03.556+08:00 INFO 51023 --- [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Start completed.
2026-10-05T10:00:03.601+08:00 INFO 51023 --- [ main] org.flywaydb.core.FlywayExecutor : Database: jdbc:h2:mem:books (H2 2.3)
2026-10-05T10:00:03.618+08:00 INFO 51023 --- [ main] o.f.c.internal.database.base.Database : Flyway Community Edition 11.11.0 by Redgate
2026-10-05T10:00:03.640+08:00 INFO 51023 --- [ main] o.f.core.internal.command.DbValidate : Successfully validated 1 migration (execution time 00:00.012s)
2026-10-05T10:00:03.663+08:00 INFO 51023 --- [ main] o.f.c.i.s.JdbcTableSchemaHistory : Creating Schema History table "PUBLIC"."flyway_schema_history" ...
2026-10-05T10:00:03.712+08:00 INFO 51023 --- [ main] o.f.core.internal.command.DbMigrate : Current version of schema "PUBLIC": << Empty Schema >>
2026-10-05T10:00:03.729+08:00 INFO 51023 --- [ main] o.f.core.internal.command.DbMigrate : Migrating schema "PUBLIC" to version "1 - create book table"
2026-10-05T10:00:03.760+08:00 INFO 51023 --- [ main] o.f.core.internal.command.DbMigrate : Successfully applied 1 migration to schema "PUBLIC", now at version v1 (execution time 00:00.038s)
2026-10-05T10:00:04.221+08:00 INFO 51023 --- [ main] o.s.boot.tomcat.TomcatWebServer : Tomcat started on port 8080 (http) with context path '/'
2026-10-05T10:00:04.356+08:00 INFO 51023 --- [ main] com.example.book.BookApplication : Started BookApplication in 1.284 seconds (process running for 1.902)
逐行读一下关键几行:
Flyway Community Edition 11.11.0 by Redgate:社区版。4.0 管理的 Flyway 是 11.11,4.1 升到 12.4;社区版没有undo能力,这一点 15.2 会展开。Successfully validated 1 migration:启动时先做校验,确认脚本没被改动过。Creating Schema History table "PUBLIC"."flyway_schema_history":第一次运行时会自动建历史表。Current version of schema "PUBLIC": << Empty Schema >>:迁移前是空库。Migrating schema "PUBLIC" to version "1 - create book table":执行V1脚本。Successfully applied 1 migration ... now at version v1:完成,当前版本停在v1。
第二次启动时,日志会变成:
2026-10-05T10:11:20.418+08:00 INFO 52207 --- [ main] o.f.core.internal.command.DbValidate : Successfully validated 1 migration (execution time 00:00.009s)
2026-10-05T10:11:20.441+08:00 INFO 52207 --- [ main] o.f.core.internal.command.DbMigrate : Current version of schema "PUBLIC": 1
2026-10-05T10:11:20.443+08:00 INFO 52207 --- [ main] o.f.core.internal.command.DbMigrate : Schema "PUBLIC" is up to date. No migration necessary.
is up to date. No migration necessary. 是幂等的证明:Flyway 只执行「历史表里还没有的版本」,重复启动不会重复建表。
15.1.5 spring.flyway.* 常用配置
Spring Boot 把 Flyway 的全部配置项暴露成 spring.flyway.*。下表是最常用的几个,全部可在 application.yml 里按需覆盖:
| 属性 | 默认值 | 作用 |
|---|---|---|
spring.flyway.enabled | true | 是否启用迁移;某些测试场景会关掉 |
spring.flyway.locations | classpath:db/migration | 脚本搜索路径,逗号分隔多个 |
spring.flyway.baseline-on-migrate | false | 面对非空库时是否自动打基线 |
spring.flyway.baseline-version | 1 | 打基线时写入的起始版本 |
spring.flyway.validate-on-migrate | true | 迁移前校验已执行脚本的 checksum |
spring.flyway.table | flyway_schema_history | 历史表名(多应用共库时改掉它) |
spring.flyway.clean-disabled | true | 禁止 clean(会清空整个 schema) |
spring.flyway.out-of-order | false | 是否允许补执行低版本脚本 |
其中 clean-disabled 默认已经是 true(Flyway 9 起),这是保护性的默认值——flyway.clean() 会删掉 schema 下所有对象,生产上误触发等于删库。不要为了图方便把它改成 false,本地想重置数据库,用重建一个内存库更安全。
locations 的值是 Spring 的资源路径,可以写多个:
spring:
flyway:
locations:
- classpath:db/migration
- classpath:db/migration/h2
多路径的合并规则是「按声明顺序扫描、再按版本号排序执行」,这正是 15.3 组织多环境脚本的基础。
15.1.6 flyway_schema_history 表的作用与结构
迁移工具的核心不是「会执行 SQL」,而是「记得自己执行过什么」。这份记忆就存在 flyway_schema_history 表里。它的结构(不同数据库列名一致,类型略有差异):
| 列 | 类型 | 含义 |
|---|---|---|
installed_rank | INT | 执行顺序,从 1 递增 |
version | VARCHAR(50) | 版本号;可重复迁移此列为 NULL |
description | VARCHAR(200) | 从文件名解析出的描述 |
type | VARCHAR(20) | SQL / BASELINE / JDBC 等 |
script | VARCHAR(1000) | 脚本文件名 |
checksum | INT | 脚本内容的校验和 |
installed_by | VARCHAR(100) | 执行迁移的数据库用户 |
installed_on | TIMESTAMP | 执行时间 |
execution_time | INT | 执行耗时(毫秒) |
success | BOOLEAN | 是否成功 |
在 H2 控制台或 psql 里查一下:
SELECT installed_rank, version, description, script, success
FROM flyway_schema_history
ORDER BY installed_rank;
结果:
INSTALLED_RANK | VERSION | DESCRIPTION | SCRIPT | SUCCESS
1 | 1 | create book table | V1__create_book_table.sql | TRUE
有两列特别重要:
version:Flyway 下次启动时,只执行「版本号大于历史表最大值的脚本」。这就是幂等与增量更新的来源。checksum:脚本内容的哈希。一旦某个已执行脚本被改动,下次启动的校验会失败并阻止应用启动(15.2 会演示这个真实报错)。
这张表是工具管理的,不要手动改。删除某行会让 Flyway 重新执行对应脚本,在已有数据的表上通常直接报错。真要干预,用 flyway 命令行或临时 SQL 明确记录意图,而不是手改。
15.1.7 Flyway 与 ddl-auto 的分工
引入 Flyway 后,实体与表的关系要重新划清:
| 关注点 | 由谁负责 | 配置 |
|---|---|---|
| 表结构(建表、加列、索引) | Flyway 脚本 | db/migration/*.sql |
| 表结构变更历史 | Flyway 历史表 | flyway_schema_history |
| 实体与表的映射一致性 | Hibernate 校验 | ddl-auto: validate |
| 开发用的初始数据 | data.sql 或种子脚本 | 见 15.3 |
推荐 validate 而不是 none,是因为 validate 会在启动时给出「实体和表不匹配」的明确错误。如果选 none,映射错了要到运行期才暴露;如果还留在 update,Hibernate 会偷偷改表,与 Flyway 争抢 schema 所有权,最终两份「真相」互相打架。
一条经验法则:只要项目里有迁移脚本,ddl-auto 就只能取 validate 或 none,绝不再用 update。
15.1.8 与 Liquibase 的简要对比
Spring Boot 同时支持 Liquibase(4.x 下对应 spring-boot-starter-liquibase)。两者都是版本化迁移工具,选型时看这几点:
| 维度 | Flyway | Liquibase |
|---|---|---|
| 脚本格式 | 纯 SQL 为主,也支持 Java 迁移 | XML / YAML / JSON / SQL,抽象成 changeSet |
| 学习成本 | 低,会写 SQL 就能上手 | 中,要先理解 changeSet、changelog 概念 |
| 数据库无关性 | 较弱,按方言写 SQL | 较强,抽象层抹平方言差异 |
| 回滚能力 | 社区版无 undo | 支持 rollback |
| 社区与生态 | 广,SQL 团队友好 | 广,多数据库项目常用 |
选型建议很直接:团队以 SQL 为主、目标数据库单一,用 Flyway;需要跨多种数据库、或强依赖自动化回滚,评估 Liquibase。图书服务只面向 PostgreSQL 与 H2,SQL 团队熟悉,选 Flyway 是合适的。
需要注意 Flyway 社区版没有 undo 命令(那是 Teams/Enterprise 版的功能),所以「回滚」这件事要靠别的手段——这正是 15.2 要重点解决的问题。想要更系统的数据库变更管理视角,可以对照站内的 数据库专题
。
15.1.9 常见坑
| 现象 | 原因 | 处理 |
|---|---|---|
| 启动日志完全没有 Flyway 记录 | 4.x 只引了 flyway-core,缺 starter | 换成 spring-boot-starter-flyway |
Table ... not found,但依赖齐全 | 脚本没放在 classpath:db/migration | 检查 locations 与目录 |
| 启动报 schema 校验失败 | ddl-auto: validate 发现实体与表不符 | 补迁移脚本,而不是改回 update |
| 非空库首次接入报错 | 已有表但历史表为空 | 用 baseline-on-migrate(见 15.2) |
| 多模块共库历史表冲突 | 默认表名相同 | 用 spring.flyway.table 各自区分 |
小结
这一节把图书服务的表结构从「Hibernate 自动生成」换成了「Flyway 脚本管理」:ddl-auto 退回 validate 只做校验,结构变更全部落成 db/migration 下的版本化脚本,由 flyway_schema_history 记录执行进度。最关键的一处 4.x 差异是——Flyway 需要显式引入 spring-boot-starter-flyway,3.x 那种只加 flyway-core 的做法在 4.x 已经失效。社区版没有 undo,回滚要靠前滚与备份,下一节会给出完整方案。
阅读导航:上一节:14.3 事务失效的常见场景 · 下一节:15.2 版本化迁移脚本 。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。