Flyway 管理数据库版本变更

一、 Flyway介绍

Flyway的定位:数据库的版本控制。

Flyway是一款开源的数据库版本管理工具,Flyway可以独立于应用实现管理并跟踪数据库的变更(DDL和DML语句),Flyway根据自己的约定,不需要复杂的配置就可以实现数据的Migrate(迁移)。Migrations可以写成SQL脚本,也可以写在Java代码中,Flyway还支持Spring Boot。
使用Flyway帮助用户完成数据库迁移的工作,Flyway可以从任意一个数据库版本自动迁移到最新版本,由于迁移脚本和代码同步提交,从而保证代码和数据库时刻匹配。

二、为什么要用Flyway

随着项目的不断迭代,数据库表结构、数据都在发生着变化。甚至有的业务在多环境版本并行运行。数据为王的时代,管理好数据库的版本也成为了迫切的需要。如何能做到像 Git 之类的版本控制工具来管理数据库?Java 项目中常用 Flyway 和 Liquibase 来管理数据库版本。其中 Flyway 相对来说比较受欢迎。

在真实的项目开发中,我们每个人都会有一个应用软件和与其相对应的数据库。对于个人开发来说,这样就够了。但是,项目开发一般都不止一个人,因此一定会出现我的本地有一套软件和相应的数据库系统,另一个同事会在他的本地有一套他自己的软件和相应的数据库系统。我们需要面临的第一个问题就是我们两个人如何集成我们的数据库系统,之后还要处理如何将数据库系统迁移到测试环境和生产环境当中去。其实道理和Git合并代码一样的道理,当2个人或多个同时修改了一份代码那么我们如何进行数据库同步?在比如我们如果修改了脚本那么如何同步测试环境和生产环境,以上那么变得非常麻烦,现在就用到了我们的flyway。

三、 Flyway的工作模式

介绍Flyway是如何工作的,也可以理解为Flyway的数据库升级方案。

Flyway可以对数据库进行升级,从任意一个版本升级到最新的版本。但是升级的依据是用户自己编写的sql脚本,用户自己决定每一个版本的升级内容。

Flyway不限定脚本里面的内容,但是对脚本文件的名称有一定的命名规范要求:

image

版本号可以使用小版本,如V1.1。

具体要求:

  • 版本号和版本描述之间,使用两个下划线分隔。
  • 版本描述之间,使用一个下划线分隔单词。
  • 版本号唯一:不允许多个脚本文件有相同的版本号。

使用Flyway升级,会自动创建一张历史记录表:flyway_schema_history。

这张表记录了每一次升级的记录,包括已经执行了哪些脚本,脚本的文件名,内容校验和,执行的时间和结果:

meta表数据截图

Flyway在升级数据库的时候,会检查已经执行过的版本对应的脚本是否发生变化,包括脚本文件名,以及脚本内容。如果flyway检测到发生了变化,则抛出错误,并终止升级。

如果已经执行过的脚本没有发生变化,Flyway会跳过这些脚本,依次执行后续版本的脚本,并在记录表中插入对应的升级记录。

所以,Flyway总是幂等的,而且可以支持跨版本的升级。

如果你好奇,Flyway如何检查脚本文件的内容是否有修改。你可以注意以下记录表中有一个字段checksum,它记录了脚本文件的校验和。flyway通过比对文件的校验和来检测文件的内容是否变更。

使用上面的方式,升级一个空的数据库,或者在一直使用Flyway升级方案的数据库上进行升级,都不会又问题。但是,如果在已有的数据库引入Flyway,就需要一些额外的工作。

Flyway检测数据库中是否有历史记录表,没有则代表是第一次升级。此时,flyway要求数据库是空的,并拒绝对数据库进行升级。

你可以设置baseline-on-migrate参数为true,flyway会自动将当前的数据库记录为V1版本,然后执行升级脚本。这也表示用户所准备的脚本中,V1版本的脚本会被跳过,只有V1之后的版本才会被执行。

四、Flyway 的“潜”规则

4.1 SQL文件执行顺序

