Flyway 数据库版本管理工具使用指南
#springboot #java
一、为什么需要 Flyway
在引入 Flyway 之前,数据库变更依赖人工执行 SQL,存在以下问题:
- 上线时忘记执行 SQL,导致应用报错
- 同一条 SQL 被重复执行,破坏数据结构
- 各环境数据库结构不一致,测试通过但生产报错
- 没有记录哪些 SQL 执行过、什么时候执行的
Flyway 的作用:让数据库结构变更像代码一样被版本管理,应用启动时自动执行还未执行过的 SQL,已执行过的绝不重复执行。
二、工作原理
Flyway 启动时做三件事:
① 扫描 resources/db/migration/ 目录下所有 SQL 文件
② 查询数据库中的 flyway_schema_history 表,确认哪些已执行
③ 按版本号顺序,只执行尚未执行的文件
flyway_schema_history 是 Flyway 自动创建和维护的记录表,无需手动创建:
+---------+---------------------------+---------+---------------------+
| version | description | success | installed_on |
+---------+---------------------------+---------+---------------------+
| 1.0.1 | init | 1 | 2026-01-10 10:00:00 |
| 1.0.2.1 | add user ext table | 1 | 2026-02-15 14:30:00 |
| 1.0.2.2 | add order pay column | 1 | 2026-02-15 14:30:01 |
| 1.0.2.3 | add index order status | 1 | 2026-02-15 14:30:01 |
| 1.0.2.4 | dict payment type | 1 | 2026-02-15 14:30:02 |
+---------+---------------------------+---------+---------------------+
三、项目集成
3.1 添加 Maven 依赖
在 pom.xml 中添加:
<!-- Flyway 核心 -->
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-core</artifactId>
</dependency>
<!-- MySQL 支持(使用 MySQL 必须加) -->
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-mysql</artifactId>
</dependency>
Spring Boot 2.7+ 已内置 Flyway 版本管理,无需指定版本号。
3.2 application.yml 配置
spring:
flyway:
enabled: true # 开启 Flyway
locations: classpath:db/migration # SQL 文件目录
baseline-on-migrate: true # 老项目首次接入时不报错,新项目可不加
validate-on-migrate: true # 启动时校验文件完整性,强烈建议开启
table: flyway_schema_history # 记录表名(默认值,可不写)
各环境数据库连接各自配置,Flyway 会自动作用于对应的数据库,无需额外区分。
3.3 目录结构
src/main/resources/
db/
migration/
V1.0.1__init.sql
V1.0.2.1__add_user_ext_table.sql
V1.0.2.2__add_order_pay_column.sql
V1.0.2.3__add_index_order_status.sql
V1.0.2.4__dict_payment_type.sql
R__recreate_view_order_summary.sql
四、文件命名规范
4.1 完整格式
{前缀}{版本号}__{描述}.sql
示例:V1.0.2.1__add_user_ext_table.sql
4.2 前缀说明
| 前缀 | 名称 | 行为 | 适用场景 |
|---|---|---|---|
V | 版本化迁移 | 只执行一次,执行后不再重复 | 建表、加字段、加索引、数据初始化 |
R | 可重复迁移 | 文件内容变化就重新执行 | 视图、存储过程、函数 |
U | 撤销迁移 | 对应 V 文件的回滚脚本 | 社区版不支持,了解即可 |
4.3 版本号规则
版本号中的单下划线 _ 会被自动替换为点 .,以下两种写法完全等价:
V1.0.2.1__add_user_table.sql ✅
V1_0_2_1__add_user_table.sql ✅(等价写法)
建议统一使用点号,更直观。
4.4 分隔符
两个下划线 __ 是版本号和描述的分隔符,一个下划线是版本号内部的分隔,不要混淆:
V1.0.2.1__add_user_table.sql
───────── ───────────────
版本号 描述(单下划线会显示为空格)
V1.0.2_add_user_table.sql ❌ 错误:Flyway 会把描述也当成版本号一部分
4.5 描述规则
描述中的单下划线会在记录表中显示为空格,只影响可读性,不影响执行:
文件名 history 表 description 显示
V1.0.2.2__add_order_pay_column → add order pay column
R__recreate_view_order_summary → recreate view order summary
五、同一版本多类型变更
一个版本内有多种类型的变更时,用小版本号区分执行顺序,严格按以下顺序排列:
V1.0.2.1__xxx.sql ← 第一步:新建表
V1.0.2.2__xxx.sql ← 第二步:现有表加字段
V1.0.2.3__xxx.sql ← 第三步:加索引(依赖字段存在)
V1.0.2.4__xxx.sql ← 第四步:字典 / 初始化数据(依赖表和字段存在)
顺序不能乱,索引依赖字段,数据依赖表结构,写错顺序启动直接报错。
六、SQL 编写规范
6.1 建表:加 IF NOT EXISTS
-- V1.0.2.1__add_user_ext_table.sql
CREATE TABLE IF NOT EXISTS `sys_user_ext` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`user_id` BIGINT NOT NULL COMMENT '关联用户ID',
`nickname` VARCHAR(64) DEFAULT NULL COMMENT '昵称',
`avatar` VARCHAR(255) DEFAULT NULL COMMENT '头像URL',
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `idx_user_id` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户扩展信息';
6.2 加字段:先判断列是否存在
MySQL 8.0 以下不支持 ADD COLUMN IF NOT EXISTS,用存储过程判断:
-- V1.0.2.2__add_order_pay_column.sql
DROP PROCEDURE IF EXISTS add_column_if_not_exists;
DELIMITER //
CREATE PROCEDURE add_column_if_not_exists()
BEGIN
IF NOT EXISTS (
SELECT 1 FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 't_order'
AND COLUMN_NAME = 'pay_channel'
) THEN
ALTER TABLE `t_order`
ADD COLUMN `pay_channel` VARCHAR(32) DEFAULT NULL COMMENT '支付渠道' AFTER `pay_type`,
ADD COLUMN `refund_amount` DECIMAL(10,2) DEFAULT 0.00 COMMENT '退款金额' AFTER `pay_channel`;
END IF;
END //
DELIMITER ;
CALL add_column_if_not_exists();
DROP PROCEDURE IF EXISTS add_column_if_not_exists;
6.3 加索引:先判断索引是否存在
-- V1.0.2.3__add_index_order_status.sql
DROP PROCEDURE IF EXISTS add_index_if_not_exists;
DELIMITER //
CREATE PROCEDURE add_index_if_not_exists()
BEGIN
IF NOT EXISTS (
SELECT 1 FROM information_schema.STATISTICS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 't_order'
AND INDEX_NAME = 'idx_status_created'
) THEN
ALTER TABLE `t_order` ADD INDEX `idx_status_created` (`status`, `created_at`);
END IF;
END //
DELIMITER ;
CALL add_index_if_not_exists();
DROP PROCEDURE IF EXISTS add_index_if_not_exists;
6.4 字典 / 初始化数据:保证幂等
-- V1.0.2.4__dict_payment_type.sql
-- 用 INSERT IGNORE 避免重复插入报错
INSERT IGNORE INTO `sys_dict_item` (`dict_code`, `item_value`, `item_label`, `sort`) VALUES
('PAYMENT_TYPE', 'WECHAT', '微信支付', 1),
('PAYMENT_TYPE', 'ALIPAY', '支付宝', 2),
('PAYMENT_TYPE', 'BALANCE', '余额支付', 3);
6.5 视图 / 存储过程:用 R 前缀
-- R__recreate_view_order_summary.sql
-- 视图用 CREATE OR REPLACE,文件内容修改后 Flyway 自动重新执行
CREATE OR REPLACE VIEW `v_order_summary` AS
SELECT
u.name AS user_name,
COUNT(o.id) AS order_count,
SUM(o.amount) AS total_amount
FROM `t_order` o
JOIN `sys_user` u ON o.user_id = u.id
GROUP BY u.id, u.name;
七、铁律:已提交的 V 文件绝对不能修改
Flyway 对每个 V 文件计算 checksum,修改已执行过的文件内容,下次启动会报错:
Validate failed: Migration checksum mismatch for migration version 1.0.2.1
Expected: 112233445 Resolved: 998877665
✅ 正确做法:新建更高版本的文件来修正
# 发现 V1.0.2.1 建表时漏加了字段
# ❌ 错误:直接修改 V1.0.2.1__add_user_ext_table.sql
# ✅ 正确:新建文件
V1.0.2.5__fix_user_ext_add_remark.sql ← 用新文件补充
R 文件(可重复迁移)可以随意修改,这是它存在的意义。
八、多人协作版本号冲突问题
多人同时开发时,最容易出现两个人都创建了 V1.0.2.x 的文件,合并时 Flyway 报版本冲突。
推荐方案:使用时间戳作为版本号
V20260324141500__add_user_ext_table.sql
V20260324153020__add_order_pay_column.sql
时间戳精确到秒,绝对不会冲突,强烈推荐多人并行开发时使用。
九、与 CI/CD 流程的配合
结合项目现有的 GitLab CI/CD 流程,无需在流水线中单独处理数据库变更,Flyway 在应用启动时自动执行。
dev 提交代码(含新的 V1.0.2.x__.sql)
└─ CI 构建镜像,部署到开发环境
└─ 容器启动 → Flyway 自动执行新 SQL → 开发库结构更新 ✅
MR 合并到 release → 部署测试环境
└─ 容器启动 → Flyway 自动执行新 SQL → 测试库结构更新 ✅
MR 合并到 main → 手动触发部署生产
└─ 容器启动 → Flyway 自动执行新 SQL → 生产库结构更新 ✅
SQL 文件跟着代码一起走,同一版本的代码和数据库结构永远对齐,不再出现代码升级了但数据库忘改的情况。
十、常见问题
Q:老项目已有数据库,第一次接入 Flyway 怎么办?
确保 application.yml 中配置了 baseline-on-migrate: true,Flyway 会把当前数据库状态标记为基线,只执行基线之后的新文件,不会尝试重新执行历史 SQL。
Q:启动报 checksum mismatch 怎么处理?
说明有人修改了已执行过的 V 文件,找到被修改的文件,将内容恢复原样(git 回退),然后用新版本文件来实现需要的变更。
Q:想回滚某次数据库变更怎么办?
新建一个更高版本的文件,手动写回滚 SQL(如 DROP COLUMN、DROP TABLE)。Flyway 社区版不支持自动回滚,生产环境回滚数据库变更需谨慎评估,建议提前准备好回滚脚本。
Q:本地开发时频繁改表结构,要一直新建 V 文件吗?一次迭代改了 10 次难道要建 10 个文件?
不需要。V 文件记录的是"变更结果",不是"变更过程"。 本地开发分两个阶段来处理:
开发阶段(功能未完成):本地直接用 Navicat / DBeaver 等工具随意改表结构,同时关闭本地的 Flyway,不产生任何 V 文件:
# application-local.yml 本地专用配置,不提交到代码仓库
spring:
flyway:
enabled: false # 开发期间关掉,随便改表结构
提 MR 前(功能完成):将本次所有数据库变更整理归纳成最终结果,写成一个或几个干净的 V 文件一起提交。不管本地改了几次,对外只体现最终状态:
# 本地反复改了 10 次字段,但 MR 里只提交最终版本:
V1.0.5.1__add_product_table.sql ← 建表(最终结构)
V1.0.5.2__add_order_status_column.sql ← 加字段(最终版本)
V1.0.5.3__add_index_product_name.sql ← 加索引
这和写代码是一样的道理:本地可以反复修改,但合并到分支的是整理好的最终结果,不是每一次中间过程的记录。
Q:菜单路由、字典数据之前是在页面维护再同步到其他环境,改用 Flyway 管会重复插入吗?
这类数据要区分两种情况分别处理:
情况一:系统内置数据,上线后不在页面改(如内置字典类型、系统默认菜单)
适合放 V 文件,随代码一起发布。用 INSERT IGNORE 写法保证幂等,即使意外重复执行也不会报错:
-- V1.0.5.4__init_menu_data.sql
INSERT IGNORE INTO `sys_menu` (`id`, `name`, `path`, `component`) VALUES
(1001, '用户管理', '/user', 'user/index'),
(1002, '角色管理', '/role', 'role/index');
INSERT IGNORE INTO `sys_dict_item` (`dict_code`, `item_value`, `item_label`) VALUES
('ORDER_STATUS', '0', '待支付'),
('ORDER_STATUS', '1', '已支付');
正常情况下 Flyway 根本不会重复执行 V 文件,INSERT IGNORE 只是双重保险。
情况二:各环境数据不一致,由运营 / 管理员在页面配置维护的数据
这类数据不适合放 Flyway,强行用 SQL 管反而容易覆盖掉各环境独立维护的数据。建议:
- 只管新增:新版本新加了一个菜单项,写 V 文件只插入这一条新记录,不动存量数据
- 存量继续发布单:已有的业务数据继续通过现有发布单流程同步,两者互不干扰
一句话原则:Flyway 管好结构变更和系统初始数据就够了,不要试图用它接管所有数据。
附一:各场景处理方式速查
| 场景 | 推荐做法 |
|---|---|
| 本地开发频繁改表结构 | 本地关闭 Flyway,提 MR 前整理成干净的 V 文件 |
| 系统内置字典、初始菜单 | V 文件 + INSERT IGNORE,随代码发布 |
| 运营在页面维护的业务数据 | 只写新增部分的 V 文件,存量继续发布单 |
| 视图、存储过程 | R 文件,内容可随时修改,自动重新执行 |
| 多人并行开发 | 版本号改用时间戳,避免冲突 |
附二:文件命名速查
V{版本号}__{描述}.sql 版本化迁移,只执行一次
R__{描述}.sql 可重复迁移,内容变了就重新执行
─────────────────────────────────────────────────
V1.0.1__init.sql 初始化建表
V1.0.2.1__add_xxx_table.sql 新建表
V1.0.2.2__add_xxx_column.sql 加字段
V1.0.2.3__add_idx_xxx.sql 加索引
V1.0.2.4__dict_xxx.sql 字典数据
R__recreate_view_xxx.sql 视图
R__recreate_proc_xxx.sql 存储过程