模仿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.rhkb.cn/news/235014.html

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

相关文章

C++笔记之cout高亮输出以及纯C++实现一个彩色时钟

C笔记之cout高亮输出以及纯C实现一个彩色时钟 code review! 文章目录 C笔记之cout高亮输出以及纯C实现一个彩色时钟一.cout高亮输出1.1.运行1.2.代码一1.3.代码二1.4.重置终端的文本格式到默认设置说明 二.纯C实现一个彩色时钟2.1.运行2.2.main.cc2.3.cout带颜色打印输出技巧…

springCould中的Bus-从小白开始【11】

目录 🧂1.Bus是什么❤️❤️❤️ 🌭2.什么是总线❤️❤️❤️ 🥓3.rabbitmq❤️❤️❤️ 🥞4.新建模块3366❤️❤️❤️ 🍳5.设计思想 ❤️❤️❤️ 🍿6.添加消息总线的支持❤️❤️❤️ &#x1f9…

图解Kubernetes的服务(Service)

pod 准备: 不要直接使用和管理Pods: 当使用ReplicaSet水平扩展scale时,Pods可能被terminated当使用Deployment时,去更新Docker Image Version,旧Pods会被terminated,然后创建新Pods 0 啥是服务&#xf…

OCS2 入门教程(四)- 机器人示例

系列文章目录 前言 OCS2 包含多个机器人示例。我们在此简要讨论每个示例的主要特点。 System State Dim. Input Dim. Constrained Caching Double Integrator 2 1 No No Cartpole 4 1 Yes No Ballbot 10 3 No No Quadrotor 12 4 No No Mobile Manipul…

【java爬虫】首页显示沪深300指数走势图以及前后端整合部署方法

添加首页 本文我们将在首页添加沪深300指数成立以来的整体走势数据展示,最后的效果是这样的 单独贴一张沪深300整体走势图 我感觉从总体上来看指数还是比较稳的,没有特别大的波动,当然,这只是相对而言哈哈。 首先是前端页面 &l…

【python】内存管理和数据类型问题

一、内存管理 Python有一个自动内存管理机制,但它并不总是按照期望的方式工作。例如,如果创建了一个大的列表或字典,并且没有删除它,那么这个对象就会一直占用内存,直到Python的垃圾回收器决定清理它。为了避免这种情…

Android开发基础(一)

Android开发基础(一) 本篇主要是从Android系统架构理解Android开发。 Android系统架构 Android系统的架构采用了分层的架构,共分为五层,从高到低分别是Android应用层(System Apps)、Android应用框架层&a…

二线厂商-线上测评-大数据开发

曾经投递过一些中级岗位,在面试之前,会通过邮件的方式把性格测试的题目发给你让你做一下。 一般分为单选题,多选题,性格测试题,认知理解题等等。 大概做了一个小时吧。 单选题: 感觉就是类似于以前高中时候…

前缀和--二维矩阵的前缀和

目录 子矩阵的和思路:代码: 原题链接 子矩阵的和 输入一个 n 行 m 列的整数矩阵,再输入 q 个询问,每个询问包含四个整数 x1,y1,x2,y2 ,表示一个子矩阵的左上角坐标和右下角坐标。 对于每个询问输出子矩阵中所有数的和…

C#入门篇(一)

变量 顾名思义就是变化的容器,即可以用来存放各种不同类型数值的一个容器 折叠代码 第一步:#region 第二步:按tab键 14种数据类型 有符号的数据类型 sbyte:-128~127 short:-32768~32767 int:-21亿多~21亿多…

Windows 双网卡链路聚合解决方案

Windows 双网卡链路聚合解决方案 链路聚合方案1:Metric介绍操作 方案2:NetSwitchTeam介绍操作 方案3:NIC介绍操作 方案4:Intel PROSet 链路聚合 指将多个物理端口汇聚在一起,形成一个逻辑端口,以实现出/入…

Java-布隆过滤器的实现

文章目录 前言一、概述二、误差率三、hash 函数的选择四、手写布隆过滤器五、guava 中的布隆过滤器 前言 如果想要判断一个元素是不是在一个集合里,一般想到的是将所有元素保存起来,然后通过比较确定。链表,树等等数据结构都是这种思路&…

Linux权限2

相关命令 chown [用户名] [文件]​ 更改文件拥有者(加sudo强制更改) chown [拥有者]:[所属组] [文件] 更改文件拥有者和所属组(root权限下) chgrp [用户名] [文件] 更改文件所属组 文件类型 输入ls或ll显示的文件&#xff…

设备树的绑定文档说明

一. 简介 设备树是用来描述板子上的设备信息的,不同的设备其信息不同,反映到设备树中就是属 性不同。 那么,我们在设备树中添加一个硬件对应的节点的时候从哪里查阅相关的说明呢? 在 Linux 内核源码中有详细的 .txt 文档描述…

【38 Pandas+Pyecharts | 奥迪汽车销量数据分析可视化】

文章目录 🏳️‍🌈 1. 导入模块🏳️‍🌈 2. Pandas数据处理2.1 读取数据2.2 查看数据信息2.3 数据处理 🏳️‍🌈 3. Pyecharts数据可视化3.1 奥迪用户购车时间分布3.2 奥迪各系销量占比饼图3.3 奥迪各系销量…

setup 语法糖

只有vue3.2以上版本可以使用 优点: 更少的样板内容,更简洁的代码 能够使用纯 Typescript 声明props 和抛出事件 更好的运行时性能 更好的IDE类型推断性能 在sciprt标识上加上setup 顶层绑定都可以使用 不需要return ,可以直接使用 使用组件…

如何设置电脑桌面提醒,电脑笔记软件哪个好?

对于大多数上班族来说,每天要完成的待办事项实在太多了,如果不能及时去处理,很容易因为各种因素导致忘记,从而给自己带来不少麻烦。所以,我们往往会借助一些提醒类的软件将各项任务逐一记录下来,然后设置上…

66、python - 代码仓库介绍

上一节,我们可以用自己手写的算法以及手动搭建的神经网络完成预测了,不知各位同学有没有自己尝试来预测一只猫或者一只狗,看看准确度如何? 本节应一位同学的建议,来介绍下 python 代码仓库的目录结构,以及每一部分是做什么? 我们这个小课的代码实战仓库链接为:cv_lea…

【Java技术专题】「攻破技术盲区」攻破Java技术盲点之unsafe类的使用指南(打破Java的安全管控— sun.misc.unsafe)

Java后门机制 — sun.misc.unsafe 打破Java的安全管控关于Unsafe的编程建议实例化Unsafe后门对象使用sun.misc.Unsafe创建实例单例模式处理实现浅克隆(直接获取内存的方式)直接使用copyMemory原理分析 密码安全使用Unsafe类—示例代码 运行时动态创建类超…

ocrmypdf_pdf识别

安装 安装说明 https://ocrmypdf.readthedocs.io/en/latest/installation.html#native-windows提到需要的软件: Python 3.7 (64-bit) or later Tesseract 4.0 or later Ghostscript 9.50 or later 安装 ocrmypdf pip install ocrmypdf 添加语言包 https://oc…