《Spring Boot 入门》15.1 Flyway 入门

本节从 ddl-auto: update 的风险讲起,说明为什么表结构需要版本化迁移;点明 Spring Boot 4.x 的关键变更——Flyway 必须显式引入 spring-boot-starter-flyway。内容涵盖 spring.flyway.* 常用配置、默认脚本位置、首次启动日志与历史表结构,并给出与 Liquibase 的选型对比。

本节目标:理解为什么数据库表结构需要版本化管理,掌握 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.xorg.flywaydb:flyway-core是(内置自动配置)
4.xorg.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.enabledtrue是否启用迁移;某些测试场景会关掉
spring.flyway.locationsclasspath:db/migration脚本搜索路径,逗号分隔多个
spring.flyway.baseline-on-migratefalse面对非空库时是否自动打基线
spring.flyway.baseline-version1打基线时写入的起始版本
spring.flyway.validate-on-migratetrue迁移前校验已执行脚本的 checksum
spring.flyway.tableflyway_schema_history历史表名(多应用共库时改掉它)
spring.flyway.clean-disabledtrue禁止 clean(会清空整个 schema)
spring.flyway.out-of-orderfalse是否允许补执行低版本脚本

其中 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_rankINT执行顺序,从 1 递增
versionVARCHAR(50)版本号;可重复迁移此列为 NULL
descriptionVARCHAR(200)从文件名解析出的描述
typeVARCHAR(20)SQL / BASELINE / JDBC 等
scriptVARCHAR(1000)脚本文件名
checksumINT脚本内容的校验和
installed_byVARCHAR(100)执行迁移的数据库用户
installed_onTIMESTAMP执行时间
execution_timeINT执行耗时(毫秒)
successBOOLEAN是否成功

在 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)。两者都是版本化迁移工具,选型时看这几点:

维度FlywayLiquibase
脚本格式纯 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 版本化迁移脚本 。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「java」更多文章

  1. 《Spring Boot 入门》18.3 打包与运行
  2. 《Spring Boot 入门》18.2 实现
  3. 《Spring Boot 入门》18.1 需求与设计