Flyway工具经验汇总

2021-06-13 fishedee 后端

0 概述

Flyway,是一个数据库版本管理工具。每次上线新功能的时候,都需要先更新数据库,然后再部署代码。当数据库的更新部分很多时,难免会忘掉,造成升级失败。

另外一个问题的是,当你的服务是允许客户自己私有部署的时候,每个客户可能都在不同的数据库版本上,这时候对于不同的客户给与不同的升级机制就相当重要了。

代码在这里

1 依赖

<dependency>
    <groupId>org.flywaydb</groupId>
    <artifactId>flyway-core</artifactId>
    <version>7.10.0</version>
</dependency>

flyway的库依赖

<plugin>
    <groupId>org.flywaydb</groupId>
    <artifactId>flyway-maven-plugin</artifactId>
    <version>7.1.0</version>
    <configuration>
        <url>jdbc:mysql://localhost:3306/Test?useUnicode=true&characterEncoding=utf-8&useLegacyDatetimeCode=false&serverTimezone=Asia/Shanghai</url>
        <user>root</user>
        <password>1</password>
        <locations>
            <location>classpath:/db/migration</location>
            <location>classpath:/db/migration_development</location>
        </locations>
        <driver>com.mysql.cj.jdbc.Driver</driver>
    </configuration>
</plugin>

flyway的Maven插件支持,它的功能是允许在IDE中进行直接调用flyway的命令。注意flyway的插件配置,不是自动读取application.properties的内容,需要另外在configuration里面配置的。

2 迁移脚本

2.1 SQL脚本

create table sales_order(
    id integer not null auto_increment,
    create_time timestamp not null default CURRENT_TIMESTAMP,
    modify_time timestamp not null default CURRENT_TIMESTAMP on update CURRENT_TIMESTAMP,
    primary key(id)
)engine=innodb default charset=utf8mb4 auto_increment = 10001;

在resources文件夹下面的,db的migration文件夹下添加各个迁移脚本。

2.2 文件命名

V1.0.0.0__xxxx.sql

每个迁移脚本的名字都是有规律的,必须是以V字母开头,然后放版本号(小数点有多少个不限制),然后是2个下划线,少一个下划线都不行,最后接上版本描述。必须以sql作为后缀名。

2.3 目录命名

你可以将不同脚本放在不同名称的目录都可以,Flyway会以文件名的版本号为依据执行脚本

你甚至将目录名写成带版本号的都可以,Flyway依然会忽略目录的名称,仅仅以文件名的版本号为依据执行脚本

如果文件名上没有版本号,flyway会忽略它,而不是用目录上的版本号

3 插件

打开Maven窗口,在Plugins里面,我们就能看到flyway的命令了。注意,使用插件的一个关键是,要保证先build project,才执行Maven的插件,否则会导致Maven插件引用的是旧jar包里面的resources文件。

3.1 clean

clean的命令是,清除所有的数据库表,字段和数据。

执行以后的结果

3.2 migrate

migrate的命令是,对数据库连接后,执行迁移。迁移的原理是:

  • 如果当前数据库是空的,且没有flyway_schema_history,那么就会创建flyway_schema_history,它里面会保存每个脚本的执行情况,以及当前的数据库版本号。
  • 如果当前数据库不是空的,且有flyway_schema_history。那么就会查询flyway_schema_history的版本号,假设这个版本号为1.0.0.1。那么它就会执行1.0.0.1之后的迁移脚本,并在flyway_schema_history中记录这个迁移的情况。

执行以后的结果

3.3 baseline

在migrate中,我们猜测到第3种情况是,当前数据库不是空的,且没有flyway_schema_history。这种情况就是,项目在中途的时候才引入flyway作为数据库版本迁移。这个时候,我们就需要用baseline这个命令了。

我们先删除本地flyway_schema_history表,然后执行baseline命令。

然后就会数据库中创建一个flyway_schema_history表,并且默认的版本号为1。显然,如果下次执行migration的话,就会将1.0之后的迁移脚本都执行一遍。

因此,我们需要在项目在中途的时候引入flyway的话,需要将脚本都以1.0.0.1以后的版本开始,不能以1.0.0.0的版本号开始,否则1.0.0.0版本的迁移脚本不会自动执行

3.4 validate

validation的原理是对比MetaData表与本地Migrations的checkNum值,如果值相同则验证通过,否则失败。

validation的功能在每次migration的时候都会执行,它是用来防止这种情况。开发者对某个A版本号的sql脚本迁移到了数据库,然后又修改了这个A版本号的脚本,这样就会产生不同机器下的迁移结果不一致的情况。validation就是用来检查每个迁移脚本的hash值是否与数据表flyway_schema_history的hash值是否一致,来确定开发者有没有偷偷改脚本的这个问题。

注意,我们永远不要对已经发布的迁移脚本进行修改,这是不行的。

