《Dubbo 实现原理与源码解析 —— 精品合集》 《Netty 实现原理与源码解析 —— 精品合集》
《Spring 实现原理与源码解析 —— 精品合集》 《MyBatis 实现原理与源码解析 —— 精品合集》
《Spring MVC 实现原理与源码解析 —— 精品合集》 《数据库实体设计合集》
《Spring Boot 实现原理与源码解析 —— 精品合集》 《Java 面试题 + Java 学习指南》

摘要: 原创出处 http://www.iocoder.cn/Spring-Boot/database-version-control/ 「芋道源码」欢迎转载,保留摘要,谢谢!


🙂🙂🙂关注微信公众号:【芋道源码】有福利:

  1. RocketMQ / MyCAT / Sharding-JDBC 所有源码分析文章列表
  2. RocketMQ / MyCAT / Sharding-JDBC 中文注释源码 GitHub 地址
  3. 您对于源码的疑问每条留言将得到认真回复。甚至不知道如何读源码也可以请教噢
  4. 新的源码解析文章实时收到通知。每周更新一篇左右
  5. 认真的源码交流微信群。

本文在提供完整代码示例,可见 https://github.com/YunaiV/SpringBoot-Labslab-20 目录。

原创不易,给点个 Star 嘿,一起冲鸭!

1. 概述

在我们的认知中,我们会使用 SVN 或 Git 进行代码的版本管理。但是,我们是否好奇过,数据库也是需要进行版本管理的呢?

在每次发版的时候,我们可能都会对数据库的表结构进行新增和变更,又或者需要插入一些初始化的数据。而我们的环境不仅仅只有一套,一般来说会有 DEV、UAT、PRED、PROD 四套环境,会对应 DEV、UAT、PROD 三个数据库。

PROD 环境,一般连接 PROD 数据库,做准生产的验收。

那么,就意味着我们需要对 DEV、UAT、PROD 数据库都做一遍操作。“人,是系统最大的 BUG”。很多时候,我们并不能保证像机器一样,每次都操作都正确,这就导致在不同的环境下,因为数据的每个版本的初始化,带来额外的验收成本。

甚至说,因为我们常常是手动操作 DEV 数据库,没有整理一个完整清单,保证我们在 UAT、PROD 数据库中执行相同的操作。

基于以上种种,如果我们能像管理代码版本一样,来管理我们的数据库版本,是不是这些问题可以得到很好的解决?答案是,绝大多数是的。

目前,技术社区已经提供了很多解决方案。例如说:

在 Spring Boot 项目中,提供了对 Flyway 和 Liquibase 的内置支持,所以在有数据库版本的需求时,肯定是推荐它们两。

本文,我们会对 Flyway 和 Liquibase 进行入门学习。这样,我们在学习它们的同时,可以有比较直观的使用感受,方便后续我们对它们进行选型。

2. Flyway

示例代码对应仓库:lab-20-database-version-control-flyway

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

Version control for your database.

数据库的版本管理。

Flyway 支持的数据库,主要是关系数据库。如下图所示:支持的数据库

Flyway 提供了 SQL-based migrationsJava-based migrations 两种数据库变更方式。

  • 前者使用简单,无需编写 Java 代码。
  • 后者需要使用 Java 编写代码,胜在灵活。

一般情况下,如果是做表的变更,或者记录的简单插入、更新、删除等操作,使用 SQL-based migrations 即可。

复杂场景下,我们可能需要关联多个表,则需要通过编写 Java 代码,进行逻辑处理,此时就是和使用 Java-based migrations 了。

下面,让我们来使用它们二者,更好的体会它们的区别。

2.1 引入依赖

pom.xml 文件中,引入相关依赖。

<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.3.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<modelVersion>4.0.0</modelVersion>

<artifactId>lab-20-database-version-control-flyway</artifactId>

<dependencies>
<!-- 实现对数据库连接池的自动化配置 -->
<!-- 同时,spring-boot-starter-jdbc 支持 Flyway 的自动化配置 -->
<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>

<!-- Flyway 依赖 -->
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-core</artifactId>
</dependency>
</dependencies>

</project>

具体每个依赖的作用,胖友自己认真看下艿艿添加的所有注释噢。

2.2 应用配置文件

resources 目录下,创建 application.yaml 配置文件。配置如下:

spring:
# datasource 数据源配置内容,对应 DataSourceProperties 配置属性类
datasource:
url: jdbc:mysql://127.0.0.1:3306/lab-20-flyway?useSSL=false&useUnicode=true&characterEncoding=UTF-8
driver-class-name: com.mysql.jdbc.Driver
username: root # 数据库账号
password: # 数据库密码
# flyway 配置内容,对应 FlywayAutoConfiguration.FlywayConfiguration 配置项
flyway:
enabled: true # 开启 Flyway 功能
cleanDisabled: true # 禁用 Flyway 所有的 drop 相关的逻辑,避免出现跑路的情况。
locations: # 迁移脚本目录
- classpath:db/migration # 配置 SQL-based 的 SQL 脚本在该目录下
- classpath:cn.iocoder.springboot.lab20.databaseversioncontrol.migration # 配置 Java-based 的 Java 文件在该目录下
check-location: false # 是否校验迁移脚本目录下。如果配置为 true ,代表需要校验。此时,如果目录下没有迁移脚本,会抛出 IllegalStateException 异常
url: jdbc:mysql://127.0.0.1:3306/lab-20-flyway?useSSL=false&useUnicode=true&characterEncoding=UTF-8 # 数据库地址
user: root # 数据库账号
password: # 数据库密码
  • spring.datasource 配置项,设置数据源的配置。这里暂时没有实际作用,仅仅是为了项目不报数据源的错误。
  • spring.flyway 配置项,设置 Flyway 的属性,而后可以被 FlywayAutoConfiguration 自动化配置。
    • 每个配置项的作用,胖友自己看下注释。更多的配置项,可以看看 《Spring Boot 配置属性详解 – Migration》 文章。
    • 重点看下 locations 配置项,我们分别设置了 SQL 和 Java 迁移脚本的所在目录。

2.3 Application

创建 Application.java 类,配置 @SpringBootApplication 注解即可。代码如下:

// Application.java

@SpringBootApplication
public class Application {

public static void main(String[] args) {
// 启动 Spring Boot 应用
SpringApplication.run(Application.class, args);
}

}

启动项目。执行日志如下:

// Flyway 的信息
2019-11-16 13:42:34.454 INFO 59115 --- [ main] o.f.c.internal.license.VersionPrinter : Flyway Community Edition 5.2.4 by Boxfuse
2019-11-16 13:42:34.619 INFO 59115 --- [ main] o.f.c.internal.database.DatabaseFactory : Database: jdbc:mysql://127.0.0.1:3306/lab-20-flyway (MySQL 8.0)
2019-11-16 13:42:34.643 WARN 59115 --- [ main] o.f.c.i.s.classpath.ClassPathScanner : Unable to resolve location classpath:db/migration
// 发现 0 个迁移脚本。
2019-11-16 13:42:34.657 INFO 59115 --- [ main] o.f.core.internal.command.DbValidate : Successfully validated 0 migrations (execution time 00:00.004s)
// 创建 flyway_schema_history 表
2019-11-16 13:42:34.671 INFO 59115 --- [ main] o.f.c.i.s.JdbcTableSchemaHistory : Creating Schema History table: `lab-20-flyway`.`flyway_schema_history`
// 打印当前数据库的迁移版本
2019-11-16 13:42:34.702 INFO 59115 --- [ main] o.f.core.internal.command.DbMigrate : Current version of schema `lab-20-flyway`: << Empty Schema >>
// 判断,没有需要迁移的脚本
2019-11-16 13:42:34.702 INFO 59115 --- [ main] o.f.core.internal.command.DbMigrate : Schema `lab-20-flyway` is up to date. No migration necessary.
// 启动项目完成
2019-11-16 13:42:34.759 INFO 59115 --- [ main] c.i.s.l.d.Application : Started Application in 1.2 seconds (JVM running for 1.596)
  • 看下艿艿在日志内容上的注释。