Flyway 是如何比较两个 SQL 文件的先后顺序呢?它采用 采用左对齐原则, 缺位用 0 代替 。举几个例子:

  • 1.0.1.1比1.0.1版本高。
  • 1.0.10比1.0.9.4版本高。
  • 1.0.10和1.0.010版本号一样高, 每个版本号部分的前导 0 会被忽略。

4.2 SQL文件种类

FlywaySQL 文件分为 VersionedRepeatableUndo 三种:

  • Versioned 用于版本升级, 每个版本有唯一的版本号并只能执行一次.
  • Repeatable 可重复执行, 当 Flyway检测到 Repeatable 类型的 SQL 脚本的checksum有变动, Flyway 就会重新应用该脚本. 它并不用于版本更新, 这类的migration总是在 Versioned 执行之后才被执行。
  • Undo 用于撤销具有相同版本的版本化迁移带来的影响。但是该回滚过于粗暴,过于机械化,一般不推荐使用。一般建议使用 Versioned 模式来解决。

这三种的命名规则如下图:

命名规则

  • Prefix 可配置,前缀标识,默认值V表示 Versioned,R表示 Repeatable,U表示 Undo
  • Version 标识版本号, 由一个或多个数字构成, 数字之间的分隔符可用点.或下划线_
  • Separator 可配置, 用于分隔版本标识与描述信息, 默认为两个下划线__
  • Description 描述信息, 文字之间可以用下划线_或空格 分隔
  • Suffix 可配置, 后续标识, 默认为.sql

4.3 执行流程说明

在定义脚本的时候,除了 V 字开头的脚本之外,还有一种 R 字开头的脚本,V 字开头的脚本只会执行一次,而 R 字开头的脚本,只要脚本内容发生了变化,启动时候就会执行。
使用了 Flyway 之后,如果再进行数据库版本升级,就不用修改以前的数据库脚本了,直接创建新的数据库脚本,项目在启动时检测了有新的更高版本的脚本,就会自动执行,这样,在和其他同事配合工作时,也会方便很多。因为正常我们都是从 Git 上拉代码下来,不拉数据库脚本,这样要是有人更新了数据库,其他同事不一定能够收到最新的通知,使用了 Flyway 就可以有效避免这个问题了。

4.3.1 重新执行脚本

所有的脚本,一旦执行了,就会在 flyway_schema_history 表中有记录,如果你不小心搞错了,可以手动从 flyway_schema_history 表中删除记录,然后修改 SQL 脚本后再重新启动(生产环境不建议)。

五、Spring Boot 集成 Flyway

Spring Boot 集成 Flyway

Spring Boot 提供了对 Flyway 的自动配置 。使我们可以开箱即用 Flyway 进行数据库版本控制。

5.1 新 Spring Boot 项目

Dependencies页就有 Flyway 的选项,可以搜索flyway然后回车就会自动添加该依赖,或者点击SQL–>Flyway Migration勾选该项,如下图:

加入Flyway依赖

项目创建成功后,resources目录下也会多出来一个db/migration目录,这个目录用来存放数据库脚本,如下:

db/migration目录

5.2 依赖:

核心依赖:

<!-- 无需版本号 --> <dependency> <groupId>org.flywaydb</groupId> <artifactId>flyway-core</artifactId> </dependency> 

完整pom:

<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.erbadagang.db.flyway</groupId> <artifactId>flyway</artifactId> <version>0.0.1-SNAPSHOT</version> <name>flyway</name> <description>Flayway project for Spring Boot</description> <properties> <java.version>1.8</java.version> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <spring-boot.version>2.3.0.RELEASE</spring-boot.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> <!-- 无需版本号 --> <dependency> <groupId>org.flywaydb</groupId> <artifactId>flyway-core</artifactId> </dependency> <!-- 实现对数据库连接池的自动化配置 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> </dependency> <dependency> <!-- 本示例,我们使用 MySQL --> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.48</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> <exclusions> <exclusion> <groupId>org.junit.vintage</groupId> <artifactId>junit-vintage-engine</artifactId> </exclusion> </exclusions> </dependency> </dependencies> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-dependencies</artifactId> <version>${spring-boot.version}</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <configuration> <source>1.8</source> <target>1.8</target> <encoding>UTF-8</encoding> </configuration> </plugin> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project> 