3.5 undo

这个命令不要用,在社区版的flyway是残废和bug的,回滚数据自己靠自己。

4 SpringBoot配置

4.1 配置

# baseline的描述
spring.flyway.baseline-description = 我是基线描述

# 当迁移时发现目标schema非空,而且带有没有元数据的表时,是否自动执行基准迁移,默认false.
spring.flyway.baseline-on-migrate = true

# baseline的版本号,默认为1.0
spring.flyway.baseline-version = 0.9

# validation的原理是对比MetaData表与本地Migrations的checkNum值,如果值相同则验证通过,否则失败。
# 当发现校验错误时是否自动调用clean,默认false.
spring.flyway.clean-on-validation-error = false

# 是否开启flywary,默认true.
spring.flyway.enabled = true

# 设置迁移时的编码,默认UTF-8.
spring.flyway.encoding = UTF-8

# 当读取元数据表时是否忽略错误的迁移,默认false.
spring.flyway.ignore-failed-future-migration = false

# 当初始化好连接时要执行的SQL.
spring.flyway.init-sqls =

# 迁移脚本的位置,默认db/migration.
# 这个配合Profile,能给与不同的环境不同的测试数据
spring.flyway.locations =  classpath:/db/migration,classpath:/db/migration_development

# 是否允许无序的迁移,默认false.
spring.flyway.out-of-order = false

# 迁移时是否校验,默认为true
spring.flyway.validate-on-migrate = true

配置的描述如上,特别要注意的是spring.flyway.clean-on-validation-error的选项。它是用来方便开发环境时调试数据库用的,当更改了本地的迁移脚本后,flyway会自动感受到,就会全面将所有的数据库清空,重新导入schema。但是切勿在生产环境中使用,生产环境一旦发现迁移脚本的hash值与flyway_schema_history的hash值不一致就清空数据库,这显然会产生重大的事故。

但是由于在生产环境也可能容易导入开发环境的配置,所以clean-on-validation-error最好永远都设置为false。

spring.flyway.locations =  classpath:/db/migration,classpath:/db/migration_development

这个属性也好用,可以支持多个locations。那么我们可以为不同的development环境,production环境设置部分不同的迁移脚本,例如development环境相比production环境会多一些默认的测试数据。

4.2 新项目引入flyway

# 是否开启flywary,默认true.
spring.flyway.enabled = true

# 迁移时是否校验,默认为true
spring.flyway.validate-on-migrate = true

默认的flyway会在SpringBoot启动的时候,执行一次migration。并且在migration之前会执行一次validate。那么新项目使用flyway就相当简单,把脚本写好,启动项目就行。

4.3 旧项目引入flyway

# 当迁移时发现目标schema非空,而且带有没有元数据的表时,是否自动执行基准迁移,默认false.
spring.flyway.baseline-on-migrate = true

# baseline的版本号,默认为1.0
spring.flyway.baseline-version = 0.9

对于已有的项目,在启动的时候就需要打开baseline-on-migrate,并设置baseline的版本号。那么首次启动项目时,就会执行一次baseline,赋值baseline的版本号,然后执行该版本号之后的迁移脚本了。

5 单元测试

package spring_test;

import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.annotation.Before;
import org.flywaydb.core.Flyway;
import org.junit.jupiter.api.*;
import org.springframework.aop.framework.AopContext;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.ActiveProfiles;
import spring_test.infrastructure.SalesOrderRepository;

import javax.transaction.Transactional;
import java.util.List;

import static org.junit.jupiter.api.Assertions.*;


//flyway 在测试里面都会自动启动,这个时候会默认使用远程数据库
@SpringBootTest
@ActiveProfiles("test")
@Slf4j
//不需要DirtyContext,每次用例都全部清除数据库,这样在每次运行都是干净的数据库,但是性能比较差,因为每次都要migrate
public class MyTest {

    @Autowired
    private SalesOrderTest salesOrderTest;

    @Autowired
    private SalesOrderRepository salesOrderRepository;

    @Autowired
    private Flyway flyway;

    @BeforeEach
    public void setUp(){
        //清除数据
        flyway.clean();
        flyway.migrate();
    }

    @Test
    public void test1(){
        salesOrderTest.add();

        salesOrderTest.add();

        assertEquals(salesOrderRepository.getAll().size(),2);
    }

    @Test
    public void test2(){
        salesOrderTest.add();

        assertEquals(salesOrderRepository.getAll().size(),1);
    }
}

在单元测试中,我们也可以引入flyway,用来初始化测试数据库。可以在每个@BeforeEach里面执行clean和migrate命令。当然,这样做的好处是每个测试用例都是互相独立的,但是性能就比较差了。

6 DDL与事务