在启动的日志中,我们看到 Flyway 会自动创建 flyway_schema_history 表,记录 Flyway 每次迁移( migration )的历史。表结构如下:

CREATE TABLE `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 `flyway_schema_history_s_idx` (`success`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
  • 大体看下每个字段的注释,后面对着具体的记录,会更容易理解。

2.4 SQL-based migrations

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

-- 创建用户表
CREATE TABLE `users` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '用户编号',
`username` varchar(64) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '账号',
`password` varchar(32) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '密码',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
PRIMARY KEY (`id`),
UNIQUE KEY `idx_username` (`username`)
) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;

-- 插入一条数据
INSERT INTO `users`(username, password, create_time) VALUES('yudaoyuanma', 'password', now());
  • 比较简单,就是创建用户表 users 表,并往里面插入一条记录。

重点在于 V1.0__INIT_DB.sql 的命名上。Flyway 约定如下:

FROM https://flywaydb.org/documentation/migrations#naming-1

Naming

  • Prefix 前缀:V 为版本迁移,U 为回滚迁移,R 为可重复迁移。

    在我们的示例中,我们使用 V 前缀,表示版本迁移。绝大多数情况下,我们只会使用 V 前缀。

  • Version 版本号:每一个迁移脚本,都需要一个对应一个唯一的版本号。而脚本的执行顺序,按照版本号的顺序。一般情况下,我们使用数字自增即可。

    在我们的示例中,我们使用 1.0

  • Separator 分隔符:两个 _ ,即 __ 。可配置,不过一般不配置。

  • Description 描述:描述脚本的用途。

    在我们的示例中,我们使用 INIT_DB

  • Suffix 后缀:.sql 。可配置,不过一般不配置。

我们再次启动 Application 项目。执行日志如下:

// Flyway 的信息
2019-11-16 14:20:25.893 INFO 59615 --- [ main] o.f.c.internal.license.VersionPrinter : Flyway Community Edition 5.2.4 by Boxfuse
2019-11-16 14:20:26.063 INFO 59615 --- [ main] o.f.c.internal.database.DatabaseFactory : Database: jdbc:mysql://127.0.0.1:3306/lab-20-flyway (MySQL 8.0)
// 发现一个迁移脚本,就是 V1.0__INIT_DB.sql 。
2019-11-16 14:20:26.096 INFO 59615 --- [ main] o.f.core.internal.command.DbValidate : Successfully validated 1 migration (execution time 00:00.013s)
// 打印当前数据库的迁移版本
2019-11-16 14:20:26.137 INFO 59615 --- [ main] o.f.core.internal.command.DbMigrate : Current version of schema `lab-20-flyway`: << Empty Schema >>
// 开始迁移到版本 1.0
2019-11-16 14:20:26.138 INFO 59615 --- [ main] o.f.core.internal.command.DbMigrate : Migrating schema `lab-20-flyway` to version 1.0 - INIT DB
// 可以忽略,MySQL 报的告警日志
2019-11-16 14:20:26.148 WARN 59615 --- [ main] o.f.c.i.s.DefaultSqlScriptExecutor : DB: Integer display width is deprecated and will be removed in a future release. (SQL State: HY000 - Error Code: 1681)
// 成功执行一个迁移
2019-11-16 14:20:26.157 INFO 59615 --- [ main] o.f.core.internal.command.DbMigrate : Successfully applied 1 migration to schema `lab-20-flyway` (execution time 00:00.049s)
// 启动项目完成
2019-11-16 14:20:26.214 INFO 59615 --- [ main] c.i.s.l.d.Application : Started Application in 1.236 seconds (JVM running for 1.638)
  • 看下艿艿在日志内容上的注释。

此时,我们去查询下 MySQL 。如下:

mysql> show tables;
+-------------------------+
| Tables_in_lab-20-flyway |
+-------------------------+
| flyway_schema_history |
| users |
+-------------------------+
2 rows in set (0.00 sec)
# 如上,我们可以看到两个表。
# 其中,`users` 表,就是我们需要在 `V1.0__INIT_DB.sql` 迁移脚本中,需要创建的。

mysql> SELECT * FROM users;
+----+-------------+----------+---------------------+
| id | username | password | create_time |
+----+-------------+----------+---------------------+
| 7 | yudaoyuanma | password | 2019-11-16 14:21:32 |
+----+-------------+----------+---------------------+
1 row in set (0.00 sec)
# `users` 表的该记录,就是我们希望插入的一条记录。

mysql> SELECT * FROM flyway_schema_history;
+----------------+---------+-------------+------+-------------------+-------------+--------------+---------------------+----------------+---------+
| installed_rank | version | description | type | script | checksum | installed_by | installed_on | execution_time | success |
+----------------+---------+-------------+------+-------------------+-------------+--------------+---------------------+----------------+---------+
| 1 | 1.0 | INIT DB | SQL | V1.0__INIT_DB.sql | -1362702755 | root | 2019-11-16 14:21:32 | 12 | 1 |
+----------------+---------+-------------+------+-------------------+-------------+--------------+---------------------+----------------+---------+
1 row in set (0.00 sec)
# `flyway_schema_history` 表中,增加了一条版本号为 `1.0` 的,使用 `V1.0__INIT_DB.sql` 迁移脚本的日志。
  • 看下每个操作,以及其注释。

我们再再次启动 Application 项目。执行日志如下:

// Flyway 的信息
2019-11-16 14:30:10.925 INFO 59715 --- [ main] o.f.c.internal.license.VersionPrinter : Flyway Community Edition 5.2.4 by Boxfuse
2019-11-16 14:30:11.089 INFO 59715 --- [ main] o.f.c.internal.database.DatabaseFactory : Database: jdbc:mysql://127.0.0.1:3306/lab-20-flyway (MySQL 8.0)
// 发现一个迁移脚本,就是 V1.0__INIT_DB.sql 。
2019-11-16 14:30:11.127 INFO 59715 --- [ main] o.f.core.internal.command.DbValidate : Successfully validated 1 migration (execution time 00:00.014s)
// 打印当前数据库的迁移版本为 `1.0`
2019-11-16 14:30:11.137 INFO 59715 --- [ main] o.f.core.internal.command.DbMigrate : Current version of schema `lab-20-flyway`: 1.0
// 判断已经到达最新版本,无需执行迁移
2019-11-16 14:30:11.137 INFO 59715 --- [ main] o.f.core.internal.command.DbMigrate : Schema `lab-20-flyway` is up to date. No migration necessary.
// 启动项目完成
2019-11-16 14:30:11.196 INFO 59715 --- [ main] c.i.s.l.d.Application : Started Application in 1.141 seconds (JVM running for 1.528)

下面,我们注释掉 V1.0__INIT_DB.sql 迁移脚本中的,INSERT 操作。我们再再再次启动 Application 项目。会报如下错误:

org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'flywayInitializer' defined in class path resource [org/springframework/boot/autoconfigure/flyway/FlywayAutoConfiguration$FlywayConfiguration.class]: Invocation of init method failed; nested exception is org.flywaydb.core.api.FlywayException: Validate failed: Migration checksum mismatch for migration version 1.0
-> Applied to database : -1362702755
-> Resolved locally : -883795183
  • Flyway 会给每个迁移脚本,计算出一个 checksum 字段。这样,每次启动时,都会校验已经安装( installed )的迁移脚本,是否发生了改变。如果是,抛出异常。这样,保证不会因为脚本变更,导致出现问题。

2.5 Java-based migrations

cn.iocoder.springboot.lab20.databaseversioncontrol.migration 包路径下,创建 V1_1__FixUsername.java 类,修复 users 的用户名。代码如下:

// V1_1__FixUsername.java

public class V1_1__FixUsername extends BaseJavaMigration {

private Logger logger = LoggerFactory.getLogger(getClass());

@Override
public void migrate(Context context) throws Exception {
// 创建 JdbcTemplate ,方便 JDBC 操作
JdbcTemplate template = new JdbcTemplate(context.getConfiguration().getDataSource());
// 查询所有用户,如果用户名为 yudaoyuanma ,则变更成 yutou
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 ("yudaoyuanma".equals(username)) {
Integer id = rs.getInt("id");
template.update("UPDATE users SET username = ? WHERE id = ?",
"yutou", id);
logger.info("[migrate][更新 user({}) 的用户名({} => {})", id, username, "yutou");
}
} while (rs.next());
}
});
}

@Override
public Integer getChecksum() {
return 11; // 默认返回,是 null 。
}

@Override
public boolean canExecuteInTransaction() {
return true; // 默认返回,也是 true
}

@Override
public MigrationVersion getVersion() {
return super.getVersion(); // 默认按照约定的规则,从类名中解析获得。可以自定义
}

}
  • 比较简单,胖友看下代码注释。这里仅仅是示例,实际迁移的逻辑,会更加复杂。
  • Java 迁移脚本,可以通过类名按照和 「2.4 SQL-based migrations」 一样的命名约定,自动获得版本号。当然,也可以通过重写 #getVersion() 方法,自定义版本号。

我们再再再再次启动 Application 项目。执行日志如下:

// Flyway 的信息
2019-11-16 14:45:30.733 INFO 59941 --- [ main] o.f.c.internal.license.VersionPrinter : Flyway Community Edition 5.2.4 by Boxfuse
2019-11-16 14:45:30.907 INFO 59941 --- [ main] o.f.c.internal.database.DatabaseFactory : Database: jdbc:mysql://127.0.0.1:3306/lab-20-flyway (MySQL 8.0)
// 发现一个迁移脚本,就是 V1.0__INIT_DB.sql 和 V1_1__FixUsername.java
2019-11-16 14:45:30.946 INFO 59941 --- [ main] o.f.core.internal.command.DbValidate : Successfully validated 2 migrations (execution time 00:00.014s)
// 打印当前数据库的迁移版本为 `1.0`
2019-11-16 14:45:30.956 INFO 59941 --- [ main] o.f.core.internal.command.DbMigrate : Current version of schema `lab-20-flyway`: 1.0
// 开始迁移到版本 1.1
2019-11-16 14:45:30.957 INFO 59941 --- [ main] o.f.core.internal.command.DbMigrate : Migrating schema `lab-20-flyway` to version 1.1 - FixUsername
2019-11-16 14:45:30.977 INFO 59941 --- [ main] c.i.s.l.d.migration.V1_1__FixUsername : [migrate][更新 user(7) 的用户名(yudaoyuanma => yutou)
// 成功执行一个迁移
2019-11-16 14:45:30.985 INFO 59941 --- [ main] o.f.core.internal.command.DbMigrate : Successfully applied 1 migration to schema `lab-20-flyway` (execution time 00:00.034s)
// 启动项目完成
2019-11-16 14:45:31.039 INFO 59941 --- [ main] c.i.s.l.d.Application : Started Application in 1.221 seconds (JVM running for 1.61)

此时,我们去查询下 MySQL 。如下:

mysql> SELECT * FROM users;
+----+-------------+----------+---------------------+
| id | username | password | create_time |
+----+-------------+----------+---------------------+
| 7 | yutou | password | 2019-11-16 14:21:32 |
+----+-------------+----------+---------------------+
1 row in set (0.00 sec)
# `users` 表的该记录,用户名被修改为 yutou 。

mysql> SELECT * FROM flyway_schema_history;
+----------------+---------+-------------+------+--------------------------------------------------------------------------------+-------------+--------------+---------------------+----------------+---------+
| installed_rank | version | description | type | script | checksum | installed_by | installed_on | execution_time | success |
+----------------+---------+-------------+------+--------------------------------------------------------------------------------+-------------+--------------+---------------------+----------------+---------+
| 1 | 1.0 | INIT DB | SQL | V1.0__INIT_DB.sql | -1362702755 | root | 2019-11-16 14:21:32 | 12 | 1 |
| 2 | 1.1 | FixUsername | JDBC | cn.iocoder.springboot.lab20.databaseversioncontrol.migration.V1_1__FixUsername | 11 | root | 2019-11-16 14:45:30 | 19 | 1 |
+----------------+---------+-------------+------+--------------------------------------------------------------------------------+-------------+--------------+---------------------+----------------+---------+
2 rows in set (0.00 sec)
# `flyway_schema_history` 表中,增加了一条版本号为 `1.1` 的,使用 `V1_1__FixUsername.sql` 迁移脚本的日志。

2.6 其它功能

Flyway 支持 SQL CallbacksJava Callbacks 两种回调方式,让我们在 Flyway 的执行过程中,可以实现自定义的拓展。

在上述的示例,我们是基于 Spring Boot 的使用方式。而 Flyway 还提供了如下方式:

😈 Flyway 还有一些其它细节,建议抽时间,通读下 Documentation 文档。

3. Liquibase

示例代码对应仓库:lab-20-database-version-control-liquibase

在 Liquibase 的官网 https://www.liquibase.org/ 中,对自己的介绍是:

Liquibase is the leading open source tool for database change and deployment management.

Liquibase 是用于数据库变更和部署管理的领先的开源工具。

Liquibase 支持的数据库,主要是关系数据库。如下图表格:

Database Type Name Notes
MySQL mysql No Issues
MariaDB mysql MariaDB is 100% compatible with MySQL per MariaDB developers
PostgreSQL postgresql 8.2+ is required to use the “drop all database objects” functionality.
Oracle oracle 11g driver is required when using the diff tool on databases running with AL32UTF8 or AL16UTF16
SQL Server mssql No Issues
Sybase_Enterprise sybase ASE 12.0+ required. “select into” database option needs to be set. Best driver is JTDS. Sybase does not support transactions for DDL so rollbacks will not work on failures. Foreign keys can not be dropped which can break the rollback or dropAll functionality.
Sybase_Anywhere asany Since 1.9
DB2 db2 No Issues. Will auto-call REORG when necessary.
Apache_Derby derby No Issues
HSQL hsqldb No Issues
H2 h2 No Issues
Informix informix No Issues
Firebird firebird No Issues
SQLite sqlite No Issues

Liquibase 通过在变更日志( Change Log )文件,配置每一个变更集( Change Set ),实现数据库变更的管理。

Liquibase 提供了多种格式,如下:

在 Spring Boot 中,默认配置使用 YAML Format 。所以我们在入门的示例中,也使用这种格式。

Liquibase 在变更集( Change Set )中,除了提供了和 Flyway 的 SQL-based migrationsJava-based migrations 方式之外,额外提供了基于配置,自动生成对应的 SQL 操作。我们姑且称它为 “Property-based migrations” 吧。

下面,就让我们开始入门 Liquibase 吧。

3.1 引入依赖

pom.xml 文件中,引入相关依赖。

<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.3.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<modelVersion>4.0.0</modelVersion>

<artifactId>lab-20-database-version-control-liquibase</artifactId>

<dependencies>
<!-- 实现对数据库连接池的自动化配置 -->
<!-- 同时,spring-boot-starter-jdbc 支持 Liquibase 的自动化配置 -->
<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>

<!-- Liquibase 依赖 -->
<dependency>
<groupId>org.liquibase</groupId>
<artifactId>liquibase-core</artifactId>
</dependency>
</dependencies>

</project>

具体每个依赖的作用,胖友自己认真看下艿艿添加的所有注释噢。

3.2 应用配置文件

resources 目录下,创建 application.yaml 配置文件。配置如下:

spring:
# datasource 数据源配置内容,对应 DataSourceProperties 配置属性类
datasource:
url: jdbc:mysql://127.0.0.1:3306/lab-20-liquibase?useSSL=false&useUnicode=true&characterEncoding=UTF-8
driver-class-name: com.mysql.jdbc.Driver
username: root # 数据库账号
password: # 数据库密码
# Liquibase 配置内容,对应 LiquibaseProperties 配置项
liquibase:
enabled: true # 开启 Liquibase 功能。默认为 true 。
change-log: classpath:/db/changelog/db.changelog-master.yaml # Liquibase 配置文件地址
url: jdbc:mysql://127.0.0.1:3306/lab-20-liquibase?useSSL=false&useUnicode=true&characterEncoding=UTF-8 # 数据库地址
user: root # 数据库账号
password: # 数据库密码
  • spring.datasource 配置项,设置数据源的配置。这里暂时没有实际作用,仅仅是为了项目不报数据源的错误。
  • spring.liquibase 配置项,设置 Liquibase 的属性,而后可以被 LiquibaseAutoConfiguration 自动化配置。
    • 每个配置项的作用,胖友自己看下注释。更多的配置项,可以看看 《Spring Boot 配置属性详解 – Migration》 文章。
    • 重点看下 change-log 配置项,我们设置了变更日志( Change Log )文件的路径为 "classpath:/db/changelog/db.changelog-master.yaml"

3.3 变更文件 Change Log

resources/db/changelog 目录下,创建 db.changelog-master.yaml 变更文件。如下:

databaseChangeLog:
- changeSet: # 对应一个 ChangeSet 对象
id: 0 # ChangeSet 编号
author: yunai # 作者
comments: # 备注
  • databaseChangeLog 配置下,我们可以配置多个 changeSet 配置项。每个 changeSet 配置项,代表一个 变更集( Change Set )。
  • 这里,我们先配置一个“空”的 changeSet 配置项,方便我们稍后启动项。😈 真正的 changeSet 配置项,我们会按照 Property-based migrations、SQL-based migrations、Java-based migrations 示例顺序,详细解释。

3.4 Application

创建 Application.java 类,配置 @SpringBootApplication 注解即可。代码如下:

// Application.java

@SpringBootApplication
public class Application {

public static void main(String[] args) {
// 启动 Spring Boot 应用
SpringApplication.run(Application.class, args);
}

}

启动项目。执行日志如下:

// 获得 DATABASECHANGELOGLOCK 数量。此时,该 DATABASECHANGELOGLOCK 表是不存在的。
2019-11-16 19:37:01.679 INFO 65543 --- [ main] liquibase.executor.jvm.JdbcExecutor : SELECT COUNT(*) FROM `lab-20-liquibase`.DATABASECHANGELOGLOCK
// 因为 DATABASECHANGELOGLOCK 表不存在,所以进行创建
2019-11-16 19:37:01.697 INFO 65543 --- [ main] liquibase.executor.jvm.JdbcExecutor : CREATE TABLE `lab-20-liquibase`.DATABASECHANGELOGLOCK (ID INT NOT NULL, `LOCKED` BIT(1) NOT NULL, LOCKGRANTED datetime NULL, LOCKEDBY VARCHAR(255) NULL, CONSTRAINT PK_DATABASECHANGELOGLOCK PRIMARY KEY (ID))
// 重新获得 DATABASECHANGELOGLOCK 数量。
2019-11-16 19:37:01.704 INFO 65543 --- [ main] liquibase.executor.jvm.JdbcExecutor : SELECT COUNT(*) FROM `lab-20-liquibase`.DATABASECHANGELOGLOCK
// 删除 DATABASECHANGELOGLOCK 所有记录。为什么这么做,暂时不造,没去细研究。猜测,避免其他 JVM 进程,异常崩溃,未释放锁
2019-11-16 19:37:01.709 INFO 65543 --- [ main] liquibase.executor.jvm.JdbcExecutor : DELETE FROM `lab-20-liquibase`.DATABASECHANGELOGLOCK
// 插入 DATABASECHANGELOGLOCK 记录,id = 1
2019-11-16 19:37:01.709 INFO 65543 --- [ main] liquibase.executor.jvm.JdbcExecutor : INSERT INTO `lab-20-liquibase`.DATABASECHANGELOGLOCK (ID, `LOCKED`) VALUES (1, 0)
// 查询 DATABASECHANGELOGLOCK 记录,id = 1
2019-11-16 19:37:01.711 INFO 65543 --- [ main] liquibase.executor.jvm.JdbcExecutor : SELECT `LOCKED` FROM `lab-20-liquibase`.DATABASECHANGELOGLOCK WHERE ID=1
// 成功获得到 DATABASECHANGELOGLOCK 锁
2019-11-16 19:37:01.716 INFO 65543 --- [ main] l.lockservice.StandardLockService : Successfully acquired change log lock

// 创建 DATABASECHANGELOG 表
2019-11-16 19:37:02.485 INFO 65543 --- [ main] l.c.StandardChangeLogHistoryService : Creating database history table with name: `lab-20-liquibase`.DATABASECHANGELOG
2019-11-16 19:37:02.486 INFO 65543 --- [ main] liquibase.executor.jvm.JdbcExecutor : CREATE TABLE `lab-20-liquibase`.DATABASECHANGELOG (ID VARCHAR(255) NOT NULL, AUTHOR VARCHAR(255) NOT NULL, FILENAME VARCHAR(255) NOT NULL, DATEEXECUTED datetime NOT NULL, ORDEREXECUTED INT NOT NULL, EXECTYPE VARCHAR(10) NOT NULL, MD5SUM VARCHAR(35) NULL, `DESCRIPTION` VARCHAR(255) NULL, COMMENTS VARCHAR(255) NULL, TAG VARCHAR(255) NULL, LIQUIBASE VARCHAR(20) NULL, CONTEXTS VARCHAR(255) NULL, LABELS VARCHAR(255) NULL, DEPLOYMENT_ID VARCHAR(10) NULL)
// 获得 DATABASECHANGELOG 数量。
2019-11-16 19:37:02.494 INFO 65543 --- [ main] liquibase.executor.jvm.JdbcExecutor : SELECT COUNT(*) FROM `lab-20-liquibase`.DATABASECHANGELOG
2019-11-16 19:37:02.496 INFO 65543 --- [ main] l.c.StandardChangeLogHistoryService : Reading from `lab-20-liquibase`.DATABASECHANGELOG
// 获得 DATABASECHANGELOG 所有记录。
2019-11-16 19:37:02.496 INFO 65543 --- [ main] liquibase.executor.jvm.JdbcExecutor : SELECT * FROM `lab-20-liquibase`.DATABASECHANGELOG ORDER BY DATEEXECUTED ASC, ORDEREXECUTED ASC
// 获得 DATABASECHANGELOGLOCK 数量。
2019-11-16 19:37:02.497 INFO 65543 --- [ main] liquibase.executor.jvm.JdbcExecutor : SELECT COUNT(*) FROM `lab-20-liquibase`.DATABASECHANGELOGLOCK

// 读取 Change Log 文件的 Change Set 配置
2019-11-16 19:37:02.500 INFO 65543 --- [ main] liquibase.changelog.ChangeSet : ChangeSet classpath:/db/changelog/db.changelog-master.yaml::0::yunai ran successfully in 1ms
// 插入 DATABASECHANGELOG 记录,对应 Change Set id = 0 的迁移,执行成功
2019-11-16 19:37:02.501 INFO 65543 --- [ main] liquibase.executor.jvm.JdbcExecutor : SELECT MAX(ORDEREXECUTED) FROM `lab-20-liquibase`.DATABASECHANGELOG
2019-11-16 19:37:02.503 INFO 65543 --- [ main] liquibase.executor.jvm.JdbcExecutor : INSERT INTO `lab-20-liquibase`.DATABASECHANGELOG (ID, AUTHOR, FILENAME, DATEEXECUTED, ORDEREXECUTED, MD5SUM, `DESCRIPTION`, COMMENTS, EXECTYPE, CONTEXTS, LABELS, LIQUIBASE, DEPLOYMENT_ID) VALUES ('0', 'yunai', 'classpath:/db/changelog/db.changelog-master.yaml', NOW(), 1, '8:d41d8cd98f00b204e9800998ecf8427e', 'empty', '', 'EXECUTED', NULL, NULL, '3.6.3', '3645022498')

// 释放锁
2019-11-16 19:37:02.506 INFO 65543 --- [ main] l.lockservice.StandardLockService : Successfully released change log lock

// 应用启动成功
2019-11-16 19:37:02.582 INFO 65543 --- [ main] c.i.s.l.d.Application : Started Application in 2.242 seconds (JVM running for 2.675)
  • 看下艿艿在日志内容上的注释。
  • 在整个日志中,我可以看到 ID = 0 的变更集合( Change Set )完成了执行。

在启动的日志中,我们看到 Liquibase 会自动创建两张表:

  • DATABASECHANGELOG 表,数据库变更日志。每一条记录,对应记录每个变更集合( Change Set ) 的执行日志。表结构如下:

    CREATE TABLE `DATABASECHANGELOG` (
    `ID` varchar(255) COLLATE utf8mb4_bin NOT NULL, -- Change Set 编号
    `AUTHOR` varchar(255) COLLATE utf8mb4_bin NOT NULL, -- 作者
    `FILENAME` varchar(255) COLLATE utf8mb4_bin NOT NULL, -- Change Log 文件路径
    `DATEEXECUTED` datetime NOT NULL, -- 执行时间
    `ORDEREXECUTED` int(11) NOT NULL, -- 执行的顺序
    `EXECTYPE` varchar(10) COLLATE utf8mb4_bin NOT NULL, -- 执行类型。枚举值有 EXECUTED/FAILED/SKIPPED/RERAN/MARK_RAN
    `MD5SUM` varchar(35) COLLATE utf8mb4_bin DEFAULT NULL, -- MD5 校验码
    `DESCRIPTION` varchar(255) COLLATE utf8mb4_bin DEFAULT NULL, -- 描述
    `COMMENTS` varchar(255) COLLATE utf8mb4_bin DEFAULT NULL, -- 备注
    `TAG` varchar(255) COLLATE utf8mb4_bin DEFAULT NULL, -- Tag 标签
    `LIQUIBASE` varchar(20) COLLATE utf8mb4_bin DEFAULT NULL, -- LIQUIBASE 版本号
    `CONTEXTS` varchar(255) COLLATE utf8mb4_bin DEFAULT NULL, -- 上下文
    `LABELS` varchar(255) COLLATE utf8mb4_bin DEFAULT NULL, -- Label 标签
    `DEPLOYMENT_ID` varchar(10) COLLATE utf8mb4_bin DEFAULT NULL -- 部署编号
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
  • DATABASECHANGELOGLOCK 表,DATABASECHANGELOG 表的锁,用于确保多个 JVM 进程,同时 Liquibase 尝试变更数据库。表结构如下:

    CREATE TABLE `DATABASECHANGELOGLOCK` (
    `ID` int(11) NOT NULL, -- 锁的编号。
    `LOCKED` bit(1) NOT NULL, -- 是否锁。1-锁,0-未锁
    `LOCKGRANTED` datetime DEFAULT NULL, -- 获得锁的时间
    `LOCKEDBY` varchar(255) COLLATE utf8mb4_bin DEFAULT NULL, -- 锁定人
    PRIMARY KEY (`ID`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
    • 目前,只有一条记录。也就是说,有且仅有一个 JVM 进程获得 DATABASECHANGELOG 表的锁成功。

此时,我们去查询下 MySQL 。如下:

mysql> SELECT * FROM DATABASECHANGELOG;
+----+--------+--------------------------------------------------+---------------------+---------------+----------+------------------------------------+-------------+----------+------+-----------+----------+--------+---------------+
| ID | AUTHOR | FILENAME | DATEEXECUTED | ORDEREXECUTED | EXECTYPE | MD5SUM | DESCRIPTION | COMMENTS | TAG | LIQUIBASE | CONTEXTS | LABELS | DEPLOYMENT_ID |
+----+--------+--------------------------------------------------+---------------------+---------------+----------+------------------------------------+-------------+----------+------+-----------+----------+--------+---------------+
| 0 | yunai | classpath:/db/changelog/db.changelog-master.yaml | 2019-11-16 19:37:02 | 1 | EXECUTED | 8:d41d8cd98f00b204e9800998ecf8427e | empty | | NULL | 3.6.3 | NULL | NULL | 3645022498 |
+----+--------+--------------------------------------------------+---------------------+---------------+----------+------------------------------------+-------------+----------+------+-----------+----------+--------+---------------+
1 row in set (0.00 sec)
# 查看到一条 `ID = 0` 的 `DATABASECHANGELOG` 记录,表示变更集合( Change Set ) `ID = 0` 完成了执行。

mysql> SELECT * FROM DATABASECHANGELOGLOCK;
+----+--------+-------------+----------+
| ID | LOCKED | LOCKGRANTED | LOCKEDBY |
+----+--------+-------------+----------+
| 1 | 0 | NULL | NULL |
+----+--------+-------------+----------+
1 row in set (0.00 sec)
# 查看到一条处于释放状态的锁记录。

再次启动项目。执行日志如下:

// 获得 DATABASECHANGELOGLOCK 数量。
2019-11-16 20:22:24.589 INFO 66473 --- [ main] liquibase.executor.jvm.JdbcExecutor : SELECT COUNT(*) FROM `lab-20-liquibase`.DATABASECHANGELOGLOCK
2019-11-16 20:22:24.594 INFO 66473 --- [ main] liquibase.executor.jvm.JdbcExecutor : SELECT COUNT(*) FROM `lab-20-liquibase`.DATABASECHANGELOGLOCK
// 查询 DATABASECHANGELOGLOCK 记录,id = 1
2019-11-16 20:22:24.595 INFO 66473 --- [ main] liquibase.executor.jvm.JdbcExecutor : SELECT `LOCKED` FROM `lab-20-liquibase`.DATABASECHANGELOGLOCK WHERE ID=1
// 成功获得到 DATABASECHANGELOGLOCK 锁
2019-11-16 20:22:24.603 INFO 66473 --- [ main] l.lockservice.StandardLockService : Successfully acquired change log lock

// 获得一条 DATABASECHANGELOG 。过滤 MD5SUM 非空且一条记录,有点奇怪,后面在细细研究。
2019-11-16 20:22:25.378 INFO 66473 --- [ main] liquibase.executor.jvm.JdbcExecutor : SELECT MD5SUM FROM `lab-20-liquibase`.DATABASECHANGELOG WHERE MD5SUM IS NOT NULL LIMIT 1
// 获得 DATABASECHANGELOG 数量。
2019-11-16 20:22:25.379 INFO 66473 --- [ main] liquibase.executor.jvm.JdbcExecutor : SELECT COUNT(*) FROM `lab-20-liquibase`.DATABASECHANGELOG
// 获得 DATABASECHANGELOG 所有记录。
2019-11-16 20:22:25.380 INFO 66473 --- [ main] l.c.StandardChangeLogHistoryService : Reading from `lab-20-liquibase`.DATABASECHANGELOG
2019-11-16 20:22:25.380 INFO 66473 --- [ main] liquibase.executor.jvm.JdbcExecutor : SELECT * FROM `lab-20-liquibase`.DATABASECHANGELOG ORDER BY DATEEXECUTED ASC, ORDEREXECUTED ASC

// 释放锁
2019-11-16 20:22:25.385 INFO 66473 --- [ main] l.lockservice.StandardLockService : Successfully released change log lock

2019-11-16 20:22:25.447 INFO 66473 --- [ main] c.i.s.l.d.Application : Started Application in 2.215 seconds (JVM running for 2.62)
  • 在整个日志中,我可以看到 ID = 0 的变更集合( Change Set )不会重复执行。

下面,我们修改 ID = 0 的变更集合( Change Set ),如下:

databaseChangeLog:
- changeSet: # 对应一个 ChangeSet 对象
id: 0 # ChangeSet 编号
author: yunai # 作者
comments: # 备注
changes:
- createTable: # 创建表,对应 CreateTableChange 对象。
tableName: users # 表名
remarkds: 用户表 # 表注释
columns: # 对应 ColumnConfig 数组
- column:
name: id # 字段名
type: int # 字段类型
autoIncrement: true # 自增
constraints: # 限制条件,对应一个 ConstraintsConfig 对象
primaryKey: true # 主键
nullable: false # 不允许空
  • 增加了 changes 配置项。
  • 我们再再再次启动项目。会报如下错误:

    Caused by: liquibase.exception.ValidationFailedException: Validation Failed:
    1 change sets check sum
    classpath:/db/changelog/db.changelog-master.yaml::0::yunai was: 8:d41d8cd98f00b204e9800998ecf8427e but is now: 8:2fb4d9484c5982a97eb8d580ba934395
    • 我们会发现,Liquibase 会给每个变更集合( Change Set ),计算出一个 MD5SUM 字段。这样,每次启动时,都会校验已经执行的变更集合( Change Set ),是否发生了改变。如果是,抛出异常。这样,保证不会因为 变更集合( Change Set ) 变更,导致出现问题。
  • 测试完成,我们还是将 ID = 0 的变更集合( Change Set ) 恢复原样。

3.5 Property-based migrations

修改 db.changelog-master.yaml 变更日志,增加 ID = 1 的变更集合( Change Set ) 。代码如下:

- changeSet: # 对应一个 ChangeSet 对象
id: 1 # ChangeSet 编号
author: yunai # 作者
comments: 初始化 users # 备注
changes: # 对应 Change 数组。Change 是一个接口,每种操作对应一种 Change 实现类
- createTable: # 创建表,对应 CreateTableChange 对象。
tableName: users # 表名
remarkds: 用户表 # 表注释
columns: # 对应 ColumnConfig 数组
- column:
name: id # 字段名
type: int # 字段类型
autoIncrement: true # 自增
constraints: # 限制条件,对应一个 ConstraintsConfig 对象
primaryKey: true # 主键
nullable: false # 不允许空
- column:
name: username
type: varchar(64)
constraints:
nullable: false
- column:
name: password
type: varchar(32)
constraints:
nullable: false
- column:
name: create_time
type: datetime
constraints:
nullable: false
- insert: # 插入记录,对应 InsertDataChange 对象。
tableName: users # 表名
columns: # 对应 ColumnConfig 数组
- column:
name: username # 字段名
value: yudaoyuanma # 值
- column:
name: password
value: password
- column:
name: create_time
value: now()
  • 在这个变更集合( Change Set )中,我们添加了两个变更( Change ),分别用于创建 users 表、插入一条 users 记录。
  • Liquibase 基于上述的配置,自动生成对应的 SQL 操作。

启动项目。执行日志如下:

// ... 省略雷同日志

// 创建 `users` 表
2019-11-16 21:21:42.317 INFO 67111 --- [ main] liquibase.executor.jvm.JdbcExecutor : CREATE TABLE `lab-20-liquibase`.users (id INT AUTO_INCREMENT NOT NULL, username VARCHAR(64) NOT NULL, password VARCHAR(32) NOT NULL, create_time datetime NOT NULL, CONSTRAINT PK_USERS PRIMARY KEY (id))
2019-11-16 21:21:42.323 INFO 67111 --- [ main] liquibase.changelog.ChangeSet : Table users created

// 插入一条 `users` 表的记录
2019-11-16 21:21:42.323 INFO 67111 --- [ main] liquibase.executor.jvm.JdbcExecutor : INSERT INTO `lab-20-liquibase`.users (username, password, create_time) VALUES ('yudaoyuanma', 'password', now())
2019-11-16 21:21:42.324 INFO 67111 --- [ main] liquibase.changelog.ChangeSet : New row inserted into users

// 输出 Change Log 文件的 Change Set 全部执行成功
2019-11-16 21:21:42.325 INFO 67111 --- [ main] liquibase.changelog.ChangeSet : ChangeSet classpath:/db/changelog/db.changelog-master.yaml::1::yunai ran successfully in 10ms
// 插入 DATABASECHANGELOG 记录,对应 Change Set id = 1 的迁移,执行成功
2019-11-16 21:21:42.326 INFO 67111 --- [ main] liquibase.executor.jvm.JdbcExecutor : SELECT MAX(ORDEREXECUTED) FROM `lab-20-liquibase`.DATABASECHANGELOG
2019-11-16 21:21:42.327 INFO 67111 --- [ main] liquibase.executor.jvm.JdbcExecutor : INSERT INTO `lab-20-liquibase`.DATABASECHANGELOG (ID, AUTHOR, FILENAME, DATEEXECUTED, ORDEREXECUTED, MD5SUM, `DESCRIPTION`, COMMENTS, EXECTYPE, CONTEXTS, LABELS, LIQUIBASE, DEPLOYMENT_ID) VALUES ('1', 'yunai', 'classpath:/db/changelog/db.changelog-master.yaml', NOW(), 3, '8:1dcffd4c4f87b02e3758bf6b4ecea471', 'createTable tableName=users; insert tableName=users', '', 'EXECUTED', NULL, NULL, '3.6.3', '3651302309')

// ... 省略雷同日志
  • 看下艿艿在日志内容上的注释。
  • 在整个日志中,我可以看到 ID = 1 的变更集合( Change Set )完成了执行。在这个过程中,我们可以看到两条分别用于创建 users 表和插入 users 记录的 SQL 。

此时,我们去查询下 MySQL 。如下:

mysql> SELECT * FROM DATABASECHANGELOG;
+----+--------+--------------------------------------------------+---------------------+---------------+----------+------------------------------------+-----------------------------------------------------+----------+------+-----------+----------+--------+---------------+
| ID | AUTHOR | FILENAME | DATEEXECUTED | ORDEREXECUTED | EXECTYPE | MD5SUM | DESCRIPTION | COMMENTS | TAG | LIQUIBASE | CONTEXTS | LABELS | DEPLOYMENT_ID |
+----+--------+--------------------------------------------------+---------------------+---------------+----------+------------------------------------+-----------------------------------------------------+----------+------+-----------+----------+--------+---------------+
| 0 | yunai | classpath:/db/changelog/db.changelog-master.yaml | 2019-11-16 21:35:09 | 1 | EXECUTED | 8:d41d8cd98f00b204e9800998ecf8427e | empty | | NULL | 3.6.3 | NULL | NULL | 3652109350 |
| 1 | yunai | classpath:/db/changelog/db.changelog-master.yaml | 2019-11-16 21:35:09 | 2 | EXECUTED | 8:1dcffd4c4f87b02e3758bf6b4ecea471 | createTable tableName=users; insert tableName=users | | NULL | 3.6.3 | NULL | NULL | 3652109350 |
+----+--------+--------------------------------------------------+---------------------+---------------+----------+------------------------------------+-----------------------------------------------------+----------+------+-----------+----------+--------+---------------+
2 rows in set (0.00 sec)
# 多查看到一条 `ID = 1` 的 `DATABASECHANGELOG` 记录,表示变更集合( Change Set ) `ID = 1` 完成了执行。并且,在描述中,我们可以看到对该更集合( Change Set )的简单说明。

mysql> SELECT * FROM DATABASECHANGELOGLOCK;
+----+--------+-------------+----------+
| ID | LOCKED | LOCKGRANTED | LOCKEDBY |
+----+--------+-------------+----------+
| 1 | 0 | NULL | NULL |
+----+--------+-------------+----------+
1 row in set (0.00 sec)
# 查看到一条处于释放状态的锁记录。

使用 Property-based migrations 方式,我们无需编写相应的 SQL 。虽然在编写上有些啰嗦,但是易读性还是不错的。

3.6 SQL-based migrations

修改 db.changelog-master.yaml 变更日志,增加 ID = 2 的变更集合( Change Set ) 。代码如下:

- changeSet: # 对应一个 ChangeSet 对象
id: 2 # ChangeSet 编号
author: yunai # 作者
comments: 初始化 users2 # 备注
changes: # 对应 Change 数组。Change 是一个接口,每种操作对应一种 Change 实现类
- sqlFile: # 使用 SQL 文件,对应 SQLFileChange 对象
encoding: utf8
path: classpath:db/changelog/sqlfile/CHAGE_SET_2_INIT_DB.sql

resources/db/changelog/sqlfile 目录下,创建 CHAGE_SET_2_INIT_DB.sql 变更文件。如下:

-- 创建用户表
CREATE TABLE `users2` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '用户编号',
`username` varchar(64) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '账号',
`password` varchar(32) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '密码',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
PRIMARY KEY (`id`),
UNIQUE KEY `idx_username` (`username`)
) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;

-- 插入一条数据
INSERT INTO `users2`(username, password, create_time) VALUES('yudaoyuanma', 'password', now());

启动项目。执行日志如下:

// ... 省略雷同日志

// 执行 CHAGE_SET_2_INIT_DB.sql 里的 SQL
2019-11-16 21:40:05.793 INFO 67368 --- [ main] liquibase.executor.jvm.JdbcExecutor : -- 创建用户表
CREATE TABLE `users2` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '用户编号',
`username` varchar(64) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '账号',
`password` varchar(32) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '密码',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
PRIMARY KEY (`id`),
UNIQUE KEY `idx_username` (`username`)
) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin
2019-11-16 21:40:05.800 INFO 67368 --- [ main] liquibase.executor.jvm.JdbcExecutor : -- 插入一条数据
INSERT INTO `users2`(username, password, create_time) VALUES('yudaoyuanma', 'password', now())
2019-11-16 21:40:05.801 INFO 67368 --- [ main] liquibase.changelog.ChangeSet : SQL in file classpath:db/changelog/sqlfile/CHAGE_SET_2_INIT_DB.sql executed

// 输出 Change Log 文件的 Change Set 全部执行成功
2019-11-16 21:40:05.802 INFO 67368 --- [ main] liquibase.changelog.ChangeSet : ChangeSet classpath:/db/changelog/db.changelog-master.yaml::2::yunai ran successfully in 13ms

// 插入 DATABASECHANGELOG 记录,对应 Change Set id = 2 的迁移,执行成功
2019-11-16 21:40:05.802 INFO 67368 --- [ main] liquibase.executor.jvm.JdbcExecutor : SELECT MAX(ORDEREXECUTED) FROM `lab-20-liquibase`.DATABASECHANGELOG
2019-11-16 21:40:05.804 INFO 67368 --- [ main] liquibase.executor.jvm.JdbcExecutor : INSERT INTO `lab-20-liquibase`.DATABASECHANGELOG (ID, AUTHOR, FILENAME, DATEEXECUTED, ORDEREXECUTED, MD5SUM, `DESCRIPTION`, COMMENTS, EXECTYPE, CONTEXTS, LABELS, LIQUIBASE, DEPLOYMENT_ID) VALUES ('2', 'yunai', 'classpath:/db/changelog/db.changelog-master.yaml', NOW(), 3, '8:c224782d41ad7fac92699ebf392c7f5e', 'sqlFile', '', 'EXECUTED', NULL, NULL, '3.6.3', '3652405782')

// ... 省略雷同日志
  • 看下艿艿在日志内容上的注释。
  • 在整个日志中,我可以看到 ID = 1 的变更集合( Change Set )完成了执行。在这个过程中,我们可以看到 CHAGE_SET_2_INIT_DB.sql 的 SQL 被执行。

此时,我们去查询下 MySQL 。如下:

mysql> SELECT * FROM DATABASECHANGELOG;
+----+--------+--------------------------------------------------+---------------------+---------------+----------+------------------------------------+-----------------------------------------------------+----------+------+-----------+----------+--------+---------------+
| ID | AUTHOR | FILENAME | DATEEXECUTED | ORDEREXECUTED | EXECTYPE | MD5SUM | DESCRIPTION | COMMENTS | TAG | LIQUIBASE | CONTEXTS | LABELS | DEPLOYMENT_ID |
+----+--------+--------------------------------------------------+---------------------+---------------+----------+------------------------------------+-----------------------------------------------------+----------+------+-----------+----------+--------+---------------+
| 0 | yunai | classpath:/db/changelog/db.changelog-master.yaml | 2019-11-16 21:35:09 | 1 | EXECUTED | 8:d41d8cd98f00b204e9800998ecf8427e | empty | | NULL | 3.6.3 | NULL | NULL | 3652109350 |
| 1 | yunai | classpath:/db/changelog/db.changelog-master.yaml | 2019-11-16 21:35:09 | 2 | EXECUTED | 8:1dcffd4c4f87b02e3758bf6b4ecea471 | createTable tableName=users; insert tableName=users | | NULL | 3.6.3 | NULL | NULL | 3652109350 |
| 2 | yunai | classpath:/db/changelog/db.changelog-master.yaml | 2019-11-16 21:40:05 | 3 | EXECUTED | 8:c224782d41ad7fac92699ebf392c7f5e | sqlFile | | NULL | 3.6.3 | NULL | NULL | 3652405782 |
+----+--------+--------------------------------------------------+---------------------+---------------+----------+------------------------------------+-----------------------------------------------------+----------+------+-----------+----------+--------+---------------+
3 rows in set (0.00 sec)
# 多查看到一条 `ID = 2` 的 `DATABASECHANGELOG` 记录,表示变更集合( Change Set ) `ID = 2` 完成了执行。并且,在描述中,我们可以看到 `sqlFile` 。

mysql> SELECT * FROM DATABASECHANGELOGLOCK;
+----+--------+-------------+----------+
| ID | LOCKED | LOCKGRANTED | LOCKEDBY |
+----+--------+-------------+----------+
| 1 | 0 | NULL | NULL |
+----+--------+-------------+----------+
1 row in set (0.00 sec)
# 查看到一条处于释放状态的锁记录。

相比 Property-based migrations 方式来说,艿艿更愿意使用 SQL-based migrations 。毕竟,我们更加熟悉 SQL 语法,交给 DBA 审计也更加方便。

3.7 Java-based migrations

cn.iocoder.springboot.lab20.databaseversioncontrol.migration 包路径下,创建 CHANGE_SET_3_FixUsername.java 类,修复 users 的用户名。代码如下:

// CHANGE_SET_3_FixUsername.java

public class CHANGE_SET_3_FixUsername implements CustomTaskChange {

private Logger logger = LoggerFactory.getLogger(getClass());

@Override
public void execute(Database database) throws CustomChangeException {
JdbcConnection connection = (JdbcConnection) database.getConnection();
try (PreparedStatement psmt = connection.prepareStatement("SELECT id, username, password, create_time FROM users")) {
try (ResultSet rs = psmt.executeQuery()) {
while (rs.next()) {
String username = rs.getString("username");
if ("yudaoyuanma".equals(username)) {
Integer id = rs.getInt("id");
// 这里,再来一刀更新操作,偷懒不写了。
logger.info("[migrate][更新 user({}) 的用户名({} => {})", id, username, "yutou");
}
}
}
} catch (Exception e) {
throw new RuntimeException(e);
}
}

@Override
public String getConfirmationMessage() {
return null;
}

@Override
public void setUp() throws SetupException {
}

@Override
public void setFileOpener(ResourceAccessor resourceAccessor) {
}

@Override
public ValidationErrors validate(Database database) {
return null;
}

}
  • 通过实现 CustomTaskChange 接口,实现 Java 自定义迁移逻辑。
  • 这里有一点比较蛋疼,在 #execute(Database database) 方法中,提供给我们的是 Database 对象,无法方便的获得 DataSource 对象,从而使用 Spring JdbcTemplate 。

修改 db.changelog-master.yaml 变更日志,增加 ID = 3 的变更集合( Change Set ) 。代码如下:

- changeSet: # 对应一个 ChangeSet 对象
id: 3 # ChangeSet 编号
author: yunai # 作者
comments: 修复 `users` 的用户名 # 备注
changes: # 对应 Change 数组。Change 是一个接口,每种操作对应一种 Change 实现类
- customChange: {class: cn.iocoder.springboot.lab20.databaseversioncontrol.migration.CHANGE_SET_3_FixUsername} # 对应 CustomTaskChange

启动项目。执行日志如下:

// ... 省略雷同日志

// CHANGE_SET_3_FixUsername 的执行日志
2019-11-16 21:56:48.782 INFO 67585 --- [ main] c.i.s.l.d.m.CHANGE_SET_3_FixUsername : [migrate][更新 user(1) 的用户名(yudaoyuanma => yutou)
2019-11-16 21:56:48.782 INFO 67585 --- [ main] liquibase.changelog.ChangeSet : null

// 插入 DATABASECHANGELOG 记录,对应 Change Set id = 3 的迁移,执行成功
2019-11-16 21:56:48.783 INFO 67585 --- [ main] liquibase.changelog.ChangeSet : ChangeSet classpath:/db/changelog/db.changelog-master.yaml::3::yunai ran successfully in 3ms
2019-11-16 21:56:48.783 INFO 67585 --- [ main] liquibase.executor.jvm.JdbcExecutor : SELECT MAX(ORDEREXECUTED) FROM `lab-20-liquibase`.DATABASECHANGELOG
2019-11-16 21:56:48.784 INFO 67585 --- [ main] liquibase.executor.jvm.JdbcExecutor : INSERT INTO `lab-20-liquibase`.DATABASECHANGELOG (ID, AUTHOR, FILENAME, DATEEXECUTED, ORDEREXECUTED, MD5SUM, `DESCRIPTION`, COMMENTS, EXECTYPE, CONTEXTS, LABELS, LIQUIBASE, DEPLOYMENT_ID) VALUES ('3', 'yunai', 'classpath:/db/changelog/db.changelog-master.yaml', NOW(), 4, '8:2595b6826984b91149063782c4cd6c29', 'customChange', '', 'EXECUTED', NULL, NULL, '3.6.3', '3653408775')

// ... 省略雷同日志
  • 看下艿艿在日志内容上的注释。
  • 在整个日志中,我可以看到 ID = 3 的变更集合( Change Set )完成了执行。在这个过程中,我们可以看不到 CHANGE_SET_3_FixUsername 的任何信息,除了我们打的 INFO 日志。所以,在使用 Java-based migrations 方式时,最好自己打下日志,方便排查。

此时,我们去查询下 MySQL 。如下:

mysql> SELECT * FROM DATABASECHANGELOG;
+----+--------+--------------------------------------------------+---------------------+---------------+----------+------------------------------------+-----------------------------------------------------+----------+------+-----------+----------+--------+---------------+
| ID | AUTHOR | FILENAME | DATEEXECUTED | ORDEREXECUTED | EXECTYPE | MD5SUM | DESCRIPTION | COMMENTS | TAG | LIQUIBASE | CONTEXTS | LABELS | DEPLOYMENT_ID |
+----+--------+--------------------------------------------------+---------------------+---------------+----------+------------------------------------+-----------------------------------------------------+----------+------+-----------+----------+--------+---------------+
| 0 | yunai | classpath:/db/changelog/db.changelog-master.yaml | 2019-11-16 21:35:09 | 1 | EXECUTED | 8:d41d8cd98f00b204e9800998ecf8427e | empty | | NULL | 3.6.3 | NULL | NULL | 3652109350 |
| 1 | yunai | classpath:/db/changelog/db.changelog-master.yaml | 2019-11-16 21:35:09 | 2 | EXECUTED | 8:1dcffd4c4f87b02e3758bf6b4ecea471 | createTable tableName=users; insert tableName=users | | NULL | 3.6.3 | NULL | NULL | 3652109350 |
| 2 | yunai | classpath:/db/changelog/db.changelog-master.yaml | 2019-11-16 21:40:05 | 3 | EXECUTED | 8:c224782d41ad7fac92699ebf392c7f5e | sqlFile | | NULL | 3.6.3 | NULL | NULL | 3652405782 |
+----+--------+--------------------------------------------------+---------------------+---------------+----------+------------------------------------+-----------------------------------------------------+----------+------+-----------+----------+--------+---------------+
3 rows in set (0.00 sec)
# 多查看到一条 `ID = 2` 的 `DATABASECHANGELOG` 记录,表示变更集合( Change Set ) `ID = 2` 完成了执行。并且,在描述中,我们可以看到 `sqlFile` 。

mysql> SELECT * FROM DATABASECHANGELOGLOCK;
+----+--------+-------------+----------+
| ID | LOCKED | LOCKGRANTED | LOCKEDBY |
+----+--------+-------------+----------+
| 1 | 0 | NULL | NULL |
+----+--------+-------------+----------+
1 row in set (0.00 sec)
# 查看到一条处于释放状态的锁记录。

3.8 其它功能

在上述的示例,我们是基于 Spring Boot 的使用方式。而 Flyway 还提供了如下方式:

😈 Liquibase 还有一些其它细节,建议抽时间,通读下 Documentation 文档。

666. 彩蛋

写着写着,突然意识到原来写了这么臭长。能看到这里的胖友,对艿艿一定是真爱。反正我相信了。

在写这篇文章的过程中,艿艿也在网上搜索 Flyway 和 Liquibase 的对比。毕竟,咱仅仅是做了这两者的入门,实际使用的情况,是否有什么最佳实践,又或者有什么坑,需要有在项目中真正在实践的人的经验分享。目前暂时只找到 《数据库迁移工具 Flyway 对比 Liquibase》 一文,作者的观点是:

两款数据库迁移工具其实定位上是差别的,一般我的倾向是小项目,整体变动不大的用 Flyway ,而大应用和企业应用用 Liquibase 更合适。

  • 不过从入门看下来,Liquibase 并未看到比 Flyway 带来多大的优势,反倒 Flyway 基于“约定大于配置”的思想,使用上更加便捷。欢迎胖友能够解答下艿艿在这块的疑惑。

关于最佳实践,目前找到比较合适的两篇,如下:

在实际项目使用时,可能还需要额外考虑一些事情,例如说:

  • 大表直接添加索引,直接导致线上故障。
  • 内部有 DBA ,数据库调整需要内部审核。

另外,项目发版时,涉及到的数据变更,不仅仅有关系数据库,可能还有 MongoDB、Redis、Elasticsearch 等等数据源的变更。特别是,可能涉及到数据修复,需要编写 Java 代码的情况。和朋友沟通了下,目前采用如下三种方法:

思路都是一致的,只是形态不同。

  • 1、创建迁移 Migration 项目,专门编写数据相关变更的脚本。然后连接各种环境,本机手动运行。
  • 2、在定时任务项目中,创建迁移 Migration Job 类,编写数据变更的脚本。然后,部署到服务器上,通过调度中心触发运行。
  • 3、在业务项目中,创建迁移 API 接口,编写数据变更的脚本。然后,部署到服务器上,通过调用该 API 接口运行。
文章目录
  1. 1. 1. 概述
  2. 2. 2. Flyway
    1. 2.1. 2.1 引入依赖
    2. 2.2. 2.2 应用配置文件
    3. 2.3. 2.3 Application
    4. 2.4. 2.4 SQL-based migrations
    5. 2.5. 2.5 Java-based migrations
    6. 2.6. 2.6 其它功能
  3. 3. 3. Liquibase
    1. 3.1. 3.1 引入依赖
    2. 3.2. 3.2 应用配置文件
    3. 3.3. 3.3 变更文件 Change Log
    4. 3.4. 3.4 Application
    5. 3.5. 3.5 Property-based migrations
    6. 3.6. 3.6 SQL-based migrations
    7. 3.7. 3.7 Java-based migrations
    8. 3.8. 3.8 其它功能
  4. 4. 666. 彩蛋