Flyway!你AI开发Java项目时不错的数据库版本管理工具
在后端项目里,数据库变更常常是最容易失控的一环。
代码有 Git,配置有环境变量,接口有文档,但数据库结构有时还靠一句“你把这条 SQL 在测试库执行一下”。项目小的时候,这么做似乎没什么问题;一旦团队变大、环境变多、上线频率变高,问题就会不断冒出来:
- 本地库有字段,测试库没有字段;
- 开发同学改了表结构,但忘了同步给运维;
- 线上临时修了一条 SQL,代码仓库里没有记录;
- 新同事启动项目时,不知道数据库应该建成什么样;
- 回滚应用时,发现数据库已经被改到另一个状态。
Flyway 解决的就是这个问题:把数据库变更纳入版本管理,让数据库结构像代码一样可追踪、可审计、可重复执行。
本文会从零开始讲清楚 Flyway 的核心概念,并给出一套 Spring Boot 项目中的完整集成方式。示例以 MySQL 为主,但迁移思路同样适用于 PostgreSQL、Oracle、SQL Server 等关系型数据库。
1. Flyway 是什么?
Flyway 是一个数据库迁移工具。
所谓“数据库迁移”,不是把 MySQL 迁移到 PostgreSQL,而是指数据库结构和基础数据的版本演进。比如:
- 第一次建表;
- 新增字段;
- 修改字段长度;
- 新增索引;
- 创建视图;
- 初始化系统字典;
- 修复历史脏数据;
- 更新存储过程或函数。
这些变更如果靠人工执行,很难保证每个环境都一致。Flyway 的做法是把每次变更写成一个迁移脚本,然后按版本顺序执行。
一个典型的 Flyway 脚本长这样:
V1__create_user_table.sql
V2__add_email_to_user.sql
V3__create_order_table.sql
R__refresh_user_summary_view.sql
Spring Boot 启动时,Flyway 会自动检查数据库当前已经执行到哪个版本,然后只执行还没执行过的脚本。
2. Flyway 的工作原理
Flyway 会在数据库里维护一张历史表,默认表名是:
flyway_schema_history
这张表记录了数据库迁移的执行历史,包括:
- 脚本版本;
- 脚本名称;
- 脚本类型;
- checksum;
- 执行人;
- 执行时间;
- 执行耗时;
- 执行状态。
当应用启动并执行迁移时,Flyway 会做几件事:
- 扫描项目里的迁移脚本;
- 查询
flyway_schema_history; - 判断哪些脚本已经执行过,哪些还没有执行;
- 按版本顺序执行待执行脚本;
- 执行成功后,把结果写入历史表。
这个机制带来的最大好处是:数据库变更从“口头通知 + 手工执行”,变成了“随代码提交 + 自动执行 + 有历史记录”。
3. 为什么 Spring Boot 项目适合用 Flyway?
Spring Boot 项目通常会部署到多个环境:
- 本地开发环境;
- 测试环境;
- 预发环境;
- 生产环境;
- CI 自动化测试环境。
如果数据库结构不是自动化管理的,那么每个环境都可能变成不同的状态。Flyway 和 Spring Boot 的集成非常自然:只要引入依赖,并把 SQL 文件放到约定目录,应用启动时就会自动执行数据库迁移。
Spring Boot 官方也建议:如果使用 Flyway 或 Liquibase 这类高级数据库迁移工具,就应该使用它们来创建和初始化 schema,不推荐再同时混用 schema.sql、data.sql 这类基础初始化脚本。
4. 版本说明:Spring Boot 3.x 和 4.x 的依赖差异
这里先把一个容易踩坑的点说清楚。
在 Spring Boot 3.5 文档中,官方说明自动运行 Flyway 迁移时,需要把合适的 Flyway 模块加入 classpath。对于内存数据库和文件型数据库,可以使用:
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-core</artifactId>
</dependency>
如果是 PostgreSQL、MySQL 这类数据库,还需要额外加入数据库专用模块。例如:
<!-- PostgreSQL -->
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-database-postgresql</artifactId>
</dependency>
<!-- MySQL -->
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-mysql</artifactId>
</dependency>
到了 Spring Boot 4.1 文档,官方开始提到 spring-boot-starter-flyway。对于内存数据库和文件型数据库,可以使用这个 starter;其他数据库场景仍然需要数据库专用 Flyway 模块。
也就是说:
- Spring Boot 3.x 项目:常见写法是
flyway-core+ 数据库专用模块; - Spring Boot 4.x 项目:可以优先看是否使用
spring-boot-starter-flyway,再补数据库专用模块; - 不要手写版本号,优先让 Spring Boot 的依赖管理来控制版本。
本文下面的实战示例以 Spring Boot 3.x 的通用写法为主,因为目前很多生产项目仍在 3.x 版本线上运行。使用 Spring Boot 4.x 的同学,把 flyway-core 换成对应 starter 即可,其他配置和脚本组织方式基本一致。
5. 创建一个示例 Spring Boot 项目
假设我们要做一个简单的订单系统,数据库里需要两张表:
sys_user:用户表;orders:订单表。
项目技术栈:
- Spring Boot 3.x;
- MySQL 8.x;
- Maven;
- Spring Web;
- Spring Data JPA;
- Flyway。
项目结构大致如下:
flyway-demo
├── pom.xml
└── src
└── main
├── java
│ └── com/example/flywaydemo
│ └── FlywayDemoApplication.java
└── resources
├── application.yml
└── db
└── migration
├── V1__create_sys_user_table.sql
├── V2__create_orders_table.sql
└── R__refresh_user_order_summary_view.sql
Flyway 默认会扫描:
classpath:db/migration
所以在 Spring Boot 项目里,默认目录就是:
src/main/resources/db/migration
6. 引入 Maven 依赖
pom.xml 示例:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-core</artifactId>
</dependency>
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-mysql</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
如果你使用的是 PostgreSQL,可以把 MySQL 相关依赖换成:
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-database-postgresql</artifactId>
</dependency>
如果你使用 Spring Boot 4.1 或更新版本,可以参考官方文档使用:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-flyway</artifactId>
</dependency>
然后根据数据库类型补充 flyway-mysql、flyway-database-postgresql 等模块。
7. 配置数据库和 Flyway
application.yml 示例:
spring:
datasource:
url: jdbc:mysql://localhost:3306/flyway_demo?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
jpa:
hibernate:
ddl-auto: validate
show-sql: true
flyway:
enabled: true
locations: classpath:db/migration
baseline-on-migrate: false
validate-on-migrate: true
clean-disabled: true
这里重点解释几个配置。
spring.flyway.enabled
是否启用 Flyway。
spring:
flyway:
enabled: true
默认就是 true。如果你某个环境不想让应用启动时自动迁移,可以设置为 false。
spring.flyway.locations
迁移脚本所在位置。
spring:
flyway:
locations: classpath:db/migration
也可以配置多个位置:
spring:
flyway:
locations: classpath:db/migration,filesystem:/opt/migration
classpath: 表示从应用 classpath 里找,通常就是 resources 目录。filesystem: 表示从服务器文件系统里找。
spring.flyway.validate-on-migrate
执行迁移前是否校验历史脚本。
spring:
flyway:
validate-on-migrate: true
建议保持开启。它可以帮你发现“已经执行过的脚本被人改了”这类危险操作。
spring.flyway.clean-disabled
是否禁用 clean。
spring:
flyway:
clean-disabled: true
clean 会删除 configured schemas 里的对象,非常危险。生产环境一定要禁用。
Spring Boot 当前默认 clean-disabled 就是 true,但在团队项目里建议显式写出来,让配置意图更清楚。
spring.jpa.hibernate.ddl-auto
使用 Flyway 后,不建议再让 Hibernate 自动改表。
推荐:
spring:
jpa:
hibernate:
ddl-auto: validate
validate 表示 Hibernate 只校验实体和表结构是否匹配,不主动创建或修改表。
不推荐在生产环境使用:
spring:
jpa:
hibernate:
ddl-auto: update
因为 update 会让 Hibernate 根据实体自动改库,这和 Flyway 的版本化迁移思路冲突。
8. 编写第一个迁移脚本
在目录:
src/main/resources/db/migration
创建文件:
V1__create_sys_user_table.sql
内容如下:
CREATE TABLE sys_user (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(64) NOT NULL,
email VARCHAR(128) DEFAULT NULL,
status TINYINT NOT NULL DEFAULT 1,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY uk_sys_user_username (username),
KEY idx_sys_user_email (email)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';
文件名含义:
V1__create_sys_user_table.sql
V:Versioned migration,版本迁移;1:版本号;__:两个下划线,固定分隔符;create_sys_user_table:描述;.sql:SQL 脚本。
注意:中间必须是两个下划线,不是一个。
9. 创建第二个迁移脚本
继续创建:
V2__create_orders_table.sql
内容:
CREATE TABLE orders (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
order_no VARCHAR(64) NOT NULL,
user_id BIGINT NOT NULL,
amount DECIMAL(12, 2) NOT NULL,
status VARCHAR(32) NOT NULL DEFAULT 'CREATED',
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY uk_orders_order_no (order_no),
KEY idx_orders_user_id (user_id),
CONSTRAINT fk_orders_user_id FOREIGN KEY (user_id) REFERENCES sys_user(id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单表';
当应用启动时,Flyway 会先执行 V1,再执行 V2。
10. 启动应用后会发生什么?
确保数据库已经存在:
CREATE DATABASE flyway_demo DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
然后启动 Spring Boot 应用。
你会在日志中看到类似信息:
Migrating schema `flyway_demo` to version "1 - create sys user table"
Migrating schema `flyway_demo` to version "2 - create orders table"
Successfully applied 2 migrations
执行完成后,数据库里会出现三张表:
flyway_schema_history
sys_user
orders
可以查询迁移历史:
SELECT installed_rank, version, description, type, script, checksum, success
FROM flyway_schema_history
ORDER BY installed_rank;
结果大致如下:
+----------------+---------+------------------------+------+----------------------------------+------------+---------+
| installed_rank | version | description | type | script | checksum | success |
+----------------+---------+------------------------+------+----------------------------------+------------+---------+
| 1 | 1 | create sys user table | SQL | V1__create_sys_user_table.sql | ... | 1 |
| 2 | 2 | create orders table | SQL | V2__create_orders_table.sql | ... | 1 |
+----------------+---------+------------------------+------+----------------------------------+------------+---------+
以后再次启动应用时,Flyway 会发现 V1 和 V2 已经执行过,不会重复执行。
11. 新增字段应该怎么做?
假设业务后来要求给订单表增加支付时间字段。
不要修改已经执行过的:
V2__create_orders_table.sql
正确做法是新增一个迁移脚本:
V3__add_paid_at_to_orders.sql
内容:
ALTER TABLE orders
ADD COLUMN paid_at DATETIME DEFAULT NULL COMMENT '支付时间';
为什么不能改旧脚本?
因为 V2 已经在某些环境执行过了。Flyway 已经把它的 checksum 记录到了 flyway_schema_history。如果你修改旧脚本,下一次启动时 Flyway 会发现本地脚本 checksum 和数据库历史记录不一致,然后报错。
这不是 Flyway 麻烦,而是它在保护你:已经发布过的数据库历史,不应该被悄悄改写。
团队里要形成一个规则:
已经合并、已经部署、已经在共享环境执行过的
V脚本,不要再修改。后续变更新增脚本继续往前走。
12. Versioned Migration:版本迁移
V 开头的是版本迁移。
常见文件名:
V1__init_schema.sql
V2__create_user_table.sql
V3__add_user_email.sql
V4__create_order_table.sql
V202607161030__add_order_paid_at.sql
版本迁移的特点:
- 每个版本只执行一次;
- 按版本号顺序执行;
- 每个版本号必须唯一;
- 执行后 checksum 会被记录;
- 适合建表、加字段、加索引、改数据。
版本号可以用简单数字:
V1__init.sql
V2__add_user.sql
V3__add_order.sql
也可以用带下划线的版本:
V1_1__add_user_email.sql
V1_2__add_user_phone.sql
多人协作时,更推荐使用时间戳,减少版本冲突:
V202607161030__add_user_email.sql
V202607161145__create_order_table.sql
13. Repeatable Migration:可重复迁移
R 开头的是可重复迁移。
例如:
R__refresh_user_order_summary_view.sql
内容:
CREATE OR REPLACE VIEW user_order_summary AS
SELECT
u.id AS user_id,
u.username,
COUNT(o.id) AS order_count,
COALESCE(SUM(o.amount), 0) AS total_amount
FROM sys_user u
LEFT JOIN orders o ON o.user_id = u.id
GROUP BY u.id, u.username;
Repeatable migration 的特点:
- 没有版本号;
- 文件内容变化时会重新执行;
- checksum 变化是触发重新执行的依据;
- 总是在所有待执行的版本迁移之后执行;
- 同一次迁移中,多个
R脚本按描述排序执行。
适合放在 R 脚本里的内容:
- 视图;
- 函数;
- 存储过程;
- package;
- 可整体重刷的基础数据。
可重复迁移一定要保证“重复执行不出错”。所以这类脚本通常会写成:
CREATE OR REPLACE VIEW ...
或者先删除再创建:
DROP VIEW IF EXISTS user_order_summary;
CREATE VIEW user_order_summary AS
SELECT ...
14. 初始化基础数据怎么写?
Flyway 不只能改表结构,也可以写 DML。
比如系统需要初始化角色数据,可以创建:
V4__init_role_data.sql
内容:
CREATE TABLE sys_role (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
role_code VARCHAR(64) NOT NULL,
role_name VARCHAR(64) NOT NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY uk_sys_role_code (role_code)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='角色表';
INSERT INTO sys_role (role_code, role_name)
VALUES
('ADMIN', '管理员'),
('USER', '普通用户');
如果这类数据会经常被整体刷新,也可以使用 R 脚本。但要注意,R 脚本重新执行时可能会重复插入数据,所以要处理幂等。
例如 MySQL 可以这样写:
INSERT INTO sys_role (role_code, role_name)
VALUES
('ADMIN', '管理员'),
('USER', '普通用户')
ON DUPLICATE KEY UPDATE
role_name = VALUES(role_name);
15. 多环境配置:开发、测试、生产怎么区分?
生产环境和开发环境通常不应该执行完全一样的数据脚本。
比如开发环境可能需要一些测试账号:
src/main/resources/dev/db/migration/V999__insert_dev_users.sql
然后在 application-dev.yml 中配置:
spring:
flyway:
locations: classpath:db/migration,classpath:dev/db/migration
这样:
- 公共迁移脚本放在
classpath:db/migration; - 开发环境专用脚本放在
classpath:dev/db/migration; - 只有启用
devprofile 时,开发脚本才会执行。
生产环境不要配置 dev/db/migration。
也可以使用 {vendor} 占位符做数据库类型隔离:
spring:
flyway:
locations: classpath:db/migration/{vendor}
例如 MySQL 会使用:
db/migration/mysql
这适合一个项目需要同时兼容不同数据库的场景。
16. 测试环境中的 Flyway
如果你写集成测试,可以把测试专用迁移放到:
src/test/resources/db/migration
例如:
src/test/resources/db/migration/V9999__test_data.sql
测试启动时,这类脚本只会在测试 classpath 中出现,不会被打包到正式应用中。
这很适合准备集成测试数据:
INSERT INTO sys_user (username, email, status)
VALUES
('test_user_1', 'test1@example.com', 1),
('test_user_2', 'test2@example.com', 1);
注意测试脚本版本号要避免和主迁移冲突。常见做法是使用较大的版本号,例如 V9000、V9999。
17. 旧项目如何接入 Flyway?
新项目接入 Flyway 很简单,从 V1 开始写就行。
麻烦的是旧项目:数据库里已经有几十张表,但之前没有 Flyway 历史。
这种场景要用 baseline。
假设当前生产数据库已经是一个稳定状态,我们希望从这个状态开始让 Flyway 接管,后续新增脚本从 V2 开始。
可以配置:
spring:
flyway:
baseline-on-migrate: true
baseline-version: 1
含义是:
- 如果 Flyway 发现数据库不是空的;
- 并且还没有
flyway_schema_history; - 就自动创建历史表;
- 并把当前数据库标记为 baseline version
1; - 之后
V1及以下版本不会再执行; - 后续从
V2、V3继续迁移。
更稳妥的做法是不要长期打开 baseline-on-migrate。第一次接入时打开,完成后关闭:
spring:
flyway:
baseline-on-migrate: false
对于生产库,建议由发布流程显式执行 baseline,而不是每次应用启动都允许自动 baseline。
18. 常用 Flyway 命令
Spring Boot 集成后,日常大多依赖应用启动自动执行。但理解 Flyway 命令仍然很重要。
migrate
执行迁移:
flyway migrate
它会把数据库迁移到最新版本。如果历史表不存在,Flyway 会自动创建。
info
查看迁移状态:
flyway info
可以看到哪些脚本已执行、哪些待执行、哪些失败、哪些缺失。
validate
校验已执行迁移和本地脚本是否一致:
flyway validate
如果有人修改了已经执行过的脚本,通常会在这里暴露。
baseline
给已有数据库打基线:
flyway baseline
适合老项目首次接入 Flyway。
repair
修复 schema history 表:
flyway repair
它可以做几类事情:
- 移除失败迁移记录;
- 重新对齐 checksum、description、type;
- 把缺失的迁移标记为 deleted。
repair 要谨慎使用。它不是日常解决问题的第一选择,更不能用来掩盖随意修改历史脚本的问题。
clean
清理数据库对象:
flyway clean
这个命令会删除 configured schemas 中的对象。官方也明确提醒不要对生产数据库使用。
本地开发有时会用它重置数据库,但生产环境必须禁用。
19. Spring Boot Actuator 查看 Flyway 状态
如果项目引入了 Actuator,可以通过 /actuator/flyway 查看 Flyway 迁移信息。
引入依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
暴露 endpoint:
management:
endpoints:
web:
exposure:
include: health,info,flyway
请求:
curl http://localhost:8080/actuator/flyway
返回结果里可以看到:
- script;
- version;
- description;
- checksum;
- installedOn;
- executionTime;
- state。
这在排查线上环境版本时很有用。
不过要注意,Actuator endpoint 暴露了应用内部信息,生产环境需要做好权限控制,不要无保护地暴露到公网。
20. 常用配置速查
下面是 Spring Boot 中常用的 Flyway 配置。
spring:
flyway:
enabled: true
locations: classpath:db/migration
table: flyway_schema_history
baseline-on-migrate: false
baseline-version: 1
validate-on-migrate: true
clean-disabled: true
out-of-order: false
placeholder-replacement: true
placeholders:
app_user: flyway_demo
table
指定 Flyway 历史表名称。
默认:
flyway_schema_history
一般不需要改。
out-of-order
是否允许乱序执行。
默认:
spring:
flyway:
out-of-order: false
举个例子:
数据库已经执行到:
V1
V3
这时你又补了一个:
V2
默认情况下,Flyway 不会允许这种“插队”迁移。生产环境通常也不建议打开 out-of-order,否则不同环境的执行顺序可能不一致。
placeholder-replacement
是否启用占位符替换。
默认是 true。
配置:
spring:
flyway:
placeholders:
default_status: ACTIVE
SQL 中使用:
INSERT INTO config_item (config_key, config_value)
VALUES ('user.default.status', '${default_status}');
启动时,Flyway 会把 ${default_status} 替换为 ACTIVE。
21. Flyway 和 JPA 怎么配合?
一个很常见的问题是:用了 JPA,还需要 Flyway 吗?
答案是:生产项目建议需要。
JPA 的实体类描述的是当前模型,而 Flyway 描述的是数据库从旧状态演进到新状态的过程。
比如当前实体是:
@Entity
public class User {
@Id
private Long id;
private String username;
private String email;
}
JPA 能告诉你“现在应该有 email 字段”,但它不适合表达:
- 这个字段什么时候加;
- 是否需要补历史数据;
- 是否需要建索引;
- 字段默认值怎么处理;
- 老数据怎么迁移;
- 多环境怎么按顺序升级。
这些事情应该交给 Flyway。
推荐组合是:
spring:
jpa:
hibernate:
ddl-auto: validate
flyway:
enabled: true
含义是:
- Flyway 负责建表、改表、迁移数据;
- Hibernate 只负责校验实体和数据库是否匹配;
- 如果不匹配,应用启动失败,提醒你补迁移脚本。

551

被折叠的 条评论
为什么被折叠?