对于每个迁移脚本,flyway都会把它放在一个事务里面进行,包括DDL和DML语句。一旦发生错误以后,就会自动回滚,从而保证不会产生一个中间状态的数据库。但是,并不是所有数据库都支持DDL语句的事务性,例如mysql就不支持,当DDL语句出现在事务里面,就会产生一个隐性的commit语句,即使事务最终是rollback的,DDL语句之前的语句都不会去回滚,只能回滚DDL之后的语句。所以,在mysql里面,我们要使DDL语句和DML语句分开放在不同的迁移脚本,让回滚操作是尽量可控的。

当然,你可以使用同时支持DDL和DML语句事务性的数据库,例如是Postgresql,Oracle和Sql Server。

7 手动迁移和基线

代码在这里

7.1 配置

spring.flyway.enabled = false

由于是手动创建Flyway来触发操作,在配置文件中关掉flyway就可以了。注意点有:

  • SpringBoot不再自动生成Flyway实例,也就是你无法用@Autowired注解来获取Flyway实例
  • SpringBoot上关于Flyway的配置也无法影响手动创建的Flyway实例,配置需要自己手动方式来传入。

7.2 基线的SQL文件

在默认情况下,对于新项目,我们只需要用迁移功能就能一直将数据库更新到最新版本。但是,对于历史迁移非常多的脚本,我们希望可以只用一个脚本直达到最近的某个版本,这样就能提高数据库迁移的速度了。

例如,V1.0,V1.1和V1.2是普通的迁移脚本,而beforeBaseline.sql是聚合迁移脚本,可以从一个全新的数据库直达到V1.1版本,如何再用V1.2,更新到最新的版本库。

我们这里使用flyway的callback特性来实现的。beforeBaseline.sql是执行baseline任务前的迁移脚本,负责初始化数据库的。

脚本的命名的后缀的多样化的,前缀是固定的,例如是afterMigrate__init_employee.sql,或者afterMigrate__init_operator.sql等等。

create table sales_order(
    id serial not null,
    name varchar(64) not null,
    primary key(id)
);

V1.0__create_init.sql的内容

alter table sales_order add column phone_number varchar(64) not null default '';

V1.1__create_phone_number_column.sql的内容

create table sales_order(
    id serial not null,
    name varchar(64) not null,
    phone_number varchar(64) not null,
    primary key(id)
);

beforeBaseline.sql的内容,可以看到,同时包含了V1.0和V1.1的内容

7.3 执行迁移或基线

package spring_test;

import lombok.extern.slf4j.Slf4j;
import org.flywaydb.core.Flyway;
import org.flywaydb.core.api.MigrationInfo;
import org.flywaydb.core.api.MigrationInfoService;
import org.flywaydb.core.api.output.ValidateResult;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Component;

import javax.sql.DataSource;
import java.sql.Types;
import java.util.List;
import java.util.Map;

@Component
@Slf4j
public class FlywayInit {

    @Autowired
    private DataSource dataSource;

    @Autowired
    private JdbcTemplate jdbcTemplate;

    public boolean isExistFlywayHistory(String schemaName,String relName){
        List<Map<String, Object>> result = jdbcTemplate.queryForList("SELECT EXISTS (\n" +
                "  SELECT 1\n" +
                "  FROM   pg_catalog.pg_class c\n" +
                "  JOIN   pg_catalog.pg_namespace n ON n.oid = c.relnamespace\n" +
                "  WHERE  n.nspname = ?\n" +
                "  AND    c.relname = ?\n" +
                "  AND    c.relkind = 'r'\n" + // only tables
                ")",
                new Object[]{schemaName,relName},
                new int[]{Types.VARCHAR,Types.VARCHAR});
        Boolean target = (Boolean)result.get(0).get("exists");
        return target;
    }

    public void init(){
        Flyway flyway = Flyway.configure()
                .dataSource(dataSource)
                .baselineOnMigrate(true)
                .baselineVersion("1.1")
                .load();
        if( isExistFlywayHistory("public","flyway_schema_history") == false ){
            //未迁移过,直接从跳到1.1版本,并执行1.1版本的baseLine操作
            flyway.baseline();
        }
        flyway.migrate();
    }
}

我们先用jdbcTemplate查询是否存在flywayHistory,不存在的话直接从基线开始迁移,否则的话就是逐步迁移。注意,properties文件关于flyway的配置,不影响手动创建Flyway的配置,需要手工传入配置。

8 总结

Flyway相对Liquibase的好处在于,用原生的SQL语句写迁移脚本,简单易理解。缺点当然就是它无法理解SQL语句的意义,造成undo操作和回滚操作都不能很好地支持。

参考资料:

  • https://www.jianshu.com/p/567a8a161641
  • https://www.cnblogs.com/liuyupen/p/11101594.html
  • https://blog.csdn.net/shangyadongze/article/details/80331182
  • https://blog.csdn.net/weixin_39618176/article/details/110657085

相关文章