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有以下几个功能:

  1. 移除表中执行记录为失败的数据
  2. 将已经执行的迁移和可执行的迁移之间的校验和、描述和类型重新对齐
  3. 删除数据表中,在当前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的情况下:

  1. 如果是历史环境升级,flyway会判断数据库非空,则会跳过1.0版本的脚本,从1.1开始执行,避免了1.0脚本执行报错的情况,
  2. 如果是新部署的环境,flyway判断数据库为空,会直接从1.0版本顺序往下执行,此时设置的基线迁移和基线版本不会生效(数据库为空的情况下)
  3. 如果存在很多个不同的环境,每个环境的数据库表都不一致,例如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删除才可以

posted @ 2024-07-31 15:55  浪迹天涯的派大星  阅读(1328)  评论(0编辑  收藏  举报