自动化同步多服务器数据库表结构

当项目每次进行版本升级的时候,如果在这次迭代中涉及表结构变更,需要将不同的生产环境下,都需要同步表结构的DDL语句,比较麻烦,而且还有可能忘记同步脚本,导致生产环境报错....

该方案采用SpringBoot+Mybatis/MybatisPlus框架,完成在项目启动时,自动化执行sql脚本,并且同时支持版本号【如果当前版本号高于该sql文件,则不执行】。

1、先创建一张表,专门用来记录已经同步过的sql脚本文件名、对应的版本号。

CREATE TABLE `hd_version` (`id` varchar(64) NOT NULL,`version` varchar(64) DEFAULT NULL COMMENT '版本号',`created` datetime DEFAULT NULL COMMENT '创建时间',`remark` varchar(500) DEFAULT NULL COMMENT '备注',PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='数据版本';
import java.util.Date;
import lombok.Data;
@Data
public class HdVersionEntity {/*** 主键id*/private String id;/*** 版本号(一般是文件名去掉文件后缀)*/private String version;/*** 文件名*/private String remark;/*** 创建时间*/private Date created;
}
import lombok.Data;@Data
public class SchemaData {/*** 版本号*/public String version;/*** 文件名*/public String fileName;public SchemaData(String version, String fileName) {this.version = version;this.fileName = fileName;}
}

 2、接着编写dao层

import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Update;@Mapper
public interface HdCommonDao  {/*** 查询表中是否存在当前版本号* @param version* @return*/int selectVersion(@Param("version") String version);/*** 插入版本* @param entity* @return*/int insertVersion(HdVersionEntity entity);/*** 执行sql,可以是DML、DDL* @param sql*/@Update("${sql}")void updateSql(@Param("sql") String sql);
}

3、以及对应的Mapper文件

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"><!--这里文件空间命名改成自己路径下的-->
<mapper namespace="com.xxx.DatabaseAutoFill.HdCommonDao"><select id="selectVersion" resultType="int">selecT count(1) from hd_versionwhere version = #{version}</select><select id="selectTableExist" resultType="int">select count(*) count  from information_schema.TABLES where TABLE_NAME = #{tableName} and  table_schema = (select database())</select><insert id="insertVersion">insert into hd_version(id,version, remark, created) values (uuid(),#{version}, #{remark}, #{created})</insert></mapper>

4、 编写实现类

注意,这里是将整段逻辑放在ApplicationRunner接口下执行,即当Spring容器加载完之后,会立即执行该方法。

@Order(1)
@Component
@Slf4j
public class HdSchemaExecutor implements ApplicationRunner {@AutowiredHdCommonDao hdCommonDao;// 数据库脚本文件列表private static final String PREFIX = "--v";@Override@Transactionalpublic void run(ApplicationArguments args) throws IOException {String basePath = "/dbVersion/MySQL.sql";InputStream inputStream = this.getClass().getResourceAsStream(basePath);String sqlScript = IoUtil.readUtf8(inputStream);assert inputStream != null;inputStream.close();/*** 一次至多只会执行一个版本,其实我们可以拿到所有的版本并执行最后一个版本即可*/List<String> versionList = new ArrayList<>();String[] lines = sqlScript.split("\n");for (String line : lines) {if(line.toLowerCase().contains(PREFIX)){versionList.add(line);}}// 得到版本号整串String latestVersion = versionList.get(versionList.size()-1);// 写入数据库的版本号前缀String version = latestVersion.substring(latestVersion.lastIndexOf("-")+1).trim().toLowerCase();int index = sqlScript.lastIndexOf(latestVersion); // 查找s2在s1中的起始位置String result = "";if (index != -1) {// 截取s2在s1中结束位置之后的部分result = sqlScript.substring(index + latestVersion.length());} else {log.info("current version exception:{}",version);LogUtil.info(version, "current version exception");}//String[] resultList = result.split("\n");String[] resultList = result.split(";");int cnt = hdCommonDao.selectVersion(version);boolean successInsert = false;// 说明不需要写入库if(cnt ==1 )return;for (String line : resultList) {if(!line.toLowerCase().contains("drop") && !line.toLowerCase().contains("delete") && line.length() > 25 && !line.contains("--")) {//开始执行插入操作try {hdCommonDao.updateSql(line.trim());successInsert = true;log.info("version:{},start sql script:{}",version,line.trim());LogUtil.info("version, sql script:",version,line.trim());} catch (Exception e) {log.info("version:{},sql执行异常:{}",version,line.trim());LogUtil.info("sql执行异常",line.trim());}}}if(successInsert){HdVersionEntity entity = new HdVersionEntity();entity.setVersion(version);entity.setCreated(new Date());hdCommonDao.insertVersion(entity);}log.info("auto deploying sql finished...");}
}

这里主要干三件事:

读取指定路径下的文件夹中的所有文件
根据这些文件的文件名去表里查,是否插入过,没有说明需要被插入,即需要执行的sql脚本
执行sql脚本

我这里的路径是resources下的相对路径,因为我这个代码是要打包放到线上环境的,用绝对路径可能会报(FILE NOT FOUND ERROR)FNFE。 

PS

以上方法对于Spring容器加载时,没有强依赖的表,是可以通用的 (可能有点拗口)。

即,如果Spring容器启动时,如果需要依赖某张表,否则启动失败的话怎么办,还能用我们上述方法吗?

理论上是不行的,我这里将容器启动时,必须强依赖的表(Quartz框架)删去,启动时报错。
那对应这种情况,该怎么解决呢?

 其实这种框架,都会提供注解,如:

表明,在项目启动的时候,会自动完成jdbc的初始化,即如果你没有表,会先给你执行表的创建,因此不需要我们去考虑。

spring.quartz.jdbc.initialize-schema=always

Quartz也起来了。 

写在最后

由于这个工程是临时突加的,我也不好随便就测试环境的库来删删改改,因此我在本地windows上用docker部署了mysql,来测试的。以下是在windows上的docker部署mysql步骤:

docker pull mysql:8.0

在c盘用户目录下,创建conf、data、logs三个文件夹

 在conf目录下,创建my.cnf文件,里面编写如下内容。

[mysql]
#设置mysql客户端默认字符集
default-character-set=UTF8MB4
[mysqld]
#设置3306端口
port=3306
#允许最大连接数
max_connections=200
#允许连接失败的次数
max_connect_errors=10
#默认使用“mysql_native_password”插件认证
default_authentication_plugin=mysql_native_password
#服务端使用的字符集默认为8比特编码的latin1字符集
character-set-server=UTF8MB4
#开启查询缓存
explicit_defaults_for_timestamp=true
#创建新表时将使用的默认存储引擎
default-storage-engine=INNODB
#等待超时时间秒
wait_timeout=60
#交互式连接超时时间秒
interactive-timeout=600
# 对数据库表大小写不敏感设置,默认设置为小写,比较也全部设置为小写在比较
lower-case-table-names=1
# 设置默认时区
default-time_zone='+8:00'

启动容器,注意在windows下 需要把每行后面的 `\`删去,否在windows下会启动失败

 docker run --name mysql8.0 \
-v D:\docker\data\mysql8.0\config\my.cnf:/etc/mysql/my.cnf \
-v D:\docker\data\mysql8.0\data:/var/lib/mysql \
-v D:\docker\data\mysql8.0\logs:/logs -p 3306:3306 \
-e MYSQL_ROOT_PASSWORD=123456 \

-e TZ=Asia/Shanghai \
-d mysql:8.0 \
--lower-case-table-names=1

这样,理论上就能启动成功了。

分享几个常用的命令:
docker exec -it 容器名称/容器id  bash  #进入容器

docker logs 容器名称/容器id -f -n=100 查看容器最后一百行日志

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/bicheng/72683.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

DeepSeek安全:AI网络安全评估与防护策略

&#x1f345; 点击文末小卡片 &#xff0c;免费获取网络安全全套资料&#xff0c;资料在手&#xff0c;涨薪更快 本文基于现有的公开资料&#xff0c;从企业资深网络安全专家的视角&#xff0c;系统梳理DeepSeek技术在网络安全领域的潜在贡献与核心风险&#xff0c;并结合中国…

【论文笔记】Attentive Eraser

标题&#xff1a;Attentive Eraser: Unleashing Diffusion Model’s Object Removal Potential via Self-Attention Redirection Guidance Source&#xff1a;https://arxiv.org/pdf/2412.12974 收录&#xff1a;AAAI 25 作者单位&#xff1a;浙工商&#xff0c;字节&#…

【powerjob】 powerjobserver注册服务IP错误

1、问题&#xff1a;powerjobserver 4.3.6 的服务器上有多个网卡对应多个ip,示例 eth0 :IP1 &#xff0c;docker0:IP2 和worker 进行通信时 正确的应该时IP1 但是注册显示获取的确实IP2,导致 worker 通过ip2和server通信&#xff0c;网络不通&#xff0c;注册不上 2、解决方案 …

视频录像机视频通道是指什么

视频录像机的视频通道是指摄像机在监控矩阵或硬盘录像机设备上的视频输入的物理位置。 与摄像头数量关系&#xff1a;在视频监控系统中&#xff0c;有多少个摄像头就需要多少路视频通道&#xff0c;通道数量决定了视频录像机可接入摄像头的数量&#xff0c;一般硬盘录像机有4路…

面试150,数组 / 字符串

27. 移除元素 class Solution:def removeElement(self, nums: List[int], val: int) -> int:# 把不等于 val 的值移动到前面n len(nums)left 0for right in range(n):if nums[right] ! val:nums[left] nums[right]left 1return left26. 删除有序数组中的重复项 只保留 1…

【江科大STM32】TIM输入捕获模式PWMI模式测频率

一、输入捕获测频率 接线图&#xff1a; 测信号的输入引脚为PA6&#xff0c;信号从PA6进来&#xff0c;待测的PWM信号也是STM32自己生成的&#xff0c;输出引脚是PA0&#xff0c;所以接线这里直接用一根线将PA0引到PA6就可以了。 如果有信号发生器的话&#xff0c;也可以设置成…

湖仓一体化及冷、热、实时三级存储

一、湖仓一体化&#xff08;Lakehouse&#xff09; 湖仓一体化&#xff08;Lakehouse&#xff09;是数据湖&#xff08;Data Lake&#xff09;与数据仓库&#xff08;Data Warehouse&#xff09;的结合&#xff0c;旨在解决传统数据架构中数据孤岛、存储冗余、计算性能不足等问…

go切片定义和初始化

1.简介 切片是数组的一个引用&#xff0c;因此切片是引用类型&#xff0c;在进行传递时&#xff0c;遵守引用传递的机制。切片的使用和数组类似&#xff0c;遍历切片、访问切片的元素和切片的长度都一样。。切片的长度是可以变化的&#xff0c;因此切片是一个可以动态变化的数…

游戏引擎学习第138天

仓库:https://gitee.com/mrxiao_com/2d_game_3 资产&#xff1a;game_hero_test_assets_003.zip 发布 我们的目标是展示游戏运行时的完整过程&#xff0c;从像素渲染到不使用GPU的方式&#xff0c;我们自己编写了渲染器并完成了所有的工作。今天我们开始了一些新的内容&#…

毕业项目推荐:基于yolov8/yolov5/yolo11的暴力行为检测识别系统(python+卷积神经网络)

文章目录 概要一、整体资源介绍技术要点功能展示&#xff1a;功能1 支持单张图片识别功能2 支持遍历文件夹识别功能3 支持识别视频文件功能4 支持摄像头识别功能5 支持结果文件导出&#xff08;xls格式&#xff09;功能6 支持切换检测到的目标查看 二、数据集三、算法介绍1. YO…

docker中kibana启动后,通过浏览器访问,出现server is not ready yet

问题&#xff1a;当我在浏览器访问kibana时&#xff0c;浏览器给我报了server is not ready yet. 在网上试了很多方法&#xff0c;都未能解决&#xff0c;下面是我的方法&#xff1a; 查看kibana日志&#xff1a; docker logs -f kibana从控制台打印的日志可以发现&#xff…

在 Docker 中,无法直接将外部多个端口映射到容器内部的同一个端口

Docker 的端口映射是一对一的&#xff0c;即一个外部端口只能映射到容器内部的一个端口。 1. 为什么不能多对一映射&#xff1f; 端口冲突&#xff1a; 如果外部多个端口映射到容器内部的同一个端口&#xff0c;Docker 无法区分外部请求应该转发到哪个内部端口&#xff0c;会…

游戏引擎学习第120天

仓库:https://gitee.com/mrxiao_com/2d_game_3 上次回顾&#xff1a;周期计数代码 我们正在进行一个项目的代码优化工作&#xff0c;目标是提高性能。当前正在优化某个特定的代码片段&#xff0c;已经将其执行周期减少到48个周期。为了实现这一目标&#xff0c;我们设计了一个…

C++中的.h文件一般是干什么的?

在C中&#xff0c;.h 文件通常是 头文件&#xff08;Header File&#xff09;&#xff0c;它们的主要作用是声明类、函数、常量、宏以及其他在多个源文件&#xff08;.cpp文件&#xff09;之间共享的元素。头文件提供了一个接口&#xff0c;使得不同的源文件能够访问这些共享的…

基础算法总结

基础算法总结 1、模拟1.1 什么是模拟算法1.2 算法题1.2.1 多项式输出1.2.2 蛇形方阵 2 高精度算法2.1 什么是高精度算法2.2 算法题2.2.1 高精度加法 2.2.2 高精度乘法 3 普通枚举3.1 算法题3.1.1 铺地毯 3.1.2 回文日期 4 前缀和算法4.1 什么是前缀和4.2 算法题4.2.1 最大子段和…

密码学(哈希函数)

4.1 Hash函数与数据完整性 数据完整性&#xff1a; 检测传输消息&#xff08;加密或未加密&#xff09;的修改。 密码学Hash函数&#xff1a; 构建某些数据的简短“指纹”&#xff1b;如果数据被篡改&#xff0c;则该指纹&#xff08;以高概率&#xff09;不再有效。Hash函数…

游戏引擎学习第135天

仓库:https://gitee.com/mrxiao_com/2d_game_3 回顾 game_asset.cpp 的创建 在开发过程中&#xff0c;不使用任何现成的游戏引擎或第三方库&#xff0c;而是直接基于 Windows 进行开发&#xff0c;因为 Windows 目前仍然是游戏的标准平台&#xff0c;因此首先在这个环境中进行…

Linux:文件描述符与重定向

目录 一、文件描述符 1.文件内核对象 2.文件描述符分配原则 二、文件重定向 1.重定向的现象 输出重定向 输入重定向 dup2 2.重定向的使用 三、标准输出和标准错误 继上篇文章中&#xff0c;我们了解了fd打印的值为文件描述符&#xff0c;那么它还有什么作用呢&…

白盒测试(3):PCB阻抗测试方法

PCB阻抗测试是确保信号完整性的关键&#xff0c;通过测量走线的特性阻抗&#xff0c;验证其是否符合设计目标。常用方法包括时域反射法&#xff08;TDR&#xff09;、网络分析仪法和仿真软件法。TDR通过分析反射信号定位阻抗异常&#xff0c;网络分析仪通过S参数计算阻抗&#…

CentOS 7 安装Nginx-1.26.3

无论安装啥工具、首先认准了就是官网。Nginx Nginx官网下载安装包 Windows下载&#xff1a; http://nginx.org/download/nginx-1.26.3.zipLinxu下载 wget http://nginx.org/download/nginx-1.26.3.tar.gzLinux安装Nginx-1.26.3 安装之前先安装Nginx依赖包、自行选择 yum -y i…