模仿Activiti工作流自动建表机制,实现Springboot项目启动后自动创建多表关联的数据库与表的方案

文/朱季谦

熬夜写完,尚有不足,但仍在努力学习与总结中,而您的点赞与关注,是对我最大的鼓励!

在一些本地化项目开发当中,存在这样一种需求,即开发完成的项目,在第一次部署启动时,需能自行构建系统需要的数据库及其对应的数据库表。

若要解决这类需求,其实现在已有不少开源框架都能实现自动生成数据库表,如mybatis plus、spring JPA等,但您是否有想过,若要自行构建一套更为复杂的表结构时,这种开源框架是否也能满足呢,若满足不了话,又该如何才能实现呢?

我在前面写过一篇 Activiti工作流学习笔记(三)——自动生成28张数据库表的底层原理分析,里面分析过工作流Activiti自动构建28数据库表的底层原理。在我看来,学习开源框架的底层原理,其中一个原因是,须从中学到能为我所用的东西。故而,在分析理解完工作流自动构建28数据库表的底层原理之后,我决定也写一个基于Springboot框架的自行创建数据库与表的demo。我参考了工作流Activiti6.0版本的底层建表实现的逻辑,基于Springboot框架,实现项目在第一次启动时可自动构建各种复杂如多表关联等形式的数据库与表的。

整体实现思路并不复杂,大概是这样:先设计一套完整创建多表关联的数据库sql脚本,放到resource里,在springboot启动过程中,自动执行sql脚本。

首先,先一次性设计一套可行的多表关联数据库脚本,这里我主要参考使用Activiti自带的表做实现案例,因为它内部设计了众多表关联,就不额外设计了。

