一卓的博客

怕什么真理无穷,
进一寸有一寸的欢喜。

0%

SpringBoot 集成数据库版本管理工具 Flyway

为什么要进行数据库版本控制?

假设我们有一个名为 Shiny 的项目,其主要交付项目是一个名为 Shiny Soft 的软件,该软件连接到名为 Shiny DB 的数据库。
最简单的图可能看起来像这样:
img

但是在大多数项目中,我们的数据库都是像以下这样:
img
通常,我们的环境不仅仅只有一套,一般来说会有 DEV、TEST、PROD 三套环境。
而在每次发版的时候,我们可能都会对数据库的表结构进行新增和变更,又或者需要插入一些初始化的数据。
那么,就意味着我们需要对 DEV、TEST、PROD 数据库都做一遍操作。
很多时候,我们并不能保证像机器一样,每次都操作都正确,很快出现了许多问题:

  • 该计算机上的数据库处于什么状态?
  • 此脚本是否已经应用?
  • 之后,生产中的快速修复是否已在测试中应用?
  • 如何设置新的数据库实例?

这些问题的答案常常是:我们不知道。

数据库版本控制是控制这种混乱的好方法。本文就带大家入门一个数据库版本控制工具 Flyway。

什么是 Flyway

在 Flyway 的官网 https://flywaydb.org/ 中,对自己的介绍是:

Version control for your database.
Robust schema evolution across all your environments.
With ease, pleasure and plain SQL.

即 数据库的版本控制。
使用轻松简单的 SQL 在所有环境中进行稳健的架构演变。

Flyway 提供了 SQL-based migrationsJava-based migrations 两种数据库变更方式。本文将分别用这两种方式来演示如何使用 Flyway。

Spring Boot 集成 Flyway

在 Spring Boot 项目中,提供了对 Flyway 的内置支持。基本上能实现开箱即用。

第一步:首先创建一个 Spring Boot 项目。
第二步:pom.xml 文件引入 Flyway 依赖

pom.xml文件中,引入如下依赖:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<dependencies>
<!-- 实现对数据库连接池的自动化配置 -->
<!-- spring-boot-starter-jdbc 支持 Flyway 的自动化配置 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>

<!-- 本文 Mysql 使用的 5.6 版本,所以 flyway 选择 5.2.4,高版本 Flyway 需升级 mysql 版本 -->
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-core</artifactId>
<version>5.2.4</version>
</dependency>

<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.19</version>
</dependency>
</dependencies>
第三步:配置 application.properties

application.properties 配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# 数据库地址
spring.datasource.url=jdbc:mysql://yizhuo.com:3306/flyway?useUnicode=true&characterEncoding=utf-8&useSSL=false&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=Asia/Shanghai
# 数据库驱动
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
# 数据库账号
spring.datasource.username=root
# 数据库密码
spring.datasource.password=12345

# 开启 Flyway 功能
spring.flyway.enabled=true
# 禁用 Flyway 所有的 drop 相关的逻辑,避免出现跑路的情况。
spring.flyway.cleanDisabled=true
# Flyway 版本控制所使用的表名 默认为 flyway_schema_history
spring.flyway.table=management_flyway_schema_history
# 配置 SQL-based 的 SQL 脚本在该目录下,默认配置即 classpath:db/migration
spring.flyway.locations[0]=classpath:db/migration
# 配置 Java-based 的 Java 类在该目录下
spring.flyway.locations[1]=classpath:com.github.zhuyizhuo.flyway.demo.migration
# 是否校验迁移脚本目录。如果配置为 true,代表需要校验, 如果目录下没有迁移脚本,会抛出 IllegalStateException 异常
spring.flyway.check-location=false
# 数据库地址
spring.flyway.url=${spring.datasource.url}
# 数据库账号
spring.flyway.user=${spring.datasource.username}
# 数据库密码
spring.flyway.password=${spring.datasource.password}
方式一:SQL-based migrations 创建 SQL 迁移脚本

