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

news2024/9/30 7:24:12

文/朱季谦

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

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

若要解决这类需求,其实现在已有不少开源框架都能实现自动生成数据库表,如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

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

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

相关文章

【视频图像篇】模糊图像处理之车辆运动造成的字体模糊还原

【视频图像篇】模糊图像处理之车辆运动造成的字体模糊还原 处理车辆运动过程中造成字体模糊重影的图像处理过程—【蘇小沐】 0、目录 1、实验环境 2、路径 3、去运动模糊(有噪声) 4、其它参数 5、结果展示 1、实验环境 系统环境Windows 11 专业…

phpstudy面板Table ‘mysql.proc‘ doesn‘t exist解决办法

原因分析:误删了mysql数据库 解决办法如下: 1、停止服务 2、先把mysql文件夹下的data文件夹备份,因为data文件里存有数据库文件。然后再删除data文件。 3、cmd管理员命令进入到mysql中的bin目录下 ,执行mysqld --initialize-…

Window端口占用处理

您好,我是码农飞哥(wei158556),感谢您阅读本文,欢迎一键三连哦。 💪🏻 1. Python基础专栏,基础知识一网打尽,9.9元买不了吃亏,买不了上当。 Python从入门到精…

K8S-应用访问

1 service对象定位 2 Service 实践 手工创建Service 根据应用部署资源对象,创建SVC对象 kubectl expose deployment nginx --port80 --typeNodePortyaml方式创建Service nginx-web的service资源清单文件 apiVersion: v1 kind: Service metadata:name: sswang-ngi…

OpenHarmony底座升级指南(3.2升级4.0)

前言 本文旨在帮助开发者完成底座升级,文中主要以OpenHarmony 3.2 release 升级至OpenHarmony 4.0 release为模板描述。 一、流程概览: 1.1 准备工作 在准备阶段,需要完整收集所有的定制化修改,明确修改人;并且要将…

如何选择最适合的采购付款 (P2P) 解决方案?

无论企业的业务流程执行得如何,流程中始终存在改进空间。更好的管理系统是获得更好结果的关键,尤其是当企业处于增长阶段时。强大的采购到付款(P2P)系统是加快采购流程,同时保持采购支出可见性的最有效方法之一。 什么…

qml的操作 -- VS2022开发qml,

在使用VS开发软件的时候一般大型软件都会使用模组的方式。每个模组之间独立开发,关于qml写的UI模组也不例外,如果所有的qml都挤在一个文件夹下也不利于管理,维护起来也比较吃力。比较好的管理方法就是按照功能分布存放在不同的文件夹下。还有…

关于“Python”的核心知识点整理大全65

目录 20.2.19 设置 SECRET_KEY 20.2.20 将项目从 Heroku 删除 注意 20.3 小结 附录 A 安装Python A.1.1 确定已安装的版本 A.1.2 在 Linux 系统中安装 Python 3 A.2 在 OS X 系统中安装 Python A.2.1 确定已安装的版本 A.2.2 使用 Homebrew 来安装 Python 3 注意 …

【Docker】docker 服务相关命令

目录 1. 启动docker 服务 2.查看docker 服务的状态 3. 停止docker 服务 4.重启 docker 服务 5.开机自启动命令 1. 启动docker 服务 systemctl start docker 2.查看docker 服务的状态 systemctl status docker 3. 停止docker 服务 systemctl stop docker 此时再使用 syst…

群晖NAS+DMS7.0以上版本+无docker机型安装zerotier

测试机型:群晖synology 218play / DSM版本为7.2.1 因218play无法安装docker,且NAS系统已升级为7.0以上版本,按zerotier官网说法无法安装zerotier, 不过还是可以通过ssh终端和命令方式安装zerotier。 1、在DSM新建文件夹 用于存放zerotier脚…

数据链路层(Data Link Layer)

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

听GPT 讲Rust源代码--compiler(28)

File: rust/compiler/rustc_codegen_llvm/src/llvm/mod.rs 文件rust/compiler/rustc_codegen_llvm/src/llvm/mod.rs是Rust编译器的LLVM代码生成模块的一个文件。该文件定义了一些用于与LLVM交互的结构体、枚举和常量。 此文件的主要作用是: 定义编译器和LLVM之间的接…

电脑单机游戏推荐:嗜血印 BLOODY SPELL 中文版

《嗜血印》该游戏的故事发生在一个充满秘密和恐怖的江湖中。一伙自称为“灵虚教”的神秘组织闯入万法归宗门派,导致天下大乱。妹妹小鲤被掳为人质,同门师兄弟相继遭到毒手。当嗜血咒印打开的那一刻,重识自我的苏夜锦,为了守护自己…

理想L9自动驾驶域控制器拆解

理想L9自动驾驶域控制器拆解 目录 理想L9自动驾驶域控制器拆解简介:1、SOC-Orin2、MCU-TC3973、交换机4、GMSL解串芯片5、存储 printf("欢迎关注公众号:车载嵌入式探索者,博主建立了一个车规级开发交流群, 感兴趣的朋友可以关…

Linux习题6

解析:排序必须得是rwx,所以B不对 解析: /etc/resolv.conf:是DNS配置文件。在网卡配置文件中进行配置,默认情况下,网卡配置文件DNS优于/etc/resolv.conf。 /etc/hostname:在centos7,配置主机名…

axure rp9添加当前日期、表单添加到中继器

1.[[Now.toLocaleDateString()]] [[Now.toLocaleTimeString()]]当前日期:2024/1/6 13:19:47 2. 输入内容添加到表单; 3.中继器的序号显示; 4. 中继器中添加按钮以及其他的;在中继器中添加动态面板,放入需要添加的按…

【HarmonyOS】装饰器下的状态管理与页面路由跳转实现

从今天开始,博主将开设一门新的专栏用来讲解市面上比较热门的技术 “鸿蒙开发”,对于刚接触这项技术的小伙伴在学习鸿蒙开发之前,有必要先了解一下鸿蒙,从你的角度来讲,你认为什么是鸿蒙呢?它出现的意义又是…

XML技术分析02

一、XML——树形结构 <?xml version"1.0" encoding"ISO-8859-1"?><note><to>George</to><from>John</from><heading>Reminder</heading><body>Dont forget the meeting!</body> </note…

VMware中删除虚拟机

虚拟机使用完成后&#xff0c;需要删除虚拟机如何操作呢&#xff1f; 1.首先进入VMware 2.选择需要删除的虚拟机&#xff0c;点击右键 3.直接选择“移除”&#xff1f; 当然不是&#xff0c;这只是从这么目录显示中去掉了&#xff0c;并非 “真正” 删除该虚拟机 注意&#x…

【XR806开发板使用】开发环境搭建、Hello工程以及开发事项

XR806开发板试用 很有幸能获得本次技术社区和全志组织的XR806开发板试用活动。之前开发的嵌入式应用都是在Windows平台上进行的&#xff0c;对于Linux下的开发并不熟悉&#xff0c;在社区里看到群友使用官方提供的docker环境进行开发&#xff0c;顺着群友的指导&#xff0c;找…