5.3 application.yml

配置数据库连接池

#############flyway########## #服务端口 server: port: 8080 spring: application: #服务名 name: flyway main: allow-bean-definition-overriding: true datasource: url: jdbc:mysql://localhost:3306/fescar?useSSL=false&useUnicode=true&characterEncoding=UTF-8 driver-class-name: com.mysql.jdbc.Driver type: com.zaxxer.hikari.HikariDataSource # 使用 Hikari 数据库连接池 #    driver-class-name: com.mysql.cj.jdbc.Driver username: root password: 123456 

Flyway部分的配置为:

# flyway 配置 spring: flyway: # 启用或禁用 flyway enabled: true # flyway 的 clean 命令会删除指定 schema 下的所有 table, 生产务必禁掉。这个默认值是 false 理论上作为默认配置是不科学的。 clean-disabled: true # SQL 脚本的目录,多个路径使用逗号分隔 默认值 classpath:db/migration locations: classpath:db/migration #  metadata 版本控制信息表 默认 flyway_schema_history table: flyway_schema_history # 如果没有 flyway_schema_history 这个 metadata 表, 在执行 flyway migrate 命令之前, 必须先执行 flyway baseline 命令 # 设置为 true 后 flyway 将在需要 baseline 的时候, 自动执行一次 baseline。 baseline-on-migrate: true # 指定 baseline 的版本号,默认值为 1, 低于该版本号的 SQL 文件, migrate 时会被忽略 baseline-version: 1 # 字符编码 默认 UTF-8 encoding: UTF-8 # 是否允许不按顺序迁移 开发建议 true  生产建议 false out-of-order: false # 需要 flyway 管控的 schema list,这里我们配置为flyway  缺省的话, 使用spring.datasource.url 配置的那个 schema, # 可以指定多个schema, 但仅会在第一个schema下建立 metadata 表, 也仅在第一个schema应用migration sql 脚本. # 但flyway Clean 命令会依次在这些schema下都执行一遍. 所以 确保生产 spring.flyway.clean-disabled 为 true schemas: flyway # 执行迁移时是否自动调用验证   当你的 版本不符合逻辑 比如 你先执行了 DML 而没有 对应的DDL 会抛出异常 validate-on-migrate: true 

5.4 编写 SQL 初始化脚本

如果db/migration目录下没有SQL文件,启动项目会报错:

Flyway failed to initialize: none of the following migration scripts locations could be found: - classpath:db/migration” 

BTW:这个英文提示我断句好几次才勉强看懂,如果说成nothing in the following location是不是更容易理解?

我们先编写一个初始化 SQL 文件,向数据库添加一张sys_user表。请注意命名规则,脚本名称为V1.0.0__Add_table_user.sql。SQL 脚本的位置在配置的spring.flyway.locations值,即db/migration目录 下。内容为:

use `fescar`; CREATE TABLE `sys_user` ( `user_id` bigint  NOT NULL AUTO_INCREMENT, `username` varchar(50) NOT NULL unique , `encode_password` varchar(50) NOT NULL, `age` int(3) NOT NULL, PRIMARY KEY (`user_id`) ) ENGINE = InnoDB
  DEFAULT CHARSET = utf8mb4; insert into fescar.sys_user values (1,'guoxiuzhi','{noop}123456',18); 

5.5 启动 Spring Boot 应用

可以看到如下日志信息,代表成功执行了sql。如果有问题会报相应的异常信息。

2020-07-04 15:17:29.807  INFO 12200 --- [main] o.f.core.internal.command.DbValidate : Successfully validated 2 migrations (execution time 00:00.012s) 2020-07-04 15:17:29.819  INFO 12200 --- [main] o.f.core.internal.command.DbMigrate : Migrating schema `flyway` to version 1.0.0
2020-07-04 15:17:29.896  INFO 12200 --- [main] o.f.core.internal.command.DbMigrate : Successfully applied 1 migration to schema `flyway` (execution time 00:00.086s) 

