Flyway + SpringBoot:打造可靠的数据库版本管理方案

2,021 阅读5分钟

Flyway数据库版本管理

一、为什么要使用flyway

1、描述

  • 迁移指的是把数据库结构从一个版本更新到另一个版本。使用 Flyway 时,对数据库的所有更改都称为迁移。
  • 迁移可以是版本化,也可以是可重复。迁移脚本分为三种类型:VersionedRepeatable和Undo
  • 其中,Versioned用于版本升级,每个版本有唯一的版本号并只能执行一次;
  • Repeatable可重复执行,当Flyway检测到Repeatable类型的SQL脚本的checksum有变动,Flyway就会重新应用该脚本;Undo用于撤销上一次的迁移操作。

img.png

2、为什么要使用flyway

  • 在多人开发的项目中,我们都习惯了使用SVN或者Git来对代码做版本控制,主要的目的就是为了解决多人开发代码冲突和版本回退的问题。
  • 数据库的变更也需要版本控制,在日常开发中,我们经常会遇到下面的问题:
    1. 自己写的SQL忘了在所有环境执行。
    2. 别人写的SQL我们不能确定是否都在所有环境执行过了。
    3. 有人修改了已经执行过的SQL,期望再次执行。
    4. 需要新增环境做数据迁移。
    5. 每次发版需要手动控制先发DB版本,再发布应用版本。
    6. 其它场景。

3.flyway是如何工作的

  1. 项目启动,应用程序完成数据库连接池的建立后,Flyway自动运行。
  2. 初次使用时,flyway会创建一个flyway_schema_history表,用于记录sql执行记录。
  3. Flyway会扫描项目指定路径下(默认是classpath:db/migration)的所有sql脚本,与flyway_schema_history表脚本记录进行比对。如果数据库记录执行过的脚本记录,与项目中的sql脚本不一致,Flyway会报错并停止项目执行。
  4. 如果校验通过,则根据表中的sql记录最大版本号,忽略所有版本号不大于该版本的脚本。再按照版本号从小到大,逐个执行其余脚本。

二、SpringBoot3引用依赖

<dependency>
  <groupId>org.flywaydb</groupId>
  <artifactId>flyway-core</artifactId>
  <version>10.17.2</version>
</dependency>
<!--必须同时引入flyway-mysql,否则会报不支持mysql8.1版本数据库。-->
<dependency>
  <groupId>org.flywaydb</groupId>
  <artifactId>flyway-mysql</artifactId>
  <version>10.17.2</version>
</dependency>
        <!--集成MyBatis-Plus-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.7</version>
<exclusions>
 <exclusion>
  <groupId>org.mybatis</groupId>
  <artifactId>mybatis-spring</artifactId>
 </exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>3.0.3</version>
</dependency>

集成flyway,sql脚本不执行问题

需整合mybatis,否则启动无法执行脚本

集成flyway,MySQL中root无法访问数据库问题,必须确保外部对root所有数据库的访问权限

GRANT ALL PRIVILEGES ON . TO 'root'@'%' WITH GRANT OPTION; FLUSH PRIVILEGES;

三、命名规则

1、仅需要被执行一次的SQL命名以大写的"V"开头,V+版本号(版本号的数字间以”.“或”_“分隔开)+双下划线(用来分隔版本号和描述)+文件描述+后缀名。例如:V20201100__create_user.sqlV2.1.5__create_user_ddl.sqlV4.1_2__add_user_dml.sql

2、可重复运行的SQL,则以大写的“R”开头,后面再以两个下划线分割,其后跟文件名称,最后以.sql结尾。(不推荐使用)比如:R__truncate_user_dml.sql

四、默认存放文件路径db.migration(因为flyway也遵循约定大于配置原则)

img_2.png

五、springboot的yml配置