sql脚本的语句就是平常的create建表语句,类似如下:

  1 create table ACT_PROCDEF_INFO (
  2    ID_ varchar(64) not null,
  3     PROC_DEF_ID_ varchar(64) not null,
  4     REV_ integer,
  5     INFO_JSON_ID_ varchar(64),
  6     primary key (ID_)
  7 ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE utf8_bin;

增加外部主键、索引——

  1 create index ACT_IDX_INFO_PROCDEF on ACT_PROCDEF_INFO(PROC_DEF_ID_);
  2 
  3 alter table ACT_PROCDEF_INFO
  4     add constraint ACT_FK_INFO_JSON_BA
  5     foreign key (INFO_JSON_ID_)
  6     references ACT_GE_BYTEARRAY (ID_);
  7 
  8 alter table ACT_PROCDEF_INFO
  9     add constraint ACT_FK_INFO_PROCDEF
 10     foreign key (PROC_DEF_ID_)
 11     references ACT_RE_PROCDEF (ID_);
 12 
 13 alter table ACT_PROCDEF_INFO
 14     add constraint ACT_UNIQ_INFO_PROCDEF
 15     unique (PROC_DEF_ID_);

整体就是设计一套符合符合需求场景的sql语句,保存在.sql的脚本文件里,最后统一存放在resource目录下,类似如下:

image-20210315132805036

接下来,就是实现CommandLineRunner的接口,重写其run()的bean回调方法,在run方法里开发能自动建库与建表逻辑的功能。

目前,我已将开发的demo上传到了我的github,感兴趣的童鞋,可自行下载,目前能直接下下来在本地环境运行,可根据自己的实际需求针对性参考使用。

首先,在解决这类需求时,第一个先要解决的地方是,Springboot启动后如何实现只执行一次建表方法。

这里需要用到一个CommandLineRunner接口,这是Springboot自带的,实现该接口的类,其重写的run方法,会在Springboot启动完成后自动执行,该接口源码如下:

  1 @FunctionalInterface
  2 public interface CommandLineRunner {
  3 
  4    /**
  5     *用于运行bean的回调
  6     */
  7    void run(String... args) throws Exception;
  8 
  9 }

扩展一下,在Springboot中,可以定义多个实现CommandLineRunner接口类,并且可以对这些实现类中进行排序,只需要增加@Order,其重写的run方法就可以按照顺序执行,代码案例验证:

  1 @Component
  2 @Order(value=1)
  3 public class WatchStartCommandSqlRunnerImpl implements CommandLineRunner {
  4 
  5     @Override
  6     public void run(String... args) throws Exception {
  7         System.out.println("第一个Command执行");
  8     }
  9 
 10 
 11 @Component
 12 @Order(value = 2)
 13 public class WatchStartCommandSqlRunnerImpl2 implements CommandLineRunner {
 14     @Override
 15     public void run(String... args) throws Exception {
 16         System.out.println("第二个Command执行");
 17     }
 18 }
 19 

控制台打印的信息如下:

  1 第一个Command执行
  2 第二个Command执行

根据以上的验证,因此,我们可以通过实现CommandLineRunner的接口,重写其run()的bean回调方法,用于在Springboot启动后实现只执行一次建表方法。实现项目启动建表的功能,可能还需实现判断是否已经有相应数据库,若无,则应先新建一个数据库,同时,得考虑还没有对应数据库的情况,因此,我们通过jdbc第一次连接MySQL时,应连接一个原有自带存在的库。每个MySql安装成功后,都会有一个mysql库,在第一次建立jdbc连接时,可以先连接它。

image-20210315080736373

代码如下:

Class.forName("com.mysql.jdbc.Driver");
String url="jdbc:mysql://127.0.0.1:3306/mysql?useUnicode=true&characterEncoding=UTF-8&ueSSL=false&serverTimezone=GMT%2B8";
Connection conn= DriverManager.getConnection(url,"root","root");

建立与MySql软件连接后,先创建一个Statement对象,该对象是jdbc中可用于执行静态 SQL 语句并返回它所生成结果的对象,这里可以使用它来执行查找库与创建库的作用。

  1  //创建Statement对象
  2  Statement statment=conn.createStatement();
  3  /**
  4  使用statment的查询方法executeQuery("show databases like \"fte\"")
  5  检查MySql是否有fte这个数据库
  6  **/
  7  ResultSet resultSet=statment.executeQuery("show databases like \"fte\"");
  8  //若resultSet.next()为true,证明已存在;
  9  //若false,证明还没有该库,则执行statment.executeUpdate("create database fte")创建库
 10  if(resultSet.next()){
 11      log.info("数据库已经存在");
 12   }else {
 13   log.info("数据库未存在,先创建fte数据库");
 14   if(statment.executeUpdate("create database fte")==1){
 15      log.info("新建数据库成功");
 16      }
 17    }

在数据库fte自动创建完成后,就可以在该fte库里去做建表的操作了。

我将建表的相关方法都封装到SqlSessionFactory类里,相关建表方法同样需要用到jdbc的Connection连接到数据库,因此,需要把已连接的Connection引用变量当做参数传给SqlSessionFactory的初始构造函数:

  1    public void createTable(Connection conn,Statement stat) throws SQLException {
  2         try {
  3 
  4             String url="jdbc:mysql://127.0.0.1:3306/fte?useUnicode=true&characterEncoding=UTF-8&ueSSL=false&serverTimezone=GMT%2B8";
  5             conn=DriverManager.getConnection(url,"root","root");
  6             SqlSessionFactory sqlSessionFactory=new SqlSessionFactory(conn);
  7             sqlSessionFactory.schemaOperationsBuild("create");
  8         } catch (SQLException e) {
  9             e.printStackTrace();
 10         }finally {
 11             stat.close();
 12             conn.close();
 13         }
 14     }

初始化new SqlSessionFactory(conn)后,就可以在该对象里使用已进行连接操作的Connection对象了。

  1 public class SqlSessionFactory{
  2     private Connection connection ;
  3     public SqlSessionFactory(Connection connection) {
  4         this.connection = connection;
  5     }
  6 ......
  7 }

这里传参可以有两种情况,即“create”代表创建表结构的功能,“drop”代表删除表结构的功能:

  1 sqlSessionFactory.schemaOperationsBuild("create");

进入到这个方法里,会先做一个判断——

  1 public void schemaOperationsBuild(String type) {
  2     switch (type){
  3         case "drop":
  4             this.dbSchemaDrop();break;
  5         case "create":
  6             this.dbSchemaCreate();break;
  7     }
  8 }

若是this.dbSchemaCreate(),执行建表操作:

  1 /**
  2  * 新增数据库表
  3  */
  4 public void dbSchemaCreate() {
  5 
  6     if (!this.isTablePresent()) {
  7         log.info("开始执行create操作");
  8         this.executeResource("create", "act");
  9         log.info("执行create完成");
 10     }
 11 }

this.executeResource("create", "act")代表创建表名为act的数据库表——

  1 public void executeResource(String operation, String component) {
  2     this.executeSchemaResource(operation, component, this.getDbResource(operation, operation, component), false);
  3 }

其中 this.getDbResource(operation, operation, component)是获取sql脚本的路径,进入到方法里,可见——

  1 public String getDbResource(String directory, String operation, String component) {
  2     return "static/db/" + directory + "/mysql." + operation + "." + component + ".sql";
  3 }

接下来,读取路径下的sql脚本,生成输入流字节流:

  1 public void executeSchemaResource(String operation, String component, String resourceName, boolean isOptional) {
  2     InputStream inputStream = null;
  3 
  4     try {
  5         //读取sql脚本数据
  6         inputStream = IoUtil.getResourceAsStream(resourceName);
  7         if (inputStream == null) {
  8             if (!isOptional) {
  9                 log.error("resource '" + resourceName + "' is not available");
 10                 return;
 11             }
 12         } else {
 13             this.executeSchemaResource(operation, component, resourceName, inputStream);
 14         }
 15     } finally {
 16         IoUtil.closeSilently(inputStream);
 17     }
 18 
 19 }

最后,整个执行sql脚本的核心实现在this.executeSchemaResource(operation, component, resourceName, inputStream)方法里——

  1 /**
  2  * 执行sql脚本
  3  * @param operation
  4  * @param component
  5  * @param resourceName
  6  * @param inputStream
  7  */
  8 private void executeSchemaResource(String operation, String component, String resourceName, InputStream inputStream) {
  9     //sql语句拼接字符串
 10     String sqlStatement = null;
 11     Object exceptionSqlStatement = null;
 12 
 13     try {
 14         /**
 15          * 1.jdbc连接mysql数据库
 16          */
 17         Connection connection = this.connection;
 18 
 19         Exception exception = null;
 20         /**
 21          * 2、分行读取"static/db/create/mysql.create.act.sql"里的sql脚本数据
 22          */
 23         byte[] bytes = IoUtil.readInputStream(inputStream, resourceName);
 24         /**
 25          * 3.将sql文件里数据分行转换成字符串,换行的地方,用转义符“\n”来代替
 26          */
 27         String ddlStatements = new String(bytes);
 28         /**
 29          * 4.以字符流形式读取字符串数据
 30          */
 31         BufferedReader reader = new BufferedReader(new StringReader(ddlStatements));
 32         /**
 33          * 5.根据字符串中的转义符“\n”分行读取
 34          */
 35         String line = IoUtil.readNextTrimmedLine(reader);
 36         /**
 37          * 6.循环读取的每一行
 38          */
 39         for(boolean inOraclePlsqlBlock = false; line != null; line = IoUtil.readNextTrimmedLine(reader)) {
 40             /**
 41              * 7.若下一行line还有数据,证明还没有全部读取,仍可执行读取
 42              */
 43             if (line.length() > 0) {
 44                 /**
 45                  8.在没有拼接够一个完整建表语句时,!line.endsWith(";")会为true,
 46                  即一直循环进行拼接,当遇到";"就跳出该if语句
 47                 **/
 48                if ((!line.endsWith(";") || inOraclePlsqlBlock) && (!line.startsWith("/") || !inOraclePlsqlBlock)) {
 49                     sqlStatement = this.addSqlStatementPiece(sqlStatement, line);
 50                 } else {
 51                    /**
 52                     9.循环拼接中若遇到符号";",就意味着,已经拼接形成一个完整的sql建表语句,例如
 53                     create table ACT_GE_PROPERTY (
 54                     NAME_ varchar(64),
 55                     VALUE_ varchar(300),
 56                     REV_ integer,
 57                     primary key (NAME_)
 58                     ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE utf8_bin
 59                     这样,就可以先通过代码来将该建表语句执行到数据库中,实现如下:
 60                     **/
 61                     if (inOraclePlsqlBlock) {
 62                         inOraclePlsqlBlock = false;
 63                     } else {
 64                         sqlStatement = this.addSqlStatementPiece(sqlStatement, line.substring(0, line.length() - 1));
 65                     }
 66                    /**
 67                     * 10.将建表语句字符串包装成Statement对象
 68                     */
 69                     Statement jdbcStatement = connection.createStatement();
 70 
 71                     try {
 72                         /**
 73                          * 11.最后,执行建表语句到数据库中
 74                          */
 75                         log.info("SQL: {}", sqlStatement);
 76                         jdbcStatement.execute(sqlStatement);
 77                         jdbcStatement.close();
 78                     } catch (Exception var27) {
 79                         log.error("problem during schema {}, statement {}", new Object[]{operation, sqlStatement, var27});
 80                     } finally {
 81                         /**
 82                          * 12.到这一步,意味着上一条sql建表语句已经执行结束,
 83                          * 若没有出现错误话,这时已经证明第一个数据库表结构已经创建完成,
 84                          * 可以开始拼接下一条建表语句,
 85                          */
 86                         sqlStatement = null;
 87                     }
 88                 }
 89             }
 90         }
 91 
 92         if (exception != null) {
 93             throw exception;
 94         } 
 97     } catch (Exception var29) {
 98         log.error("couldn't " + operation + " db schema: " + exceptionSqlStatement, var29);
 99     }
100 }

复制代码

这部分代码主要功能是,先用字节流形式读取sql脚本里的数据,转换成字符串,其中有换行的地方用转义符“/n”来代替。接着把字符串转换成字符流BufferedReader形式读取,按照“/n”符合来划分每一行的读取,循环将读取的每行字符串进行拼接,当循环到某一行遇到“;”时,就意味着已经拼接成一个完整的create建表语句,类似这样形式——

  1 create table ACT_PROCDEF_INFO (
  2    ID_ varchar(64) not null,
  3     PROC_DEF_ID_ varchar(64) not null,
  4     REV_ integer,
  5     INFO_JSON_ID_ varchar(64),
  6     primary key (ID_)
  7 ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE utf8_bin;

这时,就可以先将拼接好的create建表字符串,通过 jdbcStatement.execute(sqlStatement)语句来执行入库了。当执行成功时,该ACT_PROCDEF_INFO表就意味着已经创建成功,接着以BufferedReader字符流形式继续读取下一行,进行下一个数据库表结构的构建。

整个过程大概就是这个逻辑,可以在此基础上,针对更为复杂的建表结构sql语句进行设计,在项目启动时,自行执行相应的sql语句,来进行建表。

该demo代码已经上传git,可直接下载运行:GitHub - z924931408/Springboot-AutoCreateMySqlTable: 模仿工作流引擎Activity自动建表机制实现Springboot在启动时自动生成数据库与表demo

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

第二证券:证监会紧急声明!加密货币大笔爆仓

当地时刻1月9日,美股三大指数涨跌纷歧。道指跌157.85点,跌幅为0.42%,报37525.16点;纳指涨13.94点,涨幅为0.09%,报14857.71点;标普500指数跌7.04点,跌幅为0.15%,报4756.50…

蓝凌EIS智慧协同平台 ShowUserInfo.aspx sql注入漏洞

漏洞描述: 蓝凌EIS智慧协同平台是一个简单、高效的工作方式专为成长型企业打造的沟通、协同、社交的移动办公平台,覆盖OA、沟通、客户、人事、知识等管理需求,集合了非常丰富的模块,满足组织企业在知识、项目管理系统建设等需求的…

三维猴打印PCB外壳预留板壳间距

3D文件下单那里有一个“3D模型设计规范”,里面详细讲了设计时要考虑打印的参数细节。如果有其他的设计规范不了解的也可以进去查看,里面写的很详细。 这里是打印PCB外壳预留板壳间距相关说明: 设计模型为装配体,请务必满足装配最…

IoT 物联网 MQTT 协议 5.0 版本新特性

MQTT 是一种基于发布/订阅模式的轻量级消息传输协议,专门为设备资源有限和低带宽、高延迟的不稳定网络环境的物联网场景应用而设计,可以用极少的代码为联网设备提供实时可靠的消息服务。MQTT 协议广泛应用于智能硬件、智慧城市、智慧农业、智慧医疗、新零…

基于ssm的在线视频网站开发论文

摘 要 计算机网络发展到现在已经好几十年了,在理论上面已经有了很丰富的基础,并且在现实生活中也到处都在使用,可以说,经过几十年的发展,互联网技术已经把地域信息的隔阂给消除了,让整个世界都可以即时通话…

LED电子屏组装常见故障及解决办法大全

在LED电子屏的组装过程中,可能会遇到各种故障。以下是一些常见问题及其解决方法: 1. 加载不上可能的原因及解决办法: A. 确保控制系统硬件正确上电(5V)。 B. 检查并确认用于连接控制器的串口线为直通线。 C. 检查串口…

JAVA课程设计--类京东购物车设计

目录 小组成员 负责模块: 程序介绍: 1.前期调查: 1.1京东购物车页面 首页 购物车界面 商品信息界面 搜索界面 1.2程序设计页面 首页 购物车页面 商品信息界面 搜索界面: 2.使用环境: 3.技术支持 前端V…

pycharm+pyqt 外部工具:ui转py、打开qtDesigner、.qrc转py

目录 1、打开设置-外部工具 2、qtdesigner工具 3、ui转py 4、prc转py 5、找到配置好的外部工具进行使用 1、打开设置-外部工具 点击“ctrlalts”一键打开设置界面,进入界面后“工具-外部工具-按钮-创建工具栏”打开创建外部工具窗口,如下图所示&…

算法通关村番外篇-LeetCode编程从0到1系列二

大家好我是苏麟 , 今天来说LeetCode编程从0到1系列二 . 内置函数 最后一个单词的长度 描述 : 给你一个字符串 s,由若干单词组成,单词前后用一些空格字符隔开。返回字符串中 最后一个 单词的长度。 单词 是指仅由字母组成、不包含任何空格字符的最大子…

STM32F103C8T6(HAL库函数 - 内部Flash操作)

简介 STM32F103C8T6 内部Flash 为 64KB,本次将对他多余空间进行读写。 介绍 数据手册下载 STM32F103x8/STM32F103xB 数据手册 包含Flash Memory Page分布 STM32F设备命名 设备容量类型 中容量类型 内部空间介绍 64 KBytes大小Flash Memory 从 0x0800 0000 ~…

【驱动序列】C#获取电脑硬件之CPU信息,以及它都有那些品牌

欢迎来到《小5讲堂》,大家好,我是全栈小5。 这是是《驱动序列》文章,每篇文章将以博主理解的角度展开讲解, 特别是针对知识点的概念进行叙说,大部分文章将会对这些概念进行实际例子验证,以此达到加深对知识…

燃油车智能化时代将终结,长安汽车凭啥引领“数智新汽车”周期?

日前高工智能汽车研究院发布报告称,2024年将是新能源市场的新拐点,燃油车智能化时代即将终结,新能源和智能化将深度融合发展。 伴随着整车电子电气架构加速迈入中央计算-区域控制架构时代,智能电动汽车将从单一功能升级的智能化1…

机器学习 —— 自用整理期末复习笔记

一、绪论 机器学习术语 假设空间 p5 监督学习(supervised learning)的任务是学习一个模型,使模型能够对任意给定的输入,对其相应的输出做出一个好的预测。模型属于由输入空间到输出空间的映射的集合,这个集合就是假设空…

shader技巧

数学函数: abs():绝对值函数。 acos():反余弦函数。 asin():反正弦函数。 atan():反正切函数。 ceil():向上取整函数。 cos():余弦函数。 cross():向量叉积函数。 distance()&#x…

Cesium笔记 viewer控件隐藏

Cesium初始化后,场景中会有时间轴,动画,home等控件显示,需要将这些控件隐藏,如下: init() {let viewer new Cesium.Viewer("cesiumContainer", {fullscreenButton: false, // 隐藏界面右下角全…

PLECS如何下载第三方库并导入MOSFET 的xml文件,xml库路径添加方法及相关问题

1. 首先xml库的下载,PLECS提供了一个跳转的链接。 https://www.plexim.com/download/thermal_models 2. 下载一个库(以最后一个Wolfspeed为例,属于CREE的SiC MOSFET) 下载这个就行,都包含了。不信自己可以试试再下载…

arthas 内存占用过大排查

使用经验分享 线上故障排查思路: 1、紧急处理,优先保障服务可用(如切换vip,主备容灾) 2、保留第一现场,通过jstack -l {pid} > jvmtmp.txt ,打印栈信息 (后续可以在gceasy官网上…

vagrant 用户名密码登录

正常登录后 sudo -i 切换到root权限 vim /etc/ssh/vim sshd_config 将PasswordAuthentication no设置 为yes 重启sshd.service服务 systemctl restart sshd.service

Apache Doris (六十二): Spark Doris Connector - (2)-使用

🏡 个人主页:IT贫道-CSDN博客 🚩 私聊博主:私聊博主加WX好友,获取更多资料哦~ 🔔 博主个人B栈地址:豹哥教你学编程的个人空间-豹哥教你学编程个人主页-哔哩哔哩视频 目录 1. 将编译jar包加入本地Maven仓库

Vue3:使用解构赋值来读取对象里的键-值对(值也是对象)

一、前言 在Vue3中,想要读取一个对象的“键—值”对(值也是一个对象),数据格式如下: {1:{courseName: 课程1, study: 951526, visit: 3785553},2:{courseName: 课程2, study: 181630, visit: 380830}&…