模仿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_INFO4     add constraint ACT_FK_INFO_JSON_BA5     foreign key (INFO_JSON_ID_)6     references ACT_GE_BYTEARRAY (ID_);7 8 alter table ACT_PROCDEF_INFO9     add constraint ACT_FK_INFO_PROCDEF10     foreign key (PROC_DEF_ID_)11     references ACT_RE_PROCDEF (ID_);12 13 alter table ACT_PROCDEF_INFO14     add constraint ACT_UNIQ_INFO_PROCDEF15     unique (PROC_DEF_ID_);

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

image-20210315132805036

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

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

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

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

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

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

  1 @Component2 @Order(value=1)3 public class WatchStartCommandSqlRunnerImpl implements CommandLineRunner {4 5     @Override6     public void run(String... args) throws Exception {7         System.out.println("第一个Command执行");8     }9 10 11 @Component12 @Order(value = 2)13 public class WatchStartCommandSqlRunnerImpl2 implements CommandLineRunner {14     @Override15     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 operation4  * @param component5  * @param resourceName6  * @param inputStream7  */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_bin59                     这样,就可以先通过代码来将该建表语句执行到数据库中,实现如下: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

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

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

相关文章

3C电子制造:智慧物流引领产业升级

在当今科技飞速发展的时代,3C电子制造行业正面临着一系列挑战和机遇。市场需求的多变和技术革新的加速,使得企业必须不断创新和升级。在这个过程中,智慧物流成为了一个关键的环节,它能够有效地提高生产效率、降低成本并增强企业的…

Docker学习与应用(二)-docker常用命令

注:此为笔者学习狂神说Docker的笔记,其中包含个人的笔记和理解,更多详细资讯请出门左拐B站:狂神说!!! Docker的常用命令 帮助命令 docker version # 显示docker的版本信息 docker info # 显示docker的系统信息&am…

【学习笔记】1、数字逻辑概论

1.1 数字信号 数字信号,在时间和数值上均是离散的。数字信号的表达方式:二值数字逻辑和逻辑电平描述的数字波形。 (1) 数字波形的两种类型 数值信号又称为“二值信号”。数字波形又称为“二值位形图”。 什么是一拍 一定的时…

【Pytorch】学习记录分享13——OCR(Optical Character Recognition,光学字符识别)

[TOC](OCR(Optical Character Recognition,光学字符识别)) 1. OCR资源汇总 OCR(Optical Character Recognition,光学字符识别)指提取图像中的文字信息,通常包括文本检测和文本识别。 文字检测:将图片中的文字区域位置检测出来(如图1(b)所示…

Dash+Plotly | Web应用开发(1)

本文为https://github.com/CNFeffery/DataScienceStudyNotes的学习笔记,部分源码来源于此仓库。 本期内容主要为基础概念、web布局方法和交互回调。 文章目录 Dash的主要模块Highlightlayoutcallback 惰性交互阻止初次回调忽略回调匹配错误控制部分回调输出不更新获…

【总线接口】1.以Xilinx开发板为例,直观的认识硬件板卡和接口

初接触硬件,五花八门的总线、接口一定会让你有些疑惑,我尝试用一系列文章来解开你的疑惑 系列文章 【总线接口】1.以Xilinx开发板为例,直观的认识硬件接口 【总线接口】2.学习硬件这些年接触过的硬件接口、总线 大汇总 【总线接口】…

【STM32】STM32学习笔记-DMA数据转运+AD多通道(24)

00. 目录 文章目录 00. 目录01. DMA简介02. DMA相关API2.1 DMA_Init2.2 DMA_InitTypeDef2.3 DMA_Cmd2.4 DMA_SetCurrDataCounter2.5 DMA_GetFlagStatus2.6 DMA_ClearFlag 03. DMA数据单通道接线图04. DMA数据单通道示例05. DMA数据多通道接线图06. DMA数据多通道示例一07. DMA数…

关于一个热成像仪的总结(一)硬件篇电源电路

1、电源部分 电源部分电路原理是这样的通过3.7V的锂电池供电,用Type-C选用TP4056作为充电电路给电池充电。使用MP2161开关电源作为5转3.3V 电源为MCU供电。 1-1电池 待定 1-2充电管理芯片TP4056 参考datasheet:https://atta.szlcsc.com/upload/publi…

vue3+echart绘制中国地图并根据后端返回的坐标实现涟漪动画效果

1.效果图 2.前期准备 main.js app.use(BaiduMap, {// ak 是在百度地图开发者平台申请的密钥 详见 http://lbsyun.baidu.com/apiconsole/key */ak: sRDDfAKpCSG5iF1rvwph4Q95M6tDCApL,// v:3.0, // 默认使用3.0// type: WebGL // ||API 默认API (使用此模式 BMapBMapGL) });i…

CSDN博客重新更新

说来惭愧,好久没更新博客文章,导致个人博客网站:https://lenky.info/ 所在的网络空间和域名都过期了都没发觉,直到有个同事在Dim上问我我的个人博客为啥打不开了。。。幸好之前有做整站备份,后续慢慢把内容都迁回CSDN上…

学习笔记——C++运算符之比较运算符

作用&#xff1a;用于表达式的比较&#xff0c;并返回一个真值或假值 比较运算符有以下符号&#xff1a; #include<bits/stdc.h> using namespace std; int main(){//int a10;int b20;cout<<(ab)<<endl;//0//!cout<<(a!b)<<endl;//1//>cout&…

第十天:信息打点-APPamp;小程序篇amp;抓包封包amp;XP框架amp;反编译amp;资产提取

信息打点-APP&小程序 一、内在收集-代码 从app代码中去收集 1、移动端AppInfoScanner工具信息收集 安卓语法&#xff1a; python app.py android -i <Your apk file> 这个是从app代码中提取信息。 有些app会限制代理抓包&#xff0c;需要进行解壳。 类似CDN的…

数据链路层(Data Link Layer)

数据链路层&#xff08;Data Link Layer&#xff09;是计算机网络体系结构中的一层&#xff0c;位于物理层和网络层之间。它的主要功能是在物理传输媒体上建立和管理数据链路。数据链路层的设计和实现对于网络通信的可靠性和效率至关重要。在本文中&#xff0c;我们将探讨数据链…

IIS+SDK+VS2010+SP1+SQL server2012全套工具包及安装教程

前言 今天花了两个半小时安装这一整套配置&#xff0c;这个文章的目标是将安装时间缩短到1个小时 正文 安装步骤如下&#xff1a; VS2010 —> service pack 1 —>SQL server2012 —> IIS —> SDK 工具包链接如下&#xff1a; https://pan.baidu.com/s/1WQD-KfiUW…

[技术杂谈]使用VLC将视频转成一个可循环rtsp流

通过vlc播放器&#xff0c;将一个视频转成rtsp流&#xff0c;搭建一个rtsp服务器。rtsp客户端可访问这个视频的rtsp流。 1. 打开vlc播放器&#xff0c;使用的版本如下 2. 菜单&#xff1a;媒体 ---> 流 3. 添加视频文件&#xff0c;点击添加一个mp4 文件 4. 选择串流&…

如何安装和使用夜神模拟器连接Android Studio

目录 简介 一、安装 二、使用 三、更多资源 简介 夜神模拟器是一款在Windows平台上运行的Android模拟器软件。它能够模拟Android操作系统环境&#xff0c;让用户在电脑上轻松体验Android应用程序。夜神模拟器的功能强大&#xff0c;可以满足各种需求&#xff0c;无论是娱乐…

故障诊断 | 基于FFT频谱与小波时频图的双流CNN轴承故障诊断模型(matlab +python)

目录 效果一览基本介绍程序设计参考资料 效果一览 基本介绍 故障诊断 | 基于FFT频谱与小波时频图的双流CNN轴承故障诊断模型&#xff08;matlab python&#xff09; 基于FFT频谱与小波时频图的双流CNN轴承故障诊断模型 特征拼接 python&#xff08;pytorch&#xff09; 基于2D-…

大模型迎来“AppStore时刻”,OpenAI给2024的新想象

一夜之间&#xff0c;OpenAI公布了多个重磅消息&#xff0c;引发市场关注。 钛媒体App 1月5日消息&#xff0c;今晨&#xff0c;OpenAI公司向所有GPT开发者们发布一封邮件称&#xff0c;下周将上线自定义的“GPT Store”商店&#xff0c;这有望推动ChatGPT开发者生态不断完善。…

开源加解密库之GmSSL

一、简介 GmSSL是由北京大学自主开发的国产商用密码开源库&#xff0c;实现了对国密算法、标准和安全通信协议的全面功能覆盖&#xff0c;支持包括移动端在内的主流操作系统和处理器&#xff0c;支持密码钥匙、密码卡等典型国产密码硬件&#xff0c;提供功能丰富的命令行工具及…

多线程高级知识点

多线程高级知识点 1.ThreadLocal 1.1 什么是 ThreadLocal&#xff1f; ​ ThreadLocal 叫做本地线程变量&#xff0c;意思是说&#xff0c;ThreadLocal 中填充的的是当前线程的变量&#xff0c;该变量对其他线程而言是封闭且隔离的&#xff0c;ThreadLocal 为变量在每个线程…