springboot 数据库版本升级管理常用解决方案
目录
一、前言
1.1 单独执行初始化sql
1.2 程序自动执行
二、数据库版本升级管理问题
三、spring 框架sql自动管理机制
3.1 jdbcTemplate 方式
3.1.1 创建数据库
3.1.2 创建 springboot 工程
3.1.3 初始化sql脚本
3.1.4 核心配置类
3.1.5 执行sql初始化
3.2 配置文件方式
四、Flyway 实现数据库版本自动管理
4.1 Flyway 简介
4.2 Flyway 执行流程与原理
4.3 Flyway sql脚本命名规范
4.3.1 sql命名参考示例
4.3.2 sql命名规范补充说明
4.4 SpringBoot集成Flyway
4.4.1 引入基础依赖
4.4.2 配置Flyway参数
4.4.3 准备升级sql脚本
4.4.4 测试效果一:执行sql初始化建表
4.4.5 测试效果二:执行sql创建索引
4.5 使用经验与问题总结
五、Liquibase 实现数据库版本自动管理
5.1 Liquibase简介
5.2 Liquibase 优势
5.3 SpringBoot集成Liquibase
5.3.1 导入依赖
5.3.2 新建master.xml
5.3.3 创建sql文件
5.3.4 自定义Liquibase配置类
5.3.5 效果测试一
5.3.6 效果测试二
5.4 Flyway与Liquibase的对比
六、自研数据库版本管理SDK工具
6.1 实现思路
6.2 核心代码
七、写在文末
一、前言
当springboot微服务项目完成从开发到测试全流程后,通常来说,最终交付产物是一个完整的安装包。在交付产物中一个重要文件就是sql脚本,即项目在部署完成之后运行需要的数据库表。简而言之就是:如何在完成项目安装后,减少或降低sql执行的效率?或者说:如何在项目实施时减少人工对sql相关的干预呢?结合实践经验,有下面几种思路提供参考:
1.1 单独执行初始化sql
sql脚本统一放在一个外部文件中,在项目安装部署之前先执行初始化sql,然后再安装项目。
1)好处:sql文件单独维护,与项目代码分开,可以避免文件管理上的麻烦;
2)缺点:实施时需要较多的人力干预,如果sql与工程代码是多人维护,升级中遇到问题,需要多方协调人力参与排查问题,而且这种方式难以做到自动化。
1.2 程序自动执行
顾名思义,这种方式依托程序自动完成sql的执行,即项目安装之前通过程序就可以完成所需sql表的初始化,相比上面单独执行sql的方式来说,尽量减少了人工的干预,即人力成本的浪费。
二、数据库版本升级管理问题
在正式开始讨论如何使用程序自动管理sql之前,先来聊聊为什么会涉及数据库版本的管理问题,来看下面这张图,
从这张图可还原下相对真实的开发场景:
-
V1版本,初始化数据表,给部分表创建索引,同时初始化部分数据;
-
V1版本的业务开发完成并上线;
-
V2版本新的需求来了,需要继续追加新表,之前的某个表需要补充字段,同时某个表需要批量修改数据以适配新的业务;
-
V3...
三、spring 框架sql自动管理机制
spring框架可以说早已成为很多微服务项目的标配,所以很多外部组件都提供了与spring框架整合的SDK或独立jar包,这里提供两种常用的外部jar,可以比较容易实现sql脚本的自动化执行,分别是jdbc-template与spring自带的配置方式,下面分别进行说明;
3.1 jdbcTemplate 方式
下面演示使用jdbcTemplate完成一个sql脚本自动执行的过程。
3.1.1 创建数据库
创建一个数据库,名为:bank(可自定);
create database bank;
3.1.2 创建 springboot 工程
创建一个spring boot工程,目录结构如下:
3.1.3 初始化sql脚本
在resource目录下,创建一个db的文件夹,里面有一个init.sql文件,初始化sql脚本如下:
-- 初始化建表sql
use bank;-- tb_user表建表
CREATE TABLE `tb_user` (`id` int(12) NOT NULL,`user_name` varchar(32) DEFAULT NULL,`password` varchar(64) DEFAULT NULL,`phone` varchar(32) DEFAULT NULL,`address` varchar(64) DEFAULT NULL,PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;-- tb_role表建表
CREATE TABLE `tb_role` (`id` int(12) NOT NULL,`role_name` varchar(32) DEFAULT NULL,`role_type` varchar(64) DEFAULT NULL,`code` varchar(32) DEFAULT NULL,PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;-- tb_role 表增加索引
alter tb_role add index idx_code(code);
3.1.4 核心配置类
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.core.io.Resource;
import org.springframework.jdbc.datasource.init.DataSourceInitializer;
import org.springframework.jdbc.datasource.init.DatabasePopulator;
import org.springframework.jdbc.datasource.init.ResourceDatabasePopulator;
import org.springframework.stereotype.Component;import javax.sql.DataSource;/*** 成功执行*/
@Component
@Slf4j
public class DatabaseInitializer implements InitializingBean {@Value("classpath:db/init.sql")private Resource dataScript;@Beanpublic DataSourceInitializer dataSourceInitializer(final DataSource dataSource) {final DataSourceInitializer initializer = new DataSourceInitializer();// 设置数据源initializer.setDataSource(dataSource);initializer.setDatabasePopulator(databasePopulator());return initializer;}private DatabasePopulator databasePopulator() {final ResourceDatabasePopulator populator = new ResourceDatabasePopulator();populator.addScripts(dataScript);log.info("sql脚本初始化完成");return populator;}@Overridepublic void afterPropertiesSet() throws Exception {}}
3.1.5 执行sql初始化
全部的配置到这里就完成了,启动工程即可执行,由于这种方式是实现了InitializingBean,即在容器完成bean的初始化过程中执行,所以伴随容器的启动即可完成sql的自动执行,执行之后可看到如下效果;
启动完成后,数据表也创建出来了
3.2 配置文件方式
也可以直接通过配置文件的方式,按照约定配置好相关的属性之后即可完成sql脚本的自动初始化,参考配置如下,相关的配置说明见备注;
spring:datasource:type: com.alibaba.druid.pool.DruidDataSourceurl: jdbc:mysql://IP:3306/bank?createDatabaseIfNotExist=true&useUnicode=true&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8driver-class-name: com.mysql.jdbc.Driverusername: rootpassword: root#初始化sql建表相关配置属性 ========================schema: classpath:db/init.sql #初始化sql语句,DDL相关data: classpath:db/data.sql # 追加数据,添加字段,索引等separator: ";" # sql语句中的结束分隔符,通常为;continue-on-error: false #执行错误时是否继续initialization-mode: always #是否每次启动工程时都执行platform: mysql #使用哪种数据库sqlScriptEncoding: utf-8 #编码
缺点:上述这种方式有一个明显的缺点就是第一次初始化之后,再次启动时如果配置initialization-mode仍然开启的话将会报错,只能手动修改配置。
四、Flyway 实现数据库版本自动管理
4.1 Flyway 简介
Flyway是一个款数据库版本管理工具,程序通过集成Flyway可实现启动项目时自动执行项目中数据库迭代升级所需Sql语句,从而减少sql升级时人工介入与干预。
Flyway是独立于数据库的应用、管理并跟踪数据库变更的数据库版本管理工具。通俗来讲,Flyway就像Git管理代码那样,管理不同版数据库本sql脚本,从而做到数据库同步升级。
4.2 Flyway 执行流程与原理
Flyway 是如何实现自动管理不同版本的sql文件呢?下面给出其执行过程;
-
按照Flyway 的配置要求,完成相关的配置信息;
-
运行项目,会在默认的数据库下(没有指定的话就用在项目的数据库)下创建Flyway自带的数据表,用于存储Flyway运行中的数据,比如记录了本次升级的版本号,时间等;
-
对工程文件目录进行扫描,主要是需要初始化加载或运行的sql文件路径,然后Flyway执行数据迁移时将基于sql脚本版本号进行排序,按顺序执行,这个由Flyway内部的机制保障;
-
初次执行完sql脚本之后,将会在Flyway运行时数据表中记录关键的信息,比如本次升级的版本号,下次项目继续启动时将会对比sql文件以确定是否需要再次执行sql升级;
4.3 Flyway sql脚本命名规范
下图是一张权威的关于使用Flyway命名sql脚本的规范说明。
关于这张图,做如下的说明:
Prefix (前缀)
V
为版本迁移,U
为回滚迁移,R
为可重复迁移。在日常开发中,我们使用V
前缀,表示版本迁移。绝大多数情况下,我们只会使用V
前缀。
Version (版本号)
每一个迁移脚本,都需要一个对应一个唯一的版本号。而脚本的执行顺序,按照版本号的顺序。一般情况下,每次发生了sql脚本的变更,我们使用数字自增即可。
Separator (分隔符)
两个
_
,即__
,约定的一种配置,建议按这种规范执行。
Description (描述)
在下面的示例中,我们使用 init,或update,一般见名知义即可。
Suffix (后缀)
.sql ,可配置,不过一般可以不用配置,默认即为.sql。
4.3.1 sql命名参考示例
sql命名规范非常重要,涉及到开发中可能会踩坑的问题,为了更好的加深对此问题的理解,我们以几个命名文件为例加以说明;
有如下几个sql文件
V20230810__init.sql
V20230810__data.sql
V20230810.01_init.sql
V20230810.02_data.sql
V:大写
固定大写(必须大写,小写不执行),约定规范;
20230810.01
20230810是日期,后面用.01代表序号,如果前面日期相同,但是后面的序号不一样,.02比.01版本高,.03比.02版本高,Flyway 在执行sql脚本时是严格按照版本号的顺序进行;
执行顺序
Flyway 比较两个 SQL 文件的先后顺序时采用 采用左对齐原则, 缺位用 0 代替 。
举几个例子一看便知:
- 1.0.1.1 比 1.0.1 版本高;
- 1.0.10 比 1.0.9.4 版本高;
- 1.0.10 和 1.0.010 版本号一样,每个版本号部分的前导 0 会被忽略;
如下是下文两个需要执行的sql
4.3.2 sql命名规范补充说明
需重复运行的SQL,则以大写的“R”开头,后面再以两个下划线分割,接着跟文件名称,最后以.sql结尾,比如,R20230809.03index.sql;
当与V开头的sql文件在一起执行时,V开头的SQL执行优先级要比R开头的SQL优先级高
4.4 SpringBoot集成Flyway
下面介绍如何与springboot进行整合使用,实现程序的自动版本管理,需求如下:
1、V1版本,初始化建表;
2、V2版本,给表初始化一些数据,补充索引、字段等;
3、V3版本,依次类推..
4.4.1 引入基础依赖
<dependency><groupId>org.flywaydb</groupId><artifactId>flyway-core</artifactId><version>6.0.7</version></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId><version>2.3.2.RELEASE</version></dependency>
4.4.2 配置Flyway参数
在application.yml中添加如下配置,可视情况补充
spring:datasource:type: com.alibaba.druid.pool.DruidDataSourceurl: jdbc:mysql://IP:3306/bankdriver-class-name: com.mysql.jdbc.Driverusername: rootpassword: rootflyway:enabled: trueencoding: UTF-8cleanDisabled: true## 迁移sql脚本文件存放路径,默认db/migrationlocations: classpath:db/migration## 迁移sql脚本文件名称的前缀,默认VsqlMigrationPrefix: V## 迁移sql脚本文件名称的分隔符,默认2个下划线__sqlMigrationSeparator: __# 迁移sql脚本文件名称的后缀sqlMigrationSuffixes: .sqlvalidateOnMigrate: true# 设置为true,当迁移发现数据库非空且存在没有元数据的表时,自动执行基准迁移,新建schema_version表baselineOnMigrate: true
更多的配置参数,可以进入到FlywayProperties这个类中详细查看,上述配置中可结合备注进行了解,其中核心的配置包括:
- locations,即升级sql的文件位置;
- sqlMigrationSuffixes,执行sql文件的后缀名;
- 数据库连接信息必不可少,因为Flyway升级过程中需要初始化自己的表;
4.4.3 准备升级sql脚本
在resource的目录下,创建db文件目录,在这个文件目录下创建升级相关的sql文件,如下,
V20230808.01__init.sql 文件中存放的是初始化安装相关的建表sql,内容如下:
-- 初始化建表sql
use bank;-- tb_user表建表
CREATE TABLE `tb_user` (`id` int(12) NOT NULL,`user_name` varchar(32) DEFAULT NULL,`password` varchar(64) DEFAULT NULL,`phone` varchar(32) DEFAULT NULL,`address` varchar(64) DEFAULT NULL,PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;-- tb_role表建表
CREATE TABLE `tb_role` (`id` int(12) NOT NULL,`role_name` varchar(32) DEFAULT NULL,`role_type` varchar(64) DEFAULT NULL,`code` varchar(32) DEFAULT NULL,PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
V20230808.02__update.sql 文件中存放的是追加索引,追加字段相关,内容如下:
-- 初始化建表sql
use bank;-- 给tb_user表添加一个索引
alter table tb_user add index idx_name(user_name);-- 给tb_user表追加新字段
alter table tb_user add column woker varchar(32) default null;
4.4.4 测试效果一:执行sql初始化建表
直接启动工程即可自动执行,为了看到效果,可以先将V20230808.02__update.sql内容注释掉,第一次执行完成后,再执行;
第一次执行后,从日志不难看出,对应的sql脚本被执行了,尽管另一个被注释了,仍然输出了,这也就是说,只要放到被扫描的目录下就能被Flyway检测到;
启动完成后,检查数据库看到相关的表也被创建出来了
在上文中谈到Flyway是通过flyway_schema_history这个表来管理数据库版本升级的,打开这张表,可以看到记录了详细而完整的与升级sql文件相关的信息;
4.4.5 测试效果二:执行sql创建索引
放开V20230808.02__update.sql内容注释,再次启动工程执行,遗憾的是,此时启动报错了,这个错误大概是说,检测到数据库中flyway_schema_history这张表的checksum的数据与当前准备执行的sql文件版本冲突了,直接抛出错。
这也就是说其实上面的这种操作方式是有问题的,因为一开始就把V20230808.02__update.sql这个文件放在这个目录下面了,尽管里面没有要执行的sql,但是Flyway仍然会在表中创建一个checksum的数据记录这个文件的元信息,所以再次执行的时候,我们再将注释放开的时候执行会报错,解决的办法是:
- 初始化sql使用V1版本;
- 再次发生sql变更的时候新建一个V2的文件,记录变更的sql内容,放在扫描的目录下即可;
按照这个思路,再次走一遍这个流程之后,去数据库中确认下索引是否成功创建
再检查字段是否追加成功
在没有对上述两个sql文件做任何修改的情况下再次执行,可以看到再次启动工程时就不会报错了
4.5 使用经验与问题总结
这里结合实际使用经验,将实际开发中遇到的问题做一个总结,避免后续踩坑;
- V开头的sql脚本,执行后不可修改,修改会导致启动项目时Flyway检测不通过,导致项目启动失败,一旦有sql变更,建议增加版本号使用新的sql文件;
- 开发或测试阶段需修改已执行脚本,可通过删除相关flyway_schema_history表元数据后,重启项目;
- 脚本命名不规范,导致脚本未被执行,比如未使用V开头命名文件,可参阅官网或资料认真了解命名规范;
- 脚本文件名版本和描述之间没有使用双下滑线‘__’分割导致执行失败;
- 新增脚本,版本号没有大于已有版本号,导致执行失败;
- 各sql语句要以分号‘;’结尾,忘记用分号会导致sql执行失败,语法格式牢记于心;
- sql语句不满足可重复执行,新环境执行sql导致失败;
五、Liquibase 实现数据库版本自动管理
与Flyway相同的是,通过项目集成,都可以完成对sql脚本的自动化管理,但是Liquibase提供了更丰富的操作管理,配置管理,以及插件化管理。
5.1 Liquibase简介
Liquibase是一个数据库变更的版本控制工具。项目中通过Liquibase解析用户编写的Liquibase的配置文件,生成sql语句,并执行和记录(执行是根据记录确定sql语句是否曾经执行过,和配置文件里的预判断语句确定sql是否执行)。官网地址:liquibase官网地址,Liquibase开发文档:liquibase文档
5.2 Liquibase 优势
下面总结了Liquibase的在做sql版本变更时的优势:
- 可以用上下文控制sql在何时何地如何执行;
- 根据配置文件自动生成sql语句用于预览;
- 配置文件支持SQL、XML、JSON 或者 YAML;
- 可重复执行迁移,可回滚,且支持插件化配置拓展;
- 兼容14中主流数据库如oracle,mysql,pg等,支持平滑迁移;
- 版本控制按序执行,支持schmea的变更;
- 与springboot整合简单,上手简单;
- 支持schema方式的多租户(multi-tenant);
5.3 SpringBoot集成Liquibase
使用SpringBoot与Liquibase进行整合的方式有很多,网上的参考资料也比较丰富,下面演示一种最简单的方式,方便开发中直接拿来使用。
5.3.1 导入依赖
<dependency><groupId>org.liquibase</groupId><artifactId>liquibase-core</artifactId><version>3.6.3</version></dependency>
5.3.2 新建master.xml
使用该文件,用于指定数据库初始化脚本的位置,整个配置文件以及sql脚本目录结构如下
master.xml配置如下,不难看出,在xml文件中使用了changeSet 这个标签,里面配置了sql脚本相关的信息;
<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.1.xsd"><changeSet id="v1.0.0-1" author="zcy" context="main"><sqlFile path="classpath:/sql/v1.0.0/1.init.sql"/><sqlFile path="classpath:/sql/v1.0.0/2.init-data.sql"/></changeSet></databaseChangeLog>
5.3.3 创建sql文件
sql文件中包括一个初始化表相关,另一个为创建索引和追加字段,与上文相同不再单独列举
5.3.4 自定义Liquibase配置类
自定义配置类,通过该类在spring容器完成初始化时自动执行对sql的管理,这个与上文jdbcTemplate是不是很相似,原理差不多;
import liquibase.integration.spring.SpringLiquibase;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.DefaultResourceLoader;import javax.annotation.Resource;
import javax.sql.DataSource;@Slf4j
@Configuration
public class LiquibaseConfig {//指定liquibase版本@Value("${dc.version:main}")private String contexts;//是否开启liquibase@Value("${dc.liquibase.enable:true}")private Boolean enable;//liquibase用到的两张表,记录与管理升级sql相关private static final String DATABASE_CHANGE_LOG_TABLE = "lqb_changelog_demo";private static final String DATABASE_CHANGE_LOG_LOCK_TABLE = "lqb_lock_demo";@Resourceprivate DataSource dataSource;/*** liquibase bean配置声明*/@Beanpublic SpringLiquibase liquibase() {SpringLiquibase liquibase = new SpringLiquibase();// Liquibase xml 文件路径liquibase.setChangeLog("classpath:sql/master.xml");liquibase.setDataSource(dataSource);if (StringUtils.isNotBlank(contexts)) {liquibase.setContexts(contexts);}liquibase.setShouldRun(enable);liquibase.setResourceLoader(new DefaultResourceLoader());// 覆盖Liquibase changelog表名liquibase.setDatabaseChangeLogTable(DATABASE_CHANGE_LOG_TABLE);liquibase.setDatabaseChangeLogLockTable(DATABASE_CHANGE_LOG_LOCK_TABLE);return liquibase;}}
5.3.5 效果测试一
到这里主要的配置就完成了,清空bank数据库,然后启动工程;
从控制台输出的日志可以看到使用Liquibase建表的详细过程
启动完成后,检查数据库,可以看到表成功创建出来,同时用于管理版本相关的两张表也一起创建出来了;
如果重新启动工程,不会报错,也不会再次重新创建,说明可以自动进行sql数据库的版本管理了
5.3.6 效果测试二
再在xml中追加新的sql文件,即v1.0.1的目录和相关的sql脚本
<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.1.xsd"><changeSet id="v1.0.0-1" author="zcy" context="main"><sqlFile path="classpath:/sql/v1.0.0/1.init.sql"/><sqlFile path="classpath:/sql/v1.0.0/2.init-data.sql"/></changeSet><changeSet id="v1.0.1-1" author="zcy" context="main"><sqlFile path="classpath:/sql/v1.0.1/1.init.sql"/></changeSet></databaseChangeLog>
1.init.sql内容如下
-- 初始化建表sql
use bank;-- tb_depart表建表
CREATE TABLE `tb_depart` (`id` int(12) NOT NULL,`dept_name` varchar(32) DEFAULT NULL,`dept_type` varchar(64) DEFAULT NULL,`code` varchar(32) DEFAULT NULL,PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
重启工程之后,控制台再次输出了sql中创建表的信息
检查数据库,sql中的表也被创建出来了
关于使用Liquibase进行版本管理和命名规范的问题,限于篇幅原因就不再详细展开了,有兴趣的同学可参考相关资料和官方文档,网上的资料很丰富。
5.4 Flyway与Liquibase的对比
从使用上来说,Flyway与Liquibase都能与项目快速实现整合实现对数据库版本的管理,成本较小,但是两款数据库迁移工具其实定位上是差别的,经验来说如果项目规模较小,整体变动不大的话可以考虑用 Flyway ,而大应用和企业应用用 Liquibase 更合适。
不过从入门到使用,Liquibase 并未看到比 Flyway 带来多大优势,反倒 Flyway 基于“约定大于配置”的思想,在使用上更加便捷,也更容易实现标准化。
六、自研数据库版本管理SDK工具
如果觉得引入外部或第三方组件仍觉得麻烦,可以考虑自己开发一个SDK用来做数据库版本管理,
比如像SAAS这样的平台应用,可能存在非常多的微服务模块,如果每个应用都引第三方组件,一方面技术栈难以统一,另一方面不同的应用开发人员存在不同的学习成本,相反,如果自研一套SDK工具,只需要发布一下SDK,其他应用按照SDK的规范统一接入使用即可。
6.1 实现思路
自研SDK其实也没有想象中那么难,一方面可以借鉴上述几种外部组件的实现思路,另一方面也是最核心的就是要制定标准规范,即sql文件的内容必须按照一定的规范编写,否则解析的时候会出现各种问题,总结来说如下:
- 统一技术栈,建议SDK中尽可能少的依赖第三方包;
- 商讨并制定sql文件的格式规范,下文会给出一个模板规范;
- SDK对外暴露的信息尽可能少,方便其他应用引用,减少对接和学习成本;
如下是接下来示例中即将要做的一个sql文件的内容
--version 1.0.0,build 2023-08-11 ,新建用户表
CREATE TABLE `tb_user` (`id` int(12) NOT NULL,`user_name` varchar(32) DEFAULT NULL,`password` varchar(64) DEFAULT NULL,`phone` varchar(32) DEFAULT NULL,`address` varchar(64) DEFAULT NULL,PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
不难看出,在该文件中,以 --version 开头的内容就是规范的核心内容,即:
version
表示每次升级涉及到的sql的版本号,至于是1.0.0或1.0等,可以根据团队习惯来定,需要注意的是,每次发生了sql版本的变更,程序在启动之后会执行sql的变更
build
即本次升级sql的时间,也可以理解为一个标识位,即发生sql变更的时间,方便后续出现问题时进行sql的回滚或问题追溯
至于其他的信息,比如后面的备注,或者可以根据自身的需求再补充其他的内容,因为这些信息将会作为一个记录变更sql信息的表进行数据存储,说到这里,相信有经验的同学应该知道大致的实现思路了,下面用一张图来说明该方案的实现过程,这也是程序中实现的思路;
6.2 核心代码
结合上面的实现业务流程,下面给出核心的实现逻辑,最终可以将该核心实现做成一个SDK,外部服务可以直接应用,通过调用核心的入口方法,传入相关的基础参数即可,同时为了最小化依赖,这里操作数据库使用的是jdbc的方式;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.sql.*;
import java.util.LinkedList;
import java.util.List;
import java.util.regex.Pattern;/*** 操作sysversion表相关*/
public class SysVersionUtils {public static void main(String[] args){boolean sysVersionTableExist = SysVersionConfig.isSysVersionTableExist();if(!sysVersionTableExist){//如果没有创建表,执行表的创建SysVersionConfig.checkAndCreateSysVersion();}//到这一步,说明已经创建完成了,接下来开始解析sql文件进行脚本的初始化String sqlFilePath= "classpath:sql/table_init.sql";parseAndStoreVersionInfo(sqlFilePath);}public static void parseAndStoreVersionInfo(String sqlFilePat) {File databaseFile = null;try {databaseFile = ResourceUtils.getFile(sqlFilePat);} catch (FileNotFoundException e) {System.out.println("获取或解析sql文件失败");e.printStackTrace();return;}List<String> readLines = null;try {readLines = FileUtils.readLines(databaseFile, "UTF-8");} catch (IOException e) {e.printStackTrace();System.out.println("解析sql文件失败");return;}if (CollectionUtils.isEmpty(readLines)) {return;}//如果解析数据时发现不合理的数据行,是直接抛出异常不再执行?还是忽略?可以自行扩展,这里暂时跳过,记录日志List<String> versionInfoList = new LinkedList<>();for (String readLine : readLines) {if (!isValidVersionTagLine(readLine)) {continue;}versionInfoList.add(readLine);}//检查数据库中sys_version数据是否存在Connection connection = JdbcUtils.getConnection();VersionRecord dbVersionRecord = getDbVersionRecord(connection);if (dbVersionRecord == null) {System.out.println("初次执行sql变更");String upgradeSql = getSqlTextFromLines(readLines);System.out.println("to execute sql : " + upgradeSql);//解析得到最后一行的version数据String lastVersionLine = ((LinkedList<String>) versionInfoList).getLast();TemLineRecord temLineRecord = ParseUtils.getFromLine(lastVersionLine);System.out.println("本次变更的sql版本号信息" + temLineRecord.getVersionNum() + ":" + temLineRecord.getBuildDate());//TODO 执行sql建表相关的创建initSysVersion(connection,"app",temLineRecord.getVersionNum(),temLineRecord.getBuildDate());//执行sql升级doUpgradeSql(connection,upgradeSql);System.out.println("完成sql的初始化执行");return;}/*** FIXME 1、如果sys_version已经有数据了,说明之前做过升级了,只需要解析最新的版本变更;* FIXME 2、可以重复执行,如果没有版本变更,直接忽略,不用解析*///拿到最后一行的version信息,下面使用String lastVersionLine = ((LinkedList<String>) versionInfoList).getLast();TemLineRecord lineRecord = ParseUtils.getFromLine(lastVersionLine);if(lineRecord.getBuildDate().equals(dbVersionRecord.getBuildDate()) &&lineRecord.getVersionNum().equals(dbVersionRecord.getVersionNum())){System.out.println("已经是最新版本号,无需升级");return;}//从哪一行开始的数据才是本次需要执行的sql呢?那就是:数据库中的两个字段和 --version 以及时间正好能够对的上的那一行开始getTargetSqlLines(readLines, versionInfoList, dbVersionRecord);String sqlText = getSqlTextFromLines(readLines);System.out.println("待执行的sql :" + sqlText);//执行本次sql的更新doUpgradeSql(connection,sqlText);//升级版本号updateSysVersion(lineRecord,dbVersionRecord.getId());}private static void getTargetSqlLines(List<String> readLines, List<String> versionInfoList, VersionRecord dbVersionRecord) {String targetLine = null;int lineNum = 0;for (int i = 0; i < versionInfoList.size() - 1; i++) {if (versionInfoList.get(i).contains(dbVersionRecord.getVersionNum()) && versionInfoList.get(i).contains(dbVersionRecord.getBuildDate())) {lineNum = i;break;}}targetLine = versionInfoList.get(lineNum + 1);int targetNum = 0;for (int i = 0; i < readLines.size(); i++) {if (readLines.get(i).equalsIgnoreCase(targetLine)) {//找到这一行的行号targetNum = i;break;}}//接下来移除targetNum之前的所有元素readLines.subList(0, targetNum+1).clear();}/*** 将读取到的数据行组装成sql* @param allLines* @return*/public static String getSqlTextFromLines(List<String> allLines){if(CollectionUtils.isEmpty(allLines)){System.out.println("数据行为空");return null;}StringBuilder stringBuilder = new StringBuilder();for(String text : allLines){if(text.startsWith("--")){continue;}else if(StringUtils.isEmpty(text)){continue;}stringBuilder.append(text);stringBuilder.append("\n");}return stringBuilder.toString();}/*** 更新版本号* @param lineRecord* @param id*/public static void updateSysVersion(TemLineRecord lineRecord,String id){Connection connection = JdbcUtils.getConnection();PreparedStatement ps=null;String sql="UPDATE sys_version SET BUILD_DATE=?, VERSION_NUM= ? WHERE id=?";try {ps=connection.prepareStatement(sql);ps.setString(1, lineRecord.getBuildDate());ps.setString(2, lineRecord.getVersionNum());ps.setString(3, id);ps.execute();} catch (SQLException e) {e.printStackTrace();}finally {try {connection.close();ps.close();} catch (SQLException e) {e.printStackTrace();}}}/*** 初次新增版本号* @param connection* @param id* @param versionNum* @param dateStr*/public static void initSysVersion(Connection connection,String id,String versionNum,String dateStr){PreparedStatement ps=null;String sql = "insert into sys_version values('" +id+"','"+versionNum+"','"+dateStr+"')";try {ps = connection.prepareStatement(sql);ps.execute(sql);System.out.println("初始化插入sys_version版本号成功");} catch (SQLException e) {e.printStackTrace();System.out.println("初始化插入sys_version版本号失败");}}/*** 执行升级sql* @param connection* @param sql* @throws SQLException*/private static void doUpgradeSql(Connection connection, String sql) {if(StringUtils.isEmpty(sql)){System.out.println("带升级的sql不存在");return;}Statement statement = null;try {statement = connection.createStatement();String[] sqlCommands = sql.split(";");for (String sqlCommand : sqlCommands) {if (!sqlCommand.trim().isEmpty()) {try {statement.executeUpdate(sqlCommand);} catch (SQLException e) {e.printStackTrace();System.out.println("执行升级sql失败");}}}} catch (SQLException e) {e.printStackTrace();}finally {try {statement.close();} catch (SQLException e) {e.printStackTrace();}}}/*** 获取数据库的最新版本号* @param connection* @return*/public static VersionRecord getDbVersionRecord(Connection connection) {String sql = "select * from sys_version";PreparedStatement ps = null;ResultSet rs = null;VersionRecord versionRecord = null;try {ps = connection.prepareStatement(sql);rs = ps.executeQuery();if (rs.next()) {versionRecord = new VersionRecord();versionRecord.setId(rs.getString("id"));versionRecord.setBuildDate(rs.getString("BUILD_DATE"));versionRecord.setVersionNum(rs.getString("VERSION_NUM"));}} catch (SQLException e) {e.printStackTrace();System.out.println("获取最新版本号失败");}return versionRecord;}private static Pattern oldVersionTagPattern = Pattern.compile("^--version\\s[0-9\\.]+,build\\s.*");public static boolean isValidVersionTagLine(String tagLine) {return oldVersionTagPattern.matcher(tagLine).matches();}}
关于此处的代码,如果拿来生产实现,还有一些细节点值得推敲和完善,比如日志的输出,参数的健壮性处理上,毕竟是SDK,需要考虑的更加细致一点。接下来测试下效果吧,在resources目录下有如下待执行的sql文件;
第一次执行时version表不存在,结合控制台日志,可以看到表创建成功
第二次执行,在sql中追加如下内容
--version 1.0.0,build 2023-08-13 ,创建索引
alter table tb_user add index idx_name(user_name);
然后再次执行
执行完成后,在数据库确认下索引是否创建成功
七、写在文末
本篇通过较大的篇幅详细总结了几种常用的实现数据库版本自动管理的方式,每一种都有合适的应用场景,希望对看到的小伙伴们有用,本篇到此结束,感谢观看!
相关文章:

springboot 数据库版本升级管理常用解决方案
目录 一、前言 1.1 单独执行初始化sql 1.2 程序自动执行 二、数据库版本升级管理问题 三、spring 框架sql自动管理机制 3.1 jdbcTemplate 方式 3.1.1 创建数据库 3.1.2 创建 springboot 工程 3.1.3 初始化sql脚本 3.1.4 核心配置类 3.1.5 执行sql初始化 3.2 配置文…...
78. 子集
题目描述 给你一个整数数组 nums ,数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)。 解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。 示例 1: 输入:nums [1,2,3] 输出:[[],[1],[2…...

Mask RCNN网络结构以及整体流程的详细解读
文章目录 1、概述2、Backbone3、RPN网络3.1、anchor的生成3.2、anchor的标注/分配3.3、分类预测和bbox回归3.4、NMS生成最终的anchor 4、ROI Head4.1、ROI Align4.2、cls head和bbox head4.3、mask head 1、概述 Mask RCNN是在Faster RCNN的基础上增加了mask head用于实例分割…...

Android Framework底层原理之WMS的启动流程
一 概述 今天,我们介绍 WindowManagerService(后续简称 WMS)的启动流程,WMS 是 Android 系统中,负责窗口显示的的服务。在 Android 中它也起着承上启下的作用。 如下图,就是《深入理解 Android》书籍中的…...

Leaflet入门,Leaflet加载xyz地图,以vue-leaflet插件加载高德地图为例
前言 本章介绍Leaflet使用vue2-leaflet或者vue-leaflet插件方式便捷加载xyz高德地图。 # 效果演示 vue如何使用Leaflet vue2如何使用:《Leaflet入门,如何使用vue2-leaflet实现vue2双向绑定式的使用Leaflet地图,以及初始化后拿到leaflet对象,方便调用leaflet的api》 vue3…...

【ARM Cache 系列文章 8 -- ARM DynamIQ 技术介绍
文章目录 DynamIQ 技术背景DynamIQ技术详解DynamIQ 与 big.LITTLEDynamIQ cluster 分类硬件支持 DynamIQ为什么适合人工智能? DynamIQ 技术背景 2017年3月21日下午,ARM在北京金隅喜来登酒店召开发布会,正式发布了全新的有针对人工智能及机器…...

24届近5年南京大学自动化考研院校分析
今天给大家带来的是南京大学控制考研分析 满满干货~还不快快点赞收藏 一、南京大学 学校简介 南京大学是一所历史悠久、声誉卓著的高等学府。其前身是创建于1902年的三江师范学堂,此后历经两江师范学堂、南京高等师范学校、国立东南大学、国立第四中…...
微信小程序(原生)和uniapp预览电子文件doc/pdf/ppt/excel等
微信小程序原生预览文件 function previewFile(value) {const fileExtName ${value.ext};const randFile new Date().getTime() fileExtName;uni.showLoading({title: 加载中...})wx.downloadFile({url: value.url, // 文件的本身urlfilePath: wx.env.USER_DATA_PATH / r…...

【前端 | CSS】align-items与align-content的区别
align-items 描述 CSS align-items 属性将所有直接子节点上的 align-self 值设置为一个组。align-self 属性设置项目在其包含块中在交叉轴方向上的对齐方式 align-items是针对每一个子项起作用,它的基本单位是每一个子项,在所有情况下都有效果&…...
Go语言入门
Go语言入门 简介 Go是一门由Google开发的开源编程语言,旨在提供高效、可靠和简洁的软件开发工具。Go具有静态类型、垃圾回收、并发性和高效编译的特点,适用于构建可扩展的网络服务和系统工具。本文将介绍Go语言的基础知识和常用功能,并通过…...
Python学习笔记第五十五天(Pandas CSV文件)
Python学习笔记第五十五天 Pandas CSV 文件read_csv()to_string()to_csv() 数据处理head()tail()fillna() info() 后记 Pandas CSV 文件 CSV(Comma-Separated Values,逗号分隔值,有时也称为字符分隔值,因为分隔字符也可以不是逗号…...

自然语言处理: 第七章GPT的搭建
自然语言处理: 第七章GPT的搭建 理论基础 在以transformer架构为框架的大模型遍地开花后,大模型的方向基本分成了三类分别是: decoder-only架构 , 其中以GPT系列为代表encoder-only架构,其中以BERT系列为代表encoder-decoder架构,标准的tr…...

【奶奶看了都会】2分钟学会制作最近特火的ikun幻术图
1.效果展示 最近ikun幻术图特别火啊,在网上能找到各种各样的ikun姿势图片,这些图片都是AI绘制的,能和风景完美融合在一起,今天小卷就来教大家怎么做这种图片 先看看图片效果 视频链接: 仿佛见到一位故人,…...

【深度学习】【风格迁移】Zero-shot Image-to-Image Translation
论文:https://arxiv.org/abs/2302.03027 代码:https://github.com/pix2pixzero/pix2pix-zero/tree/main 文章目录 Abstract1. Introduction相关工作3. Method Abstract 大规模文本到图像生成模型展示了它们合成多样且高质量图像的显著能力。然而&#x…...
Day 30 C++ STL 常用算法(上)
文章目录 算法概述常用遍历算法for_each——实现遍历容器函数原型示例 transform——搬运容器到另一个容器中函数原型注意示例 常用查找算法find——查找指定元素函数原型示例 find_if—— 查找符合条件的元素函数原型示例 adjacent_find——查找相邻重复元素函数原型示例 bina…...

MES系统在机器人行业生产管理种的运用
机器人的智能水平也伴随技术的迭代不断攀升。 2021年的春晚舞台上,来自全球领先工业机器人企业abb的全球首款双臂协作机器人yumi,轻松自如地表演了一出写“福”字,赢得了全国观众的赞叹。 在汽车装配领域,一台机器人可以自主完成一…...

Spark(39):Streaming DataFrame 和 Streaming DataSet 输出
目录 0. 相关文章链接 1. 输出的选项 2. 输出模式(output mode) 2.1. Append 模式(默认) 2.2. Complete 模式 2.3. Update 模式 2.4. 输出模式总结 3. 输出接收器(output sink) 3.1. file sink 3.2. kafka sink 3.2.1. 以 Streaming 方式输出数据 3.2.2. 以 batch …...

【云原生】Docker 详解(一):从虚拟机到容器
Docker 详解(一):从虚拟机到容器 1.虚拟化 要解释清楚 Docker,首先要解释清楚 容器(Container)的概念。要解释容器的话,就需要从操作系统说起。操作系统太底层,细说的话一两本书都说…...
代码随想录第48天 | 198. 打家劫舍、213. 打家劫舍II、337. 打家劫舍III
198. 打家劫舍 当前房屋偷与不偷取决于 前一个房屋和前两个房屋是否被偷了。 递归五部曲: dp[i]:考虑下标i(包括i)以内的房屋,最多可以偷窃的金额为dp[i]。决定dp[i]的因素就是第i房间偷还是不偷。 如果偷第i房间&…...

【LeetCode】按摩师
按摩师 题目描述算法分析编程代码 链接: 按摩师 题目描述 算法分析 编程代码 class Solution { public:int massage(vector<int>& nums) {int n nums.size();if(n 0) return 0;vector<int> f(n);auto g f;f[0] nums[0];for(int i 1;i<n;i){f[i] g[i…...

AI-调查研究-01-正念冥想有用吗?对健康的影响及科学指南
点一下关注吧!!!非常感谢!!持续更新!!! 🚀 AI篇持续更新中!(长期更新) 目前2025年06月05日更新到: AI炼丹日志-28 - Aud…...

Unity3D中Gfx.WaitForPresent优化方案
前言 在Unity中,Gfx.WaitForPresent占用CPU过高通常表示主线程在等待GPU完成渲染(即CPU被阻塞),这表明存在GPU瓶颈或垂直同步/帧率设置问题。以下是系统的优化方案: 对惹,这里有一个游戏开发交流小组&…...

Vue3 + Element Plus + TypeScript中el-transfer穿梭框组件使用详解及示例
使用详解 Element Plus 的 el-transfer 组件是一个强大的穿梭框组件,常用于在两个集合之间进行数据转移,如权限分配、数据选择等场景。下面我将详细介绍其用法并提供一个完整示例。 核心特性与用法 基本属性 v-model:绑定右侧列表的值&…...

UE5 学习系列(三)创建和移动物体
这篇博客是该系列的第三篇,是在之前两篇博客的基础上展开,主要介绍如何在操作界面中创建和拖动物体,这篇博客跟随的视频链接如下: B 站视频:s03-创建和移动物体 如果你不打算开之前的博客并且对UE5 比较熟的话按照以…...

(转)什么是DockerCompose?它有什么作用?
一、什么是DockerCompose? DockerCompose可以基于Compose文件帮我们快速的部署分布式应用,而无需手动一个个创建和运行容器。 Compose文件是一个文本文件,通过指令定义集群中的每个容器如何运行。 DockerCompose就是把DockerFile转换成指令去运行。 …...
浅谈不同二分算法的查找情况
二分算法原理比较简单,但是实际的算法模板却有很多,这一切都源于二分查找问题中的复杂情况和二分算法的边界处理,以下是博主对一些二分算法查找的情况分析。 需要说明的是,以下二分算法都是基于有序序列为升序有序的情况…...

学习STC51单片机32(芯片为STC89C52RCRC)OLED显示屏2
每日一言 今天的每一份坚持,都是在为未来积攒底气。 案例:OLED显示一个A 这边观察到一个点,怎么雪花了就是都是乱七八糟的占满了屏幕。。 解释 : 如果代码里信号切换太快(比如 SDA 刚变,SCL 立刻变&#…...

Unsafe Fileupload篇补充-木马的详细教程与木马分享(中国蚁剑方式)
在之前的皮卡丘靶场第九期Unsafe Fileupload篇中我们学习了木马的原理并且学了一个简单的木马文件 本期内容是为了更好的为大家解释木马(服务器方面的)的原理,连接,以及各种木马及连接工具的分享 文件木马:https://w…...
在QWebEngineView上实现鼠标、触摸等事件捕获的解决方案
这个问题我看其他博主也写了,要么要会员、要么写的乱七八糟。这里我整理一下,把问题说清楚并且给出代码,拿去用就行,照着葫芦画瓢。 问题 在继承QWebEngineView后,重写mousePressEvent或event函数无法捕获鼠标按下事…...

Kafka入门-生产者
生产者 生产者发送流程: 延迟时间为0ms时,也就意味着每当有数据就会直接发送 异步发送API 异步发送和同步发送的不同在于:异步发送不需要等待结果,同步发送必须等待结果才能进行下一步发送。 普通异步发送 首先导入所需的k…...