背景

对于数据库字段变更,数据库结构首次程序部署时的初始化,我们一般都是如下流程:

  1. 在生产环境下,找一台数据库
  2. 手动创建数据库
  3. 手动执行 SQL 脚本把表和初始数据创建好

在下次更新程序时的流程:

  1. 手动同步数据库表结构
  2. 手动同步需要增加或则变更的数据

上面多次出现到了 手动,这个过程不难,但是繁琐且容易出错。 为了解决这个我们可以利用自动化来解决。

解决方案

Spring boot 版本:2.4.1

Spring-boot 官方有一章是:使用更高级别的数据库迁移工具,自动配置中直接支持 Flyway and Liquibase.
这里我们使用 Flyway 来实现,官方也有他的 工作原理和流程,还有与 spring boot 集成指引,这里不细说。

Flyway 官方文档-迁移,建议看官方文档了解大体的功能,相信我,不会英文的直接网页翻译,绝对能看明白

基于 SQL 的迁移

官方文档-基于 SQL 的迁移

简单说一下:

  1. flywa 会以版本号的方式在指定位置检测是否有 sql 脚本文件
  2. 如果检测到有,则会执行该 sql 脚本文件
  3. 执行完成后,会把该次执行的记录存放在 flyway_schema_history 表中
  4. 下次程序启动,拿到 sql 脚本文件名中的版本号,与 flyway_schema_history 表中的记录进行对比,如果已经执行过则不再执行
  5. 另外:基于 SQL 文件的迁移,在该 SQL 文件执行过之后,会有一个校验和,你就不能修改该文件了,如果修改了,那么在重新启动程序的时候就会校验失败,程序启动失败

那么这里就有如下的利用思路:

  1. 程序首次运行,初始化表结构和初始数据
  2. 程序升级:执行表结构变更和数据变更脚本
    复杂的数据变更,后续使用到再增加

实际配置如下:
1:添加 flyway 依赖

  1. implementation 'org.flywaydb:flyway-core'
  2. // 没有使用这个,纯粹为了解决启动报错:java.lang.NoClassDefFoundError: liquibase/exception/ChangeLogParseException
  3. implementation 'org.liquibase:liquibase-core'

2:自动配置参数:在 application[-xx].yml

  1. spring:
  2. flyway:
  3. # 指定迁移脚本位置,这里放到 classpath 下的
  4. locations: classpath:db/migration
  5. # 是否关闭这个功能:当表不为空时,是否清理数据库
  6. clean-disabled: true
  7. enabled: true
  8. liquibase:
  9. enabled: false

3:在 classpath:db/migration 下准备你的数据迁移脚本
V1__Initial_Setup.sql

  1. -- 设置此次使用的数据库默认编码
  2. SET NAMES utf8mb4;
  3. -- 外键约束是否开启
  4. SET FOREIGN_KEY_CHECKS = 0;
  5. -- 这里执行你的 sql 脚本
  6. CREATE TABLE `compound_task` ....
  7. INSERT INTO ...
  8. SET FOREIGN_KEY_CHECKS = 1;

其他配置属性,请参考官方的说明,这里简单讲解下它的迁移脚本命名方式:
V1__Initial_Setup.sql 为例:

  1. V1 这个前缀的 V 是可以通过配置属性改变的,重要的是里面的数字
    它充当了版本的概念,每个版本只执行一次
  2. __ 后面的是你的描述,可以随意填写

image.png
当前展示的例子是 版本化迁移(Versioned Migrations), 上图是文件名的命名规范:

  • 前缀 / Prefix:V 用于版本化、U 撤销、R 可重复迁移,这些都可以自定义配置
  • 版本 / Version:带有点或下划线的版本可以根据需要分隔任意数量的部分(不适用于可重复的迁移)
  • 分隔符 / Separator:两个下划线__,这个也是可配置的
  • 说明 / Description:简短说明该脚本的描述
  • 后缀 / Suffix:.sql ,同样可配置

    基于 JAVA 的迁移

    官方文档,建议阅读下,比较简单

前面是基于 SQL 文件的迁移,在复杂的数据迁移下,比如需要总一个复杂的计算中衍生出另外一个或几个表的数据时,传统的 SQL 脚本就很难做到了。

java 的方式和 SQL 的方式类似,有一些小小的区别:

  1. java 类的命名也是以版本号命名的,但是该类需要继承 **BaseJavaMigration** 类或则实现 JavaMigration 接口实现更细致化的指定
  2. 默认不会提供校验和的功能,可以通过 getChecksum() 方法来返回校验和,但是这个没有太多的用处
  1. // 包名必须和配置文件中 spring.flyway.locations 中对应,才会被扫描到
  2. package db.migration;
  3. import org.flywaydb.core.api.migration.BaseJavaMigration;
  4. import org.flywaydb.core.api.migration.Context;
  5. import org.springframework.jdbc.core.BeanPropertyRowMapper;
  6. import org.springframework.jdbc.core.JdbcTemplate;
  7. import org.springframework.jdbc.datasource.SingleConnectionDataSource;
  8. import java.util.List;
  9. /**
  10. * v1.6 数据库迁移脚本
  11. *
  12. * @author mrcode
  13. * @date 2021/9/15 21:00
  14. */
  15. // 版本命名与 sql 文件命名一致,只不过其中的 . 号使用下划线 _ 来代替
  16. public class V1_6__update extends BaseJavaMigration {
  17. public void migrate(Context context) {
  18. // 构建 spring 的 JdbcTemplate
  19. final JdbcTemplate jdbcTemplate = new JdbcTemplate(new SingleConnectionDataSource(context.getConnection(), true));
  20. // 下面进行你的数据库脚本迁移
  21. // 查询数据
  22. final List<Account> accounts = jdbcTemplate.query("SELECT id,name FROM account WHERE is_valid_user = true AND is_deleted = false AND role_type = 2", new BeanPropertyRowMapper<>(Account.class));
  23. // 经过计算之后,插入到某些表中,或则修改数据库结构之类的操作
  24. jdbcTemplate.execute("INSERT INTO test_user (name) VALUES ('Obelix')");
  25. }
  26. }

常见问题/注意事项

在官方文档的 常见问题 中有说到一部分,这里说下目前我自己遇到的一些问题:

迁移失败后,flyway 会回滚吗?

这个要分数据库,我用的 Mysql,DDL(数据定义语句)是不支持回滚的,DML(CRUD 数据)是支持在事物中回滚的,所以:需要将 DDL 和 DML 分开迁移,这样在 DDL 迁移失败后你才会有补救的方法。

比如下面这个迁移 SQL

  1. SET NAMES utf8mb4;
  2. SET FOREIGN_KEY_CHECKS=0;
  3. ALTER TABLE single_data ADD COLUMN ibe7763 int(11) NULL;
  4. ALTER TABLE single_data ADD COLUMN ibe7851 int(11) NULL;
  5. ALTER TABLE single_data ADD COLUMN ibe7852 int(11) NULL;
  6. SET FOREIGN_KEY_CHECKS=1;

如果第一条 alter table 的语句执行成功了,第二条执行失败了(该表有这个字段了),那么这个脚本整体是失败的,这个时候你要如何做?

  1. 第一步:修改这个脚本,将第二条语句删除
  2. 第二步:手动在这个表上删除 ibe7763 字段

    1. ALTER TABLE single_data DROP COLUMN ibe7763 ;
  3. 将 flyway 中的 flyway_schema_history 脚本迁移记录中对应版本的记录删除掉,再启动项目,就会成功了

所以说:这种 DDL 语句在 Mysql 中是没有事物的,出现问题后就非常的麻烦。这也是为什么建议:将 ddl 和 dml 语句分开写迁移脚本的原因

:::warning 如果真的出现了上面那种手动更改了脚本,从而导致脚本不完全了,如果在全新的环境中部署的话,会因为手动删除了一些语句导致出现一些问题的情况下,这个时候就可以基于最新表结构等和初始化语句使用 基线迁移 的方式,跳过之前的所有脚本 :::