resources/db/migration 目录下,创建 V1.0__INIT_USERS.sql SQL 迁移脚本。内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
-- 创建用户表
DROP TABLE IF EXISTS `users`;
CREATE TABLE `users` (
`id` int(6) NOT NULL AUTO_INCREMENT COMMENT '用户编号',
`username` varchar(32) DEFAULT '' COMMENT '账号',
`password` varchar(32) DEFAULT '' COMMENT '密码',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
PRIMARY KEY (`id`),
UNIQUE KEY `idx_username` (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- 插入一条数据 密码为 123456 MD5 加密的 32 位大写
INSERT INTO `users`(username, password, create_time) VALUES('张三', 'E10ADC3949BA59ABBE56E057F20F883E', now());

Flyway 规定脚本 V1.0__INIT_USERS.sql 的命名必须遵循以下命名模式:
flyway-naming

  • Prefix 前缀
    • V用于版本控制(可配置), U用于撤消迁移(可配置)和 R用于可重复迁移(可配置)。
    • 版本化的迁移具有版本描述校验和。版本化迁移是最常见的迁移类型。它们仅按顺序应用一次版本化迁移通常用于:
      • 创建/更改/删除表/…
      • 参考数据更新
      • 用户数据更正
    • 撤销迁移负责撤消具有相同版本的版本化迁移的影响。不建议在线上使用,原因参考 Flyway 官网
    • 可重复的迁移描述校验和,但没有版本。每次校验和更改时(重新)应用它们,而不是仅运行一次。
      这对于管理数据库对象非常有用,该数据库对象的定义随后可以在版本控制中仅保存在单个文件中。它们通常用于
      • (重新)创建视图/过程/功能/包/…
      • 批量插入参考数据
  • Version 版本号
    • 每一个迁移脚本,都需要一个对应一个唯一的版本号。只要符合通常的点分符号,任何版本都是有效的。在大多数情况下,只需要一个简单的递增整数即可。但是Flyway非常灵活,所有这些版本都是有效的版本迁移版本:
      • 001
      • 5.2
      • 1.2.3.4.5.6.7.8.9
      • 20130115113556
      • 2013.01.15.11.35.56
        版本迁移按版本顺序应用。版本按照您通常期望的方式按数字排序。
    • 脚本的执行顺序,按照版本号的顺序。Flyway 是如何比较两个 SQL 文件的先后顺序呢?它采用 采用左对齐原则, 缺位用 0 代替 。举几个例子:
      • 1.0.1.1 比 1.0.1 版本高。
      • 1.0.10 比 1.0.9.4 版本高。
      • 1.0.10 和 1.0.010 版本号一样高, 每个版本号部分的前导 0 会被忽略。
  • Separator 分隔符:两个下划线 _ ,即 __ 。(可配置),一般使用缺省配置。
  • Description 描述:描述脚本的用途。
  • Suffix 后缀:.sql 。可配置,一般使用缺省配置。
启动 Spring Boot 项目
1
2
3
4
5
6
@SpringBootApplication
public class FlywayApplication {
public static void main(String[] args) {
SpringApplication.run(FlywayApplication.class, args);
}
}

执行日志如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
2020-06-21 21:32:31.934  INFO 40674 --- [           main] o.f.c.internal.license.VersionPrinter    : Flyway Community Edition 5.2.4 by Boxfuse
2020-06-21 21:32:37.590 INFO 40674 --- [ main] o.f.c.internal.database.DatabaseFactory : Database: jdbc:mysql://yizhuo.com:3306/flyway (MySQL 5.6)
2020-06-21 21:32:37.780 WARN 40674 --- [ main] o.f.c.i.s.classpath.ClassPathScanner : Unable to resolve location classpath:com/github/zhuyizhuo/flyway/demo/migration
// 发现 1 个迁移脚本。
2020-06-21 21:32:38.009 INFO 40674 --- [ main] o.f.core.internal.command.DbValidate : Successfully validated 1 migration (execution time 00:00.148s)
// 创建 management_flyway_schema_history 表
2020-06-21 21:32:38.223 INFO 40674 --- [ main] o.f.c.i.s.JdbcTableSchemaHistory : Creating Schema History table: `flyway`.`management_flyway_schema_history`
// 当前数据库的迁移版本
2020-06-21 21:32:38.699 INFO 40674 --- [ main] o.f.core.internal.command.DbMigrate : Current version of schema `flyway`: << Empty Schema >>
// 开始执行当前迁移脚本 版本 1.0
2020-06-21 21:32:38.729 INFO 40674 --- [ main] o.f.core.internal.command.DbMigrate : Migrating schema `flyway` to version 1.0 - INIT USERS
// MYSQL 告警日志 可忽略
2020-06-21 21:32:38.876 WARN 40674 --- [ main] o.f.c.i.s.DefaultSqlScriptExecutor : DB: Unknown table 'flyway.users' (SQL State: 42S02 - Error Code: 1051)
// 成功执行一个迁移
2020-06-21 21:32:39.446 INFO 40674 --- [ main] o.f.core.internal.command.DbMigrate : Successfully applied 1 migration to schema `flyway` (execution time 00:01.251s)
2020-06-21 21:32:39.721 INFO 40674 --- [ main] c.g.z.flyway.demo.FlywayApplication : Started FlywayApplication in 11.145 seconds (JVM running for 12.785)

查看数据库,发现表已经正常创建。并且插入了初始化数据。
image-20200621215145105

management_flyway_schema_history 表中,出现了一条迁移脚本的记录:
image-20200621215313344

各字段含义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
CREATE TABLE `management_flyway_schema_history` (
`installed_rank` int(11) NOT NULL, -- 安装顺序,从 1 开始递增。
`version` varchar(50) COLLATE utf8mb4_bin DEFAULT NULL, -- 版本号
`description` varchar(200) COLLATE utf8mb4_bin NOT NULL, -- 迁移脚本描述
`type` varchar(20) COLLATE utf8mb4_bin NOT NULL, -- 脚本类型,目前有 SQL 和 Java 。
`script` varchar(1000) COLLATE utf8mb4_bin NOT NULL, -- 脚本地址
`checksum` int(11) DEFAULT NULL, -- 脚本校验码。避免已经执行的脚本,被人变更了。
`installed_by` varchar(100) COLLATE utf8mb4_bin NOT NULL, -- 执行脚本的数据库用户
`installed_on` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, -- 安装时间
`execution_time` int(11) NOT NULL, -- 执行时长,单位毫秒
`success` tinyint(1) NOT NULL, -- 执行结果是否成功。1-成功。0-失败
PRIMARY KEY (`installed_rank`),
KEY `management_flyway_schema_history_s_idx` (`success`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

再次启动项目,日志如下:

1
2
3
4
5
6
7
2020-06-21 21:55:05.064  INFO 40853 --- [           main] o.f.c.internal.license.VersionPrinter    : Flyway Community Edition 5.2.4 by Boxfuse
2020-06-21 21:55:10.977 INFO 40853 --- [ main] o.f.c.internal.database.DatabaseFactory : Database: jdbc:mysql://yizhuo.com:3306/flyway (MySQL 5.6)
2020-06-21 21:55:11.229 WARN 40853 --- [ main] o.f.c.i.s.classpath.ClassPathScanner : Unable to resolve location classpath:com/github/zhuyizhuo/flyway/demo/migration
2020-06-21 21:55:11.548 INFO 40853 --- [ main] o.f.core.internal.command.DbValidate : Successfully validated 1 migration (execution time 00:00.212s)
2020-06-21 21:55:11.827 INFO 40853 --- [ main] o.f.core.internal.command.DbMigrate : Current version of schema `flyway`: 1.0
2020-06-21 21:55:11.857 INFO 40853 --- [ main] o.f.core.internal.command.DbMigrate : Schema `flyway` is up to date. No migration necessary.
2020-06-21 21:55:12.257 INFO 40853 --- [ main] c.g.z.flyway.demo.FlywayApplication : Started FlywayApplication in 12.763 seconds (JVM running for 14.531)

会发现当前已经是最新版本,无需执行迁移。
我们来修改一下V1.0__INIT_USERS.sql 脚本,在第一行插入一个空格,再次启动项目,会报如下错误:

1
2
3
Caused by: org.flywaydb.core.api.FlywayException: Validate failed: Migration checksum mismatch for migration version 1.0
-> Applied to database : -1350962438
-> Resolved locally : -1984295967

根据提示信息可以看出,因为该脚本的校验和发生了变化,所以抛出异常。这样的设计,保证不会因为已执行的脚本被改动,而出现不一致的问题。

方式二:Java-based migrations 创建 Java 类 [使用自定义类名]

com.github.zhuyizhuo.flyway.demo.migration包下,创建 UpdateUserName类,修改Users中初始化数据的用户名,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
public class UpdateUserName implements JavaMigration {
private Logger logger = LoggerFactory.getLogger(getClass());

@Override
public void migrate(Context context) throws Exception {
// 创建 JdbcTemplate ,方便 JDBC 操作
JdbcTemplate template = new JdbcTemplate(context.getConfiguration().getDataSource());
// 查询所有用户,如果用户名为 张三 ,则变更成 李四
template.query("SELECT id, username, password, create_time FROM users", new RowCallbackHandler() {
@Override
public void processRow(ResultSet rs) throws SQLException {
// 遍历返回的结果
do {
String username = rs.getString("username");
if ("张三".equals(username)) {
Integer id = rs.getInt("id");
template.update("UPDATE users SET username = ? WHERE id = ?",
"李四", id);
logger.info("[migrate][更新 user({}) 的用户名({} => {})", id, username, "李四");
}
} while (rs.next());
}
});
}
/** 版本号为 1.1 */
@Override
public MigrationVersion getVersion() {
return MigrationVersion.fromVersion("1.1");
}
/** 描述 */
@Override
public String getDescription() {
return "update user name";
}
@Override
public boolean isUndo() {
return false;
}
/** 执行是否应在事务内部进行 */
@Override
public boolean canExecuteInTransaction() {
return true;
}
/**
* 默认校验和为 0 需重写 返回自己自定义计算的校验和 此处 demo 返回固定值11
*/
@Override
public Integer getChecksum() {
return 11;
}
}
再次启动 Spring Boot 项目

执行日志如下:

1
2
3
4
5
6
7
8
2020-06-21 22:17:27.999  INFO 41175 --- [           main] o.f.c.internal.license.VersionPrinter    : Flyway Community Edition 5.2.4 by Boxfuse
2020-06-21 22:17:33.717 INFO 41175 --- [ main] o.f.c.internal.database.DatabaseFactory : Database: jdbc:mysql://yizhuo.com:3306/flyway (MySQL 5.6)
2020-06-21 22:17:34.135 INFO 41175 --- [ main] o.f.core.internal.command.DbValidate : Successfully validated 2 migrations (execution time 00:00.189s)
2020-06-21 22:17:34.410 INFO 41175 --- [ main] o.f.core.internal.command.DbMigrate : Current version of schema `flyway`: 1.0
2020-06-21 22:17:34.441 INFO 41175 --- [ main] o.f.core.internal.command.DbMigrate : Migrating schema `flyway` to version 1.1 - update user name
2020-06-21 22:17:35.067 INFO 41175 --- [ main] c.g.z.f.demo.migration.UpdateUserName : [migrate][更新 user(1) 的用户名(张三 => 李四)
2020-06-21 22:17:35.410 INFO 41175 --- [ main] o.f.core.internal.command.DbMigrate : Successfully applied 1 migration to schema `flyway` (execution time 00:01.126s)
2020-06-21 22:17:35.667 INFO 41175 --- [ main] c.g.z.flyway.demo.FlywayApplication : Started FlywayApplication in 12.281 seconds (JVM running for 14.572)

再次查询 mysql ,发现数据已经被更新
image-20200621222842067
并且在 management_flyway_schema_history 表中,增加了一条版本号为1.1的迁移脚本的日志。
image-20200621222930230

方式二:Java-based migrations 创建 Java 类 [使用 Flyway 的命名方式命名]

com.github.zhuyizhuo.flyway.demo.migration包下,创建 V1_2__Another_user类,新增初始化数据,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class V1_2__Another_user extends BaseJavaMigration {

@Override
public void migrate(Context context) throws Exception {
new JdbcTemplate(new SingleConnectionDataSource(context.getConnection(), true))
.execute("INSERT INTO users (username, password, create_time) VALUES ('James', 'E10ADC3949BA59ABBE56E057F20F883E', now())");
}

/**
* 默认校验和为 0 需重写 返回自己自定义计算的校验和 此处 demo 返回固定值12
*/
@Override
public Integer getChecksum() {
return 12;
}
}
再再次启动项目。

日志省略。
查看数据库数据,新增了一条James 的数据如下:

image-20200621225335153

management_flyway_schema_history 表中,增加了一条版本号为1.2的迁移脚本的日志。image-20200621225959300

代码示例

本文的完整工程可以查看下面仓库中的spring-boot-samples-flyway目录:

如果您觉得本文不错,欢迎Star支持,您的关注是我坚持的动力!

请作者喝杯咖啡吧