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

但是在大多数项目中,我们的数据库都是像以下这样:

通常,我们的环境不仅仅只有一套,一般来说会有 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 migrations 和 Java-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> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> </dependency>
<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
配置如下:
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
spring.flyway.enabled=true
spring.flyway.cleanDisabled=true
spring.flyway.table=management_flyway_schema_history
spring.flyway.locations[0]=classpath:db/migration
spring.flyway.locations[1]=classpath:com.github.zhuyizhuo.flyway.demo.migration
spring.flyway.check-location=false
spring.flyway.url=${spring.datasource.url}
spring.flyway.user=${spring.datasource.username}
spring.flyway.password=${spring.datasource.password}
|
在 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;
INSERT INTO `users`(username, password, create_time) VALUES('张三', 'E10ADC3949BA59ABBE56E057F20F883E', now());
|
Flyway 规定脚本 V1.0__INIT_USERS.sql
的命名必须遵循以下命名模式:

- 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)
|
查看数据库,发现表已经正常创建。并且插入了初始化数据。

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

各字段含义如下:
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, `version` varchar(50) COLLATE utf8mb4_bin DEFAULT NULL, `description` varchar(200) COLLATE utf8mb4_bin NOT NULL, `type` varchar(20) COLLATE utf8mb4_bin NOT NULL, `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, 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
|
根据提示信息可以看出,因为该脚本的校验和发生了变化,所以抛出异常。这样的设计,保证不会因为已执行的脚本被改动,而出现不一致的问题。
在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 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()); } }); } @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; }
@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 ,发现数据已经被更新

并且在 management_flyway_schema_history
表中,增加了一条版本号为1.1
的迁移脚本的日志。

在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())"); }
@Override public Integer getChecksum() { return 12; } }
|
再再次启动项目。
日志省略。
查看数据库数据,新增了一条James
的数据如下:
在 management_flyway_schema_history
表中,增加了一条版本号为1.2
的迁移脚本的日志。
代码示例
本文的完整工程可以查看下面仓库中的spring-boot-samples-flyway
目录:
如果您觉得本文不错,欢迎Star
支持,您的关注是我坚持的动力!