Flyway 需要在DB中先创建一个metadata表 (缺省表名为flyway_schema_history), 在该表中保存着每次migration(迁移)的记录, 记录包含migration脚本的版本号和 SQL 脚本的checksum值。下图表示了多个版本数据库变更。

flyway_schema_history表功能

对应的metadata表记录:

installed_rank version description type script checksum installed_by installed_on execution_time success
1 1.0.0 Add table user SQL V1.0.0__Add_table_user.sql 1963311468 root 2020-07-04 15:07:32 53 1

上面的表格由于太宽,在移动端显示被适应屏幕宽度而难于阅读,可以看下面截图数据:

meta表数据截图

Flyway 扫描文件系统或应用程序的类路径读取 DDLDML 以进行迁移。根据metadata表进行检查迁移。如果脚本声明的版本号小于或等于标记为当前版本的版本号之一,将忽略它们。其余迁移是待处理迁移:可用,但未应用。最后按版本号对它们进行排序并按顺序执行 并将执行结果写入 metadata 表。

迁移变化示意图

5.6 修改V1.0.0__Add_table_user.sql

修改sql脚本,一般int类型后面不加长度定义,将age int(3) NOT NULL,改成age int NOT NULL。再次启动 Spring Boot 应用 。
提示如下异常:

Caused by: org.flywaydb.core.api.FlywayException: Validate failed: Migration checksum mismatch for migration version 1.0.0
-> Applied to database : 1963311468
-> Resolved locally : 1207766644 

Flyway在升级数据库的时候,会检查已经执行过的版本对应的脚本是否发生变化,包括脚本文件名,以及脚本内容。如果flyway检测到发生了变化,则抛出错误,并终止升级。
上面抛出异常正因为我们开启了校验,所以对于checksum不一致的会抛出异常。

5.6 新建高版本SQL文件

我们新增一个版本号更高的V1.0.1__alter_table_user.sqlSQL文件。增加删除表命令和修改age int(3) NOT NULL为age int NOT NULL。

更高版本号的SQL文件

再次启动应用。

控制台打印成功迁移信息Migrating schema flyway to version 1.0.1:

2020-07-04 15:42:08.039  INFO 9084 --- [main] o.f.core.internal.command.DbValidate : Successfully validated 2 migrations (execution time 00:00.014s) 2020-07-04 15:42:08.044  INFO 9084 --- [main] o.f.core.internal.command.DbMigrate : Current version of schema `flyway`: 1.0.0
2020-07-04 15:42:08.051  INFO 9084 --- [main] o.f.core.internal.command.DbMigrate : Migrating schema `flyway` to version 1.0.1- alter table user
2020-07-04 15:42:08.070  INFO 9084 --- [main] o.f.core.internal.command.DbMigrate : Successfully applied 1 migration to schema `flyway` (execution time 00:00.028s) 

数据库迁移记录,即flyway_schema_history表的数据如下:

六、总结

通过2个版本的SQL演示,如果有高版本的SQL文件存在于db/migration目录,当启动应用的时候就会自动执行SQL到数据库中,也就是我们的目标:使用 Flyway 进行数据库版本控制
这里总结了一些在实际开发中的使用经验:

  1. 生产务必禁 spring.flyway.cleanDisabled=false 。
  2. 尽量避免使用 Undo 模式,使用Versioned模式新建高版本SQL文件。
  3. 开发版本号尽量根据团队来进行多层次的命名避免混乱。比如 V1.0.1__ProjectName_{Feature|Bugfix}_Developer_Description.sql ,这种命名同时也可以获取更多脚本的相关功能、开发者的信息。
  4. spring.flyway.outOfOrder 取值 生产上使用 false,开发中使用 true。
  5. 多个系统公用一个 数据库 schema 时配置spring.flyway.table ,为不同的系统设置不同的 metadata 表名,而不使用缺省值 flyway_schema_history 。

写在最后:

本文源代码使用 Apache License 2.0开源许可协议,可从Gitee代码地址通过git clone命令下载到本地或者通过浏览器方式查看源代码。

作者:梅西爱骑车
链接:https://www.jianshu.com/p/961b989d4f12
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。