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

  • 1. 概述
  • 2. Flyway
  • 3. Liquibase
  • 666. 彩蛋

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

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

图片

1. 概述

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

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

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

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

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

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

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

  • Flyway
  • Liquibase
  • dbdeploy
  • SQL Source Control
  • dbv
  • MyBatis Migrations

在 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 migrations 和 Java-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(11NOT NULL-- 安装顺序,从 1 开始递增。
  `version` varchar(50COLLATE utf8mb4_bin DEFAULT NULL-- 版本号
  `description` varchar(200COLLATE utf8mb4_bin NOT NULL-- 迁移脚本描述
  `type` varchar(20COLLATE utf8mb4_bin NOT NULL-- 脚本类型,目前有 SQL 和 Java 。
  `script` varchar(1000COLLATE utf8mb4_bin NOT NULL-- 脚本地址
  `checksum` int(11DEFAULT NULL-- 脚本校验码。避免已经执行的脚本,被人变更了。
  `installed_by` varchar(100COLLATE utf8mb4_bin NOT NULL-- 执行脚本的数据库用户
  `installed_on` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP-- 安装时间
  `execution_time` int(11NOT NULL-- 执行时长,单位毫秒
  `success` tinyint(1NOT 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(11NOT NULL AUTO_INCREMENT COMMENT '用户编号',
  `username` varchar(64COLLATE utf8mb4_bin DEFAULT NULL COMMENT '账号',
  `password` varchar(32COLLATE 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 proce***ow(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 Callbacks 和 Java Callbacks 两种回调方式,让我们在 Flyway 的执行过程中,可以实现自定义的拓展。

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

  • API (Java / Android) 直接使用 Java 原生的 API 。
  • Command-line tool 命令行工具
  • Maven 插件
  • Gralde 插件
  • Community Plugins and Integrations ,社区提供了 Ant、Intellij IDEA、Jenkins 等插件。

更多相关文章

  1. 太强了!这款轻量级的数据库中间件完美解决了Spring Boot 中分库分
  2. 如何在java脚本中获取节点内部文本?
  3. 如何从Python脚本向jQuery发送JSON对象?
  4. 执行Django数据库值的计算:视图。py或Javascript模板吗?
  5. 导入地址簿联系人,存储在数组中并保存到数据库
  6. Ajax调用php脚本返回404错误
  7. 使用jquery从mysql数据库加载数据
  8. 使用jquery的$ .ajax来获取node.js脚本的输出,而不是它的代码
  9. 基于JQuery+JSP的无数据库无刷新多人在线聊天室

随机推荐

  1. Android 开机画面
  2. android监听软键盘退格(删除)事件
  3. Android 图片倒影和setXfermode
  4. Android中TextView跑马灯没效果解决
  5. ANDROID 单元测试
  6. Android 抖动效果
  7. Android更新Ui的几种方法和见解
  8. launcher学习
  9. 【Android】常见布局xml参数详解
  10. [android]EditText的一些设置