Springboot集成Flyway详解
1、背景
随着项目的增多,各个项目的版本之间存在差异,因此在升级时,维护项目版本和最新版本之间增量的sql脚本成为一个严重的问题,非常耗时耗力,因此引入一个数据库变更管理工具迫在眉睫。目前比较常用的有flyway和liquibase,liquibase使用xml文件来定义和管理数据库脚本,不依赖于具体的数据库,学习成本相对较高,功能丰富,有自己的一套语法规则;flyway是基于sql语句的,依赖于具体的数据库,使用较为简单,容易上手。我们的项目固定是mysql的数据库,且项目开发周期短,因此引入了flyway。
2、Flyway介绍
官方文档地址:DocumentationFlyway
flyway实现数据变更管理的大致原理如下:使用一张表专门记录每一个sql脚本的执行情况,执行过的脚本不在重复执行,没有执行过的脚本在程序启动时自动执行。因此我们使用springboot集成flyway进行数据库变更管理,只需要如下两步:
- 将sql脚本按命名规则命名
- 将脚本存放到指定位置
2.1、文件命名规则
Flyway的脚本名称有一定的规则,一个完整的脚本名称由五部分组成:前缀+版本+分隔符+描述+文件后缀,前缀、分隔符和文件后缀,都可以通过配置项进行修改。下面介绍的是默认的配置。
前缀:V表示版本迁移,版本号必须唯一,会计算校验和,执行过的文件不允许修改,按版本顺序执行,一个脚本只会执行一次;
U表示撤销迁移,和V开头的版本号相同,表示撤销当前版本的迁移,sql文件编写与版本迁移相反作用的语句即可。
R表示可重复迁移,其没有版本,不是只运行一次,而是每次程序启动,校验发生修改时,就会执行,R始终是在V执行完成后再执行。
版本号:任意的数字版本均可,可以是一个完整的数字或者用点符号分割的数字,版本之间从左到右进行比较大小,遇到点符号进行分割,以0开头的数字会自动移除前面的0后比较大小,如下均是合法的版本:
- 1
- 001
- 6.3
- 1.2.3.4
- 2024.07.30
- 20240730
分割符:默认是两个下划线(__)作为分隔符,分割版本和描述。
描述:官方文档上说明是:下划线或者空格分割单词即可。实际短横线分割单词也可以,理论上只要不和分割符冲突即可。
文件后缀:默认是 .sql
如下均是合法的脚本名称:
V1.0.0__init.sql V1.0.002__add-new-table.sql V2023.0709__add_new_table.sql R__add_new_table.sql |
详情请参考官方文档地址: 命名规则
2.2、常用配置项
spring可能因为版本原因部分参数没有,根据实际版本配置
flyway参数名 | 默认值 | 描述 | spring配置项 |
enabled | true | 是否开启flyway | spring.flyway.enabled=true |
baselineOnMigrate | false | 开启基线迁移 | spring.flyway.baseline-on-migrate=false |
baselineVersion | 1 | 基线版本 | spring.flyway.baseline-version=1 |
batch | false | 批量执行sql语句,提升插入、更新和删除语句效率 | spring.flyway.batch=false |
callbacks | db/callback | 指定java的回调,值为包的全限定名 | 无 |
cleanDisabled | true | 执行前清除数据库中的表,生产环境务必设置成true | spring.flyway.clean-disabled=fasle |
cleanOnValidationError | false | 脚本校验发生错误时(修改已执行的脚本),是否自动clean,生产环境务必禁用 | spring.flyway.clean-on-validation-error=false |
executeInTransaction | true | sql是否在事务中执行 | 无 |
outOfOrder | false | 是否允许不按版本号顺序执行,多个人员同时开发下有用 | spring.flyway.out-of-order=false |
placeholderReplacement | true | 是否替换占位符,默认占位符形如${xxx} | spring.flyway.placeholder-replacement=false |
lockRetryCount | 50 | 迁移开始,flyway尝试获取锁,防止多实例并行执行,获取锁失败,则每15秒重试一次,直到重试次数则放弃迁移(flyway采用数据库排它锁实现) | spring.flyway.lock-retry-count=50 |
示例截图:
更多详细的配置项使用参考官方文档地址:Flyway配置项
3、项目集成Flyway
3.1、依赖引入
基于SpringBoot搭建的项目,如下引入即可,会自动引入关联的版本
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-core</artifactId>
</dependency>
3.2、相关配置项
配置项可以使用spring的配置文件设置,也可以通过Java代码进行设置
3.2.1、Spring中的配置项
spring:
flyway:
#数据库存在表时,自动使用设置的基线版本(baseline-version),数据库不存在表时,即使设置了,也会从第一个版本开始执行,默认值为false
baseline-on-migrate: true
#基线版本号,baseline-on-migrate为true时小于等于此版本号的脚本不会执行,默认值为1
baseline-version: 1.0.0
#设置为false会删除指定schema下所有的表,生产环境务必禁用,spring中该参数默认是false,需要手动设置为true
clean-disabled: true
#sql脚本存放位置,允许设置多个location,用英文逗号分割,默认值为classpath:db/migration
locations: classpath:db/migration,classpath:db/callback
#是否替换sql脚本中的占位符,占位符默认是${xxx},默认是替换,如果不需要替换,可以设置为false
placeholder-replacement: false
更多详细的配置,可以参考上面的官方配置文档,或者根据spring的相关参数提示查看。
3.2.2、Java代码实现
下面给出简单的代码示例(以下代码未经过测试,仅供参考):
@Configuration
public class FlywayConfig {
@Autowired
private DataSource dataSource;
@PostConstruct
public void config() {
Flyway flyway = Flyway.configure()
.dataSource(dataSource)
.baselineOnMigrate(true)
.locations("db/migration")
.baselineVersion("1.0.1")
.cleanDisabled(true)
.load();
flyway.migrate();
}
}
详细的相关的java方法调用参考:Flyway-Java文档
3.3、sql脚本存放位置
通过配置项spring.flyway.locations设置,默认值是classpath:db/migration,也可以参数配置指定位置(截图就是自定义的location位置),多个location通过英文逗号分割开
3.4、回调操作
flyway支持在任意操作(migrate、undo、clean、info、validate、baseline和repair)的前后,执行自定义的代码或者脚本,下面给一份支持的事件表格,官网文档:Flyway callback
Migrate | Execution |
beforeMigrate | Before Migrate runs |
beforeRepeatables | Before all repeatable migrations during Migrate |
beforeEachMigrate | Before every single migration during Migrate |
beforeEachMigrateStatement | Before every single statement of a migration during Migrate |
afterEachMigrateStatement | After every single successful statement of a migration during Migrate |
afterEachMigrateStatementError | After every single failed statement of a migration during Migrate |
afterEachMigrate | After every single successful migration during Migrate |
afterEachMigrateError | After every single failed migration during Migrate |
afterMigrate | After successful Migrate runs |
afterMigrateApplied | After successful Migrate runs where at least one migration has been applied |
afterVersioned | After all versioned migrations during Migrate |
afterMigrateError | After failed Migrate runs |
beforeUndo | Before Undo runs |
beforeEachUndo | Before every single migration during Undo |
beforeEachUndoStatement | Before every single statement of a migration during Undo |
afterEachUndoStatement | After every single successful statement of a migration during Undo |
afterEachUndoStatementError | After every single failed statement of a migration during Undo |
afterEachUndo | After every single successful migration during Undo |
afterEachUndoError | After every single failed migration during Undo |
afterUndo | After successful Undo runs |
afterUndoError | After failed Undo runs |
beforeClean | Before Clean runs |
afterClean | After successful Clean runs |
afterCleanError | After failed Clean runs |
beforeInfo | Before Info runs |
afterInfo | After successful Info runs |
afterInfoError | After failed Info runs |
beforeValidate | Before Validate runs |
afterValidate | After successful Validate runs |
afterValidateError | After failed Validate runs |
beforeBaseline | Before Baseline runs |
afterBaseline | After successful Baseline runs |
afterBaselineError | After failed Baseline runs |
beforeRepair | Before Repair runs |
afterRepair | After successful Repair runs |
afterRepairError | After failed Repair runs |
createSchema | Before automatically creating non-existent schemas |
beforeConnect | Before Flyway connects to the database |
- 实现Flyway的callback接口
flyway默认是会执行db/callback下的sql脚本,如果不想执行,可以使用参数 flyway.skipDefaultCallbacks跳过,设置为true即可,sping里面的配置参数为:
spring:
flyway:
skip-default-callbacks: true
package org.example.config;
import org.flywaydb.core.api.callback.Callback;
import org.flywaydb.core.api.callback.Context;
import org.flywaydb.core.api.callback.Event;
import org.flywaydb.core.api.configuration.Configuration;
import java.sql.Connection;
public class MyFlywayCallback implements Callback {
@Override
public boolean supports(Event event, Context context) {
return false;
}
@Override
public boolean canHandleInTransaction(Event event, Context context) {
return false;
}
/**
* 根据各种事件来自定义处理逻辑
*/
@Override
public void handle(Event event, Context context) {
//迁移发生错误后的事件
if (Event.AFTER_MIGRATE_ERROR == event) {
Configuration configuration = context.getConfiguration();
Connection connection = context.getConnection();
//自定义进行逻辑处理,并操作数据库
} else if (Event.CREATE_SCHEMA == event) {
//
}
}
@Override
public String getCallbackName() {
return null;
}
}
- 设置回调的sql文件
设置回调的sql文件的默认存放位置在db/callback下,sql文件的名称和上面表格的Migrate函数回调名称命名(beforeMigrate.sql,beforeRepeatables.sql)即可。
例如:清除执行失败的记录,便于我们开发,当sql脚本执行失败后,不需要手动去表中删除失败的记录,可以通过设置错误回调sql,自动删除失败的记录
sql脚本中的语句如下,此处可能因为配置的表名和字段不一样,有一些差别,视实际的表字段编写语句。
delete from flyway_schema_history where success=false;
注:执行失败的记录,如果不清除掉,修改脚本后再次执行就会报错,也可以使用repair指令来修复,repair有以下几个功能:
- 移除表中执行记录为失败的数据
- 将已经执行的迁移和可执行的迁移之间的校验和、描述和类型重新对齐
- 删除数据表中,在当前location文件夹下不存在的迁移记录
- 低版本的flyway是实现FlywayCallback接口(不建议使用),截图如下:
3.5、兼容历史项目
实际环境中不可能总是在项目开始就引入flyway,也可能是在项目中间引入flyway,针对数据库已经存在表和数据的情况,应该如何处理,这里flyway也有对应的解决方案,通过引入基线版本的方式来兼容历史数据,通过两个参数进行控制,baselineOnMigrate=true 和 flyway.baselineVersion=0.0,前者用于开启基线迁移,后者指定基线的版本,如何理解?
假设目前的项目,已经存在了很多表,我们需要在当前的基础上使用flyway,第一步是制作初始化的sql脚本,即将当前项目已有的数据库表结构,整体导出为一个sql文件,命名为V1.0__init.sql,后续如果再新增表或者修改表,则可以按版本号递增命名,如V1.1__add_new_table.sql。
在开启基线迁移且基线版本设置为1.0的情况下:
- 如果是历史环境升级,flyway会判断数据库非空,则会跳过1.0版本的脚本,从1.1开始执行,避免了1.0脚本执行报错的情况,
- 如果是新部署的环境,flyway判断数据库为空,会直接从1.0版本顺序往下执行,此时设置的基线迁移和基线版本不会生效(数据库为空的情况下)
- 如果存在很多个不同的环境,每个环境的数据库表都不一致,例如A环境有1,2两张表,B环境有1,2,3张表,C环境是1,2,4,但是最新的数据库表为1,2,3,4,此时如果你将1,2,3,4定义为基线版本,那么ABC环境就需要手动执行缺失的脚本,将其补齐到和基线脚本相同的位置
编写的脚本可以被反复执行,例如建表前判断表和字段时判断是否已经存在,insert数据时使用replace等等,但是不同的数据库支持的功能不一样,尽量写出可以重复执行的脚本,那么就可以减少手动补齐的工作量。
3.6、程序启动问题
程序的启动有时候会依赖于数据库的表,如果表没有创建完成,此时程序启动会出错,因此需要确保我们的程序中的数据库操作,在flyway处理完成后再执行,此处推荐两种方式避免这个问题:
第一是向spring注入bean时,代码里面会操作数据库,可以使用如下注解,等待flyway初始化完成后再进行
第二是使用ApplicationRunner或者CommandLineRunner的方式来进行程序初始化操作
3.7、其他问题
1、基线迁移有时会失效?
基线迁移生效的必须满足几个前提:
- 开启了基线迁移配置,设置了基线版本
- flyway运行前,数据库中已经存在其他表
- flyway_schema_history表不存在(首次执行)
所以需要确保代码中其他创建表的操作的顺序,有的生成表在flyway之前或者之后,会导致执行不满足预期
遇到一个问题,调试基线版本功能,flyway之前已经执行过,但是本次清除了表中的所有数据,启动程序发现基线迁移未生效,根据测试发现,就是不满足上面第三个条件,必须要把flyway_schema_history删除才可以