spring: 
 # 数据库版本管理,SQL执行
  flyway:
    # 是否启用flyway
    enabled: true
    # 编码格式,默认UTF-8
    encoding: UTF-8
    # 迁移sql脚本文件存放路径,官方默认db/migration
    locations: classpath:db/migration
    # 迁移sql脚本文件名称的前缀,默认V
    sql-migration-prefix: V
    # 迁移sql脚本文件名称的分隔符,默认2个下划线__
    sql-migration-separator: __
    # 避免带${}sql执行失败
    placeholder-prefix: '#('
    placeholder-suffix: )
    # 迁移sql脚本文件名称的后缀
    sql-migration-suffixes: .sql
    # 迁移时是否进行校验,默认true
    validate-on-migrate: true
    # 历史表,当迁移发现数据库非空且存在没有元数据的表时,自动执行基准迁移,新建schema_version表
    baseline-on-migrate: true
    # 默认1,从0开始可以规避版本1不执行问题
    baseline-version: 0
    # 避免数据被意外清空
    clean-disabled: false
    datasource:
      url: jdbc:mysql://localhost:3306/top
      username: root
      password: hlb159765
      driver-class-name: com.mysql.cj.jdbc.Driver

六、版本化迁移(V)

常见的迁移类型是版本化迁移。版本必须是唯一的。 版本化迁移仅按顺序应用一次。 版本化迁移通常用于:

  • 正在创建/更改/删除表/索引/外键…
  • 数据更新

img_3.png

七、可重复(R)

一般为可重复执行的sql语句。

八、撤销(U)

撤消迁移内容与常规版本化迁移相反。一般不推荐使用,因为版本一但需要回退,可能有破坏性的操作已经执行,我们需要经过充分测试的备份和有一些还原数据的策略。

九、错误收集和解决方案

1.如果 flyway 不是项目初期引入,而是在已有表的情况下引入,必须设置 baseline-on-migrate: true。启动项目后,flyway 就会自动在数据库中创建 flyway_schema_history 表,并且会往该表中插入一条 version = 1 的建表记录,如果迁移数据有 V1__ 开头的文件,扫描文件会忽略该文件不执行迁移,进而可能引发其他迁移数据出错的问题。这个原因是因为baseline-version的默认值就是1。

2.生产环境需要将clean-disabled设置为false,避免数据被意外清空。

3.参考:cmjava.ltd:8090/archives/sp…

踩坑日记

  • 禁用 Flyway(如果不需要数据库迁移)报错
spring:
  flyway:
    enabled: false

禁用flyway之后报错

Description:

Field flyway in com.example.springbootopenaicommon.config.FlywayRepair required a bean of type 'org.flywaydb.core.Flyway' that could not be found.

The injection point has the following annotations:
- @org.springframework.beans.factory.annotation.Autowired(required=true)


Action:

Consider defining a bean of type 'org.flywaydb.core.Flyway' in your configuration.

解决方法

  • 修改FlywayRepair类 将@Component注解替换为@Configuration并添加@ConditionalOnProperty 注解。
import org.flywaydb.core.Flyway;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Configuration;

@Configuration
@ConditionalOnProperty(prefix = "spring.flyway", name = "enabled", havingValue = "true", matchIfMissing = true)
public class FlywayRepair {

    @Autowired
    private Flyway flyway;

    public void repair() {
        flyway.repair();
    }
}
  • 在 application.yml 中控制 Flyway 启用状态
spring:
  flyway:
    enabled: false  # 禁用 Flyway

其他方法

  • 方法一:使用 @ConditionalOnBean 注解 可以使用 @ConditionalOnBean 注解,使得 FlywayRepair 类仅在 Flyway Bean 存在时才被创建。
import org.flywaydb.core.Flyway;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.context.annotation.Configuration;

@Configuration
@ConditionalOnBean(Flyway.class)
public class FlywayRepair {

    @Autowired
    private Flyway flyway;

    public void repair() {
        flyway.repair();
    }
}
  • 方法二:显式检查 Flyway 的存在 在代码中显式检查 Flyway 是否存在,并根据检查结果执行不同的逻辑。
import org.flywaydb.core.Flyway;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;

@Component
public class FlywayRepair {

    @Autowired
    private ApplicationContext applicationContext;

    public void repair() {
        if (applicationContext.containsBean("flyway")) {
            Flyway flyway = (Flyway) applicationContext.getBean("flyway");
            flyway.repair();
        }
    }
}