背景
对于数据库字段变更,数据库结构首次程序部署时的初始化,我们一般都是如下流程:
- 在生产环境下,找一台数据库
- 手动创建数据库
- 手动执行 SQL 脚本把表和初始数据创建好
在下次更新程序时的流程:
- 手动同步数据库表结构
- 手动同步需要增加或则变更的数据
上面多次出现到了 手动,这个过程不难,但是繁琐且容易出错。 为了解决这个我们可以利用自动化来解决。
解决方案
Spring boot 版本:2.4.1
Spring-boot 官方有一章是:使用更高级别的数据库迁移工具,自动配置中直接支持 Flyway and Liquibase.
这里我们使用 Flyway 来实现,官方也有他的 工作原理和流程,还有与 spring boot 集成指引,这里不细说。
Flyway 官方文档-迁移,建议看官方文档了解大体的功能,相信我,不会英文的直接网页翻译,绝对能看明白
基于 SQL 的迁移
简单说一下:
- flywa 会以版本号的方式在指定位置检测是否有 sql 脚本文件
- 如果检测到有,则会执行该 sql 脚本文件
- 执行完成后,会把该次执行的记录存放在 flyway_schema_history 表中
- 下次程序启动,拿到 sql 脚本文件名中的版本号,与 flyway_schema_history 表中的记录进行对比,如果已经执行过则不再执行
- 另外:基于 SQL 文件的迁移,在该 SQL 文件执行过之后,会有一个校验和,你就不能修改该文件了,如果修改了,那么在重新启动程序的时候就会校验失败,程序启动失败
那么这里就有如下的利用思路:
- 程序首次运行,初始化表结构和初始数据
- 程序升级:执行表结构变更和数据变更脚本
复杂的数据变更,后续使用到再增加
实际配置如下:
1:添加 flyway 依赖
implementation 'org.flywaydb:flyway-core'
// 没有使用这个,纯粹为了解决启动报错:java.lang.NoClassDefFoundError: liquibase/exception/ChangeLogParseException
implementation 'org.liquibase:liquibase-core'
2:自动配置参数:在 application[-xx].yml
spring:
flyway:
# 指定迁移脚本位置,这里放到 classpath 下的
locations: classpath:db/migration
# 是否关闭这个功能:当表不为空时,是否清理数据库
clean-disabled: true
enabled: true
liquibase:
enabled: false
3:在 classpath:db/migration
下准备你的数据迁移脚本
V1__Initial_Setup.sql
-- 设置此次使用的数据库默认编码
SET NAMES utf8mb4;
-- 外键约束是否开启
SET FOREIGN_KEY_CHECKS = 0;
-- 这里执行你的 sql 脚本
CREATE TABLE `compound_task` ....
INSERT INTO ...
SET FOREIGN_KEY_CHECKS = 1;
其他配置属性,请参考官方的说明,这里简单讲解下它的迁移脚本命名方式:
以 V1__Initial_Setup.sql
为例:
V1
这个前缀的V
是可以通过配置属性改变的,重要的是里面的数字
它充当了版本的概念,每个版本只执行一次__
后面的是你的描述,可以随意填写
当前展示的例子是 版本化迁移(Versioned Migrations), 上图是文件名的命名规范:
- 前缀 / Prefix:V 用于版本化、U 撤销、R 可重复迁移,这些都可以自定义配置
- 版本 / Version:带有点或下划线的版本可以根据需要分隔任意数量的部分(不适用于可重复的迁移)
- 分隔符 / Separator:两个下划线
__
,这个也是可配置的 - 说明 / Description:简短说明该脚本的描述
- 后缀 / Suffix:
.sql
,同样可配置基于 JAVA 的迁移
官方文档,建议阅读下,比较简单
前面是基于 SQL 文件的迁移,在复杂的数据迁移下,比如需要总一个复杂的计算中衍生出另外一个或几个表的数据时,传统的 SQL 脚本就很难做到了。
java 的方式和 SQL 的方式类似,有一些小小的区别:
- java 类的命名也是以版本号命名的,但是该类需要继承
**BaseJavaMigration**
类或则实现JavaMigration
接口实现更细致化的指定 - 默认不会提供校验和的功能,可以通过
getChecksum()
方法来返回校验和,但是这个没有太多的用处
// 包名必须和配置文件中 spring.flyway.locations 中对应,才会被扫描到
package db.migration;
import org.flywaydb.core.api.migration.BaseJavaMigration;
import org.flywaydb.core.api.migration.Context;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.SingleConnectionDataSource;
import java.util.List;
/**
* v1.6 数据库迁移脚本
*
* @author mrcode
* @date 2021/9/15 21:00
*/
// 版本命名与 sql 文件命名一致,只不过其中的 . 号使用下划线 _ 来代替
public class V1_6__update extends BaseJavaMigration {
public void migrate(Context context) {
// 构建 spring 的 JdbcTemplate
final JdbcTemplate jdbcTemplate = new JdbcTemplate(new SingleConnectionDataSource(context.getConnection(), true));
// 下面进行你的数据库脚本迁移
// 查询数据
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));
// 经过计算之后,插入到某些表中,或则修改数据库结构之类的操作
jdbcTemplate.execute("INSERT INTO test_user (name) VALUES ('Obelix')");
}
}
常见问题/注意事项
在官方文档的 常见问题 中有说到一部分,这里说下目前我自己遇到的一些问题:
迁移失败后,flyway 会回滚吗?
这个要分数据库,我用的 Mysql,DDL(数据定义语句)是不支持回滚的,DML(CRUD 数据)是支持在事物中回滚的,所以:需要将 DDL 和 DML 分开迁移,这样在 DDL 迁移失败后你才会有补救的方法。
比如下面这个迁移 SQL
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS=0;
ALTER TABLE single_data ADD COLUMN ibe7763 int(11) NULL;
ALTER TABLE single_data ADD COLUMN ibe7851 int(11) NULL;
ALTER TABLE single_data ADD COLUMN ibe7852 int(11) NULL;
SET FOREIGN_KEY_CHECKS=1;
如果第一条 alter table 的语句执行成功了,第二条执行失败了(该表有这个字段了),那么这个脚本整体是失败的,这个时候你要如何做?
- 第一步:修改这个脚本,将第二条语句删除
第二步:手动在这个表上删除 ibe7763 字段
ALTER TABLE single_data DROP COLUMN ibe7763 ;
将 flyway 中的 flyway_schema_history 脚本迁移记录中对应版本的记录删除掉,再启动项目,就会成功了
所以说:这种 DDL 语句在 Mysql 中是没有事物的,出现问题后就非常的麻烦。这也是为什么建议:将 ddl 和 dml 语句分开写迁移脚本的原因
:::warning 如果真的出现了上面那种手动更改了脚本,从而导致脚本不完全了,如果在全新的环境中部署的话,会因为手动删除了一些语句导致出现一些问题的情况下,这个时候就可以基于最新表结构等和初始化语句使用 基线迁移 的方式,跳过之前的所有脚本 :::