SpringBoot集成Flyway
一、Flyway简介
Flyway 本质上是一个开源的数据库迁移工具,它能够以自动化、可重复且可靠的方式管理数据库的变更。无论是小型项目还是大型企业级应用,Flyway 都能助力开发者轻松应对数据库架构的演进。它支持多种数据库,如常见的 MySQL、Oracle、SQL Server、PostgreSQL 等,这使其在不同的技术栈中都能大显身手。
二、FLyway可以解决什么问题
2.1 项目场景思考
我们的产品具备支持用户在本地进行部署的特性,不同用户基于自身业务需求,往往会有特定的数据库选择。例如,A 用户因业务的复杂性和对数据处理能力的高要求,选择了 Oracle 数据库;B 用户由于项目对开源数据库的偏好以及对地理信息数据处理的需求,采用了 PostgreSQL 数据库;C 用户则考虑到成本和简易性,选用了 MySQL 数据库。
由于各类数据库在语法和功能实现上存在方言差异,这使得我们无法使用同一套 SQL 语句来完成对不同数据库的初始化操作。因此,我们针对每种数据库分别精心准备了一套专属的初始化 SQL 脚本,以确保数据库能够顺利且正确地初始化。
然而,在产品的演进过程中,产品团队发现早期设计的数据库表结构已无法满足当前业务发展的需求,亟待优化。具体而言,需要为某个核心数据库表新增若干字段,并且要对历史数据中这些新增字段的值进行刷新填充。为了适配不同数据库的语法规则,我们不得不为每个数据库单独编写一份全新的 SQL 脚本,以实现这一优化目标。
为了便于后期对这些脚本进行管理,我们将所有相关的 SQL 脚本进行了系统归档。每当为新用户进行首次产品部署时,这些 SQL 脚本都必须按照顺序从头至尾完整地执行一遍,以确保数据库具备最新且正确的结构和数据状态。
随着产品的持续迭代升级,我们所支持的数据库种类日益增多,相应地,为应对数据库结构变化而新增的 SQL 脚本数量也急剧增长。这不仅导致了脚本管理的复杂度大幅提升,也使得每次部署时执行脚本的时间成本和人力成本显著增加。此外,人工频繁介入数据库变更操作,不可避免地提高了出错的概率,一旦出现错误,可能会对数据库的稳定性和数据的完整性造成严重影响。
2.2 为什么选择Flyway
Flyway有以下几个特性:
- 简单易用:Flyway 的使用极为便捷,仅需少量配置,即可快速上手。开发者只需遵循特定的命名规则编写 SQL 脚本,Flyway 就能自动识别并按顺序执行这些脚本,完成数据库的迁移操作。
- 版本控制:Flyway 为数据库迁移提供了完善的版本控制机制。每一次迁移操作都会被记录在数据库的特定表(通常是 flyway_schema_history 表)中,详细记录迁移的版本号、描述、执行时间以及校验和等信息。这使得开发者可以清晰地追踪数据库的变更历史,方便进行回滚或向前迁移到特定版本。
- 自动迁移:应用启动时,Flyway 会自动检测是否有未执行的迁移脚本。若存在,它会按照版本号顺序依次执行这些脚本,将数据库更新到最新状态。这种自动迁移功能极大地减少了人工干预,提高了开发和部署的效率。
- 可重复性:Flyway 的迁移操作具有高度的可重复性。无论在开发环境、测试环境还是生产环境,只要迁移脚本和配置相同,Flyway 就能保证数据库被迁移到一致的状态。这为团队协作开发以及持续集成 / 持续部署(CI/CD)流程提供了有力支持。
- 支持多种数据库:如前文所述,Flyway 对多种主流数据库的支持使其具备广泛的适用性。无论是使用轻量级的 MySQL 进行快速开发,还是采用 Oracle 应对企业级复杂业务场景,Flyway 都能无缝衔接,为不同数据库提供统一的迁移管理体验。
Flyway可以为我们解决的问题:
- 数据库变更管理混乱:在软件开发过程中,数据库架构常常需要随着业务需求的变化而不断调整。传统的手动管理数据库变更方式容易导致混乱,如脚本丢失、版本不一致、执行顺序错误等问题。Flyway 通过其版本控制和自动迁移功能,有效地解决了这些问题,确保数据库变更的有序进行。
- 多环境一致性难题:开发、测试、生产等不同环境的数据库状态需要保持一致,以避免因环境差异导致的问题。Flyway 的可重复性和自动迁移特性使得在不同环境中都能轻松实现数据库的同步更新,保证了各个环境下数据库的一致性。
- 团队协作障碍:在团队开发中,多个开发者可能同时对数据库进行变更。如果没有有效的协调机制,很容易出现冲突和错误。Flyway 通过统一的版本控制和清晰的迁移记录,方便团队成员了解数据库的变更情况,减少协作过程中的误解和冲突。
- 部署效率低下:手动执行数据库迁移脚本不仅繁琐,而且容易出错,特别是在频繁部署的项目中。Flyway 的自动化迁移功能大大提高了部署效率,减少了部署过程中的人为失误,使数据库部署更加快速、可靠。
三、SpringBoot集成Flyway示例
Sping已经封装了Flyway的自动装配(org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration),只要我们引入相关依赖即可实现集成使用。
演示环境说明:
- SpringBoot版本:2.7.18
- MySQL版本:8.0.26
- Flyway版本:8.5.13
项目结构:
3.1 pom依赖
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.33</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-core</artifactId>
</dependency>
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-mysql</artifactId>
</dependency>
</dependencies>
注意:我使用的SpringBoot版本为2.7.18
,集成的Flyway版本为8.5.13
,而Flyway在8.2.1
以上的版本就移除了对MySQL的依赖,所以需要额外引入flyway-mysql
依赖。
3.2 配置文件 application.yaml
spring:
# 数据库连接
datasource:
url: jdbc:mysql://192.168.33.10:3306/flyway_demo?serverTimezone=Asia/Shanghai
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
# Flyway 配置
flyway:
# 是否开启自动装配,默认为 true
enabled: true
# flyway迁移脚本路径,默认为 classpath:db/migration
locations: classpath:db/migration
# 迁移到非空schema时,是否调用基线方法,默认为false
baseline-on-migrate: true
# 是否禁止清理执行,flyway的清理功能会把整个数据库清空,可在本地环境使用,线上环境一定要禁用,默认值为false
clean-disabled: true
# 开启flyway的执行日志
logging:
level:
org:
flywaydb: debug
3.3 启动类
/**
* 启动类
*
* @author Jacks丶
* @since 2025-03-23
*/
@SpringBootApplication
public class TempApplication {
public static void main(String[] args) {
SpringApplication.run(TempApplication.class, args);
}
}
3.4 Flyway迁移SQL脚本db/migration/V1.0.1__create_student_table.sql
-- 创建学生表
create table if not exists `t_student`(
`id` varchar(32) primary key not null comment '主键ID',
`name` varchar(18) comment '姓名',
`age` int comment '年龄',
`grade` float comment '成绩',
`createTime` timestamp comment '创建时间',
`updateTime` timestamp comment '修改时间'
);
-- 添加初始数据
insert into `t_student`(`id`, `name`, `age`, `grade`, `createTime`, `updateTime`)
values (replace(uuid(), '-', ''), 'Jacks', 18, 60.0, now(), now()),
(replace(uuid(), '-', ''), 'Jacks2', 18, 70.0, now(), now());
3.5 启动项目查看结果
如果前面配置了logging.level.org.flywaydb=DEBUG
,那么可以在控制台看到SQL执行信息。
数据库信息如下:
3.6 新增迁移脚本
此时我们需要在t_student
表中添加总分字段,并且需要计算成绩之和,那我们增加一个SQL脚本V1.0.2__add_column_total_grade.sql
-- 添加表字段
alter table `t_student` add column (`total_grade` float comment '总成绩');
-- 刷新字段值
update `t_student` set `total_grade`=(`math_grade` + `computer_grade`);
启动项目查看数据库
至此,一个基本的SpringBoot集成Flyway示例就完成了。
四、Flyway迁移脚本说明
通过上面的示例演示,我们可以知道,每当新增一个迁移SQL时,Flyway会为我们自动去执行新增脚本,前提是必须按照命名规则来命名Flyway迁移脚本。
Flyway有两种命名类型的文件:
- 版本化迁移脚本:
- 命名格式:
V{版本号}__{描述信息}
- 说明:大写
V
开头,后跟版本号,版本号使用点号或下划线分隔数字,如V1_0
或V1.0
。版本号后接双下划线__
,再加上描述性文本,最后以.sql
结尾,例如V1.0__Create_users_table.sql
,描述性文本中的下划线会被转换为空格存入flyway_schema_history
。 - 注意事项:版本号必须唯一且按升序排列,Flyway 依此顺序执行迁移。如果先执行高版本迁移脚本,低版本迁移脚本就不会执行,所以在协作开发场景下,需要团队一同制定命名规则和维护版本号。
- 命名格式:
- 可重复迁移脚本:
- 命名格式:
R__{描述信息}
- 说明:以大写R开头,后接双下划线
__
,再添加描述性文本,同样以.sql
结尾,像R__Add_index_to_orders.sql
。 - 注意事项:重复执行的条件是迁移脚本内容发生变化时才会重复执行脚本。
- 命名格式:
五、博主遇到的问题及解决方案
5.1 修改已执行过的版本化迁移脚本后,程序启动会报错
报错信息:
Caused by: org.flywaydb.core.api.exception.FlywayValidateException: Validate failed: Migrations have failed validation
Migration checksum mismatch for migration version 1.0.1
解决方案:
- 方案一:回退修改脚本,约定不允许修改已上线的版本化迁移脚本。
- 方案二:使用Flyway的repair命令自动修正,不过Spring并没有提供相关配置,需要我们自定义Flyway的Bean对象,并控制其行为,步骤如下所示。
步骤一:关闭自动装配
spring:
flyway:
# 是否开启自动装配,默认为 true
enabled: false
或
/**
* 启动类
*
* @author Jacks丶
* @since 2025-03-23
*/
@SpringBootApplication(exclude = {FlywayAutoConfiguration.class})
public class TempApplication {
public static void main(String[] args) {
SpringApplication.run(TempApplication.class, args);
}
}
步骤二:仿造Spring自动装配,手动初始化Flyway对象,并在执行迁移方法migrate之前先执行repair方法。
import lombok.extern.slf4j.Slf4j;
import org.flywaydb.core.Flyway;
import org.springframework.boot.autoconfigure.flyway.FlywayProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.sql.DataSource;
/**
* Flyway注入配置类
*
* @author Jacks丶
* @since 2025-03-23
*/
@Configuration
@EnableConfigurationProperties(FlywayProperties.class)
@Slf4j
public class FlywayConfig {
@Bean
public Flyway flyway(DataSource dataSource, FlywayProperties properties) {
Flyway flyway = Flyway.configure()
.dataSource(dataSource)
.locations(properties.getLocations().toArray(new String[0]))
.cleanDisabled(true)
.baselineOnMigrate(true)
.load();
// 执行修复
flyway.repair();
// 执行迁移
flyway.migrate();
return flyway;
}
}
5.2 flyway_schema_history有失败执行记录时,程序启动会失败报错
报错信息:
Detected failed migration to version 1.0.2 (add column total grade).
Please remove any half-completed changes then run repair to fix the schema history.
参考5.1方案二进行解决。
5.3 项目中依赖使用的是Hibernate,期望使用Hibernate创建表,Flyway仅初始化基础数据。
问题场景:由于Flyway执行SQL脚本会比Hibernate初始化数据库表快,此时Flyway会报错表不存在,导致整个程序启动失败。
解决方案:让Flyway的Bean注入时期延后,Hibernate初始化数据库的阶段是在刷新ApplicationContext时期完成,所以我们需要晚于此阶段注入Bean对象,所以我们可以监听SpringBoot的生命周期事件ContextRefreshedEvent
,在此阶段后注入Bean对象。
import lombok.extern.slf4j.Slf4j;
import org.flywaydb.core.Flyway;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.flyway.FlywayProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.stereotype.Component;
import javax.sql.DataSource;
/**
* ContextRefreshedEvent事件监听类,用于注册Flyway,保证Flyway晚于Hibernate执行。
*
* @author Jacks丶
* @since 2025-03-23
*/
@Slf4j
@Component
@EnableConfigurationProperties(FlywayProperties.class)
public class FlywayAfterHibernateListener implements ApplicationListener<ContextRefreshedEvent> {
@Autowired
private DataSource dataSource;
@Autowired
private FlywayProperties flywayProperties;
@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
log.info("flyway 初始化...");
Flyway flyway = Flyway.configure()
.dataSource(dataSource)
.locations(flywayProperties.getLocations().toArray(new String[0]))
.cleanDisabled(true)
.baselineOnMigrate(true)
.load();
// 执行修复
flyway.repair();
// 执行迁移
flyway.migrate();
}
}
5.4 多种数据库类型迁移版本控制
当项目支持多种数据源时,需要单独配置每种数据库的Flyway迁移脚本,核心点在于控制Flyway读取脚本的路径。
5.4.1 场景一:数据库类型在Spring中有标准定义
如果数据库类型在org.springframework.boot.jdbc.DatabaseDriver
有定义,Flyway默认支持根据不同的数据库读取不同的脚本文件,只需要配置spring.flyway.locations
即可实现自动切换读取脚本的路径。
spring:
flyway:
locations: classpath:db/migration/{vendor}
适配MySQL的脚本,我们的脚本就需要放在db/migration/mysql下。
适配Oracle的脚本,我们的脚本就需要放在db/migration/oracle下。
说明:占位符
{vendor}
是默认使用org.springframework.boot.jdbc.DatabaseDriver
中的枚举名称小写,原理是根据JDBC的URL连接解析数据库连接驱动的类型,从而得到数据库类型,逻辑写在自动装配类中,详情见:org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration.LocationResolver#resolveLocations
5.4.2 场景二:数据库类型在Spring中没有标准定义
项目使用的数据库不在org.springframework.boot.jdbc.DatabaseDriver
中,比如我们的国产数据库,那么就需要我们自己写适配逻辑,核心点在于核心点在于控制Flyway读取脚本的路径,可参照5.1方案二自定义FLyway注入来控制locations参数的赋值逻辑。
定义一个数据库类型的配置变量my-project.database.type=dm
my-project:
database:
type: MySQL
import lombok.extern.slf4j.Slf4j;
import org.flywaydb.core.Flyway;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.flyway.FlywayProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.sql.DataSource;
/**
* Flyway注入配置类
*
* @author Jacks丶
* @since 2025-03-23
*/
@Configuration
@EnableConfigurationProperties(FlywayProperties.class)
@Slf4j
public class FlywayConfig {
@Value("${my-project.database.type:mysql}")
private String databaseType;
@Bean
public Flyway flyway(DataSource dataSource, FlywayProperties properties) {
Flyway flyway = Flyway.configure()
.dataSource(dataSource)
//.locations(properties.getLocations().toArray(new String[0]))
.locations(String.join("/", properties.getLocations().get(0), Strings.toRootLowerCase(databaseType)))
.cleanDisabled(properties.isCleanDisabled())
.baselineOnMigrate(properties.isBaselineOnMigrate())
.load();
// 执行修复
flyway.repair();
// 执行迁移
flyway.migrate();
return flyway;
}
}
六、补充知识
6.1 flyway的核心命令
- migrate:该命令会检查数据库中的迁移历史表(默认是 flyway_schema_history),找出尚未执行的迁移脚本,并按照版本号顺序依次执行这些脚本。执行成功后,数据库的结构会更新到最新的迁移版本,迁移历史表中会记录每个执行过的迁移脚本的相关信息,如版本号、描述、执行时间、校验和等。
- clean:此命令会删除指定数据库中的所有对象(表、视图、存储过程等),但不会删除数据库本身。数据库中的所有对象都会被清除,恢复到初始状态。需要注意的是,在生产环境中使用该命令要格外谨慎,因为它会导致数据永久丢失。
- info:查询迁移历史表,获取所有迁移脚本的详细信息,包括脚本的版本号、描述、状态(已应用、未应用、失败等)、执行时间、校验和等。会在控制台输出一个表格,展示每个迁移脚本的详细信息,方便用户了解数据库的迁移状态。
- validate:检查迁移历史表中的校验和与迁移脚本的当前校验和是否一致,同时验证迁移脚本的版本号顺序是否正确。如果所有校验和都一致且版本号顺序正确,会输出验证成功的信息;否则,会输出验证失败的详细信息,提示哪些脚本的校验和不匹配或版本号顺序有误。
- undo:将数据库状态回滚到指定的版本。该命令会按照相反的顺序执行已经应用的迁移脚本的逆向操作(如果有的话),直到达到指定的版本。数据库的结构会回滚到指定版本的状态,迁移历史表中会更新相应的记录。需要注意的是,并非所有的迁移脚本都支持逆向操作,因此有些情况下可能无法完全回滚。
- repair:修复迁移历史表中的不一致问题。具体包括移除失败的迁移记录、纠正校验和、调整版本排序等。迁移历史表会被修复,恢复到一致状态,后续的迁移操作可以正常进行。
- baseline:为一个已经存在的数据库创建迁移历史表,并将当前数据库状态标记为指定的基线版本。通常在将 Flyway 引入一个已经存在的项目时使用。会在数据库中创建迁移历史表,并插入一条记录,将当前数据库状态标记为基线版本,后续的迁移操作会从该基线版本开始。
七:参考链接
- Spring自动装配的配置说明:https://docs.spring.io/spring-boot/docs/2.7.18/reference/html/application-properties.html#appendix.application-properties.data-migration
- Flyway官网:https://documentation.red-gate.com/fd/quickstart-guides-237929933.html
- Flyway详解(使用说明及避坑指南、一文搞懂flyway)