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

本文涉及的产品
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
RDS MySQL Serverless 高可用系列,价值2615元额度,1个月
简介: 在一些本地化项目开发当中,存在这样一种需求,即开发完成的项目,在第一次部署启动时,需能自行构建系统需要的数据库及其对应的数据库表。

文/朱季谦

 

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

 

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

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

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

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

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

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

 1 @FunctionalInterface

 2publicinterface 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)

 3publicclass WatchStartCommandSqlRunnerImpl implements CommandLineRunner {

 4

 5     @Override

 6     publicvoid run(String... args) throws Exception {

 7         System.out.println("第一个Command执行");

 8     }

 9

10

11 @Component

12 @Order(value = 2)

13publicclass WatchStartCommandSqlRunnerImpl2 implements CommandLineRunner {

14     @Override

15     publicvoid 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连接时,可以先连接它。

代码如下:

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    publicvoid 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对象了。

 1publicclass SqlSessionFactory{

 2     private Connection connection ;

 3     public SqlSessionFactory(Connection connection) {

 4         this.connection = connection;

 5     }

 6 ......

 7 }

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

 1 sqlSessionFactory.schemaOperationsBuild("create");

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

 1publicvoid 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  */

 4publicvoid 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的数据库表——

 1publicvoid executeResource(String operation, String component) {

 2     this.executeSchemaResource(operation, component, this.getDbResource(operation, operation, component), false);

 3 }

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

 1public String getDbResource(String directory, String operation, String component) {

 2     return "static/db/" + directory + "/mysql." + operation + "." + component + ".sql";

 3 }

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

 1publicvoid 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  */

 8privatevoid 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,可直接下载运行:https://github.com/z924931408/Springboot-AutoCreateMySqlTable.git

相关实践学习
如何快速连接云数据库RDS MySQL
本场景介绍如何通过阿里云数据管理服务DMS快速连接云数据库RDS MySQL,然后进行数据表的CRUD操作。
全面了解阿里云能为你做什么
阿里云在全球各地部署高效节能的绿色数据中心,利用清洁计算为万物互联的新世界提供源源不断的能源动力,目前开服的区域包括中国(华北、华东、华南、香港)、新加坡、美国(美东、美西)、欧洲、中东、澳大利亚、日本。目前阿里云的产品涵盖弹性计算、数据库、存储与CDN、分析与搜索、云通信、网络、管理与监控、应用服务、互联网中间件、移动服务、视频服务等。通过本课程,来了解阿里云能够为你的业务带来哪些帮助     相关的阿里云产品:云服务器ECS 云服务器 ECS(Elastic Compute Service)是一种弹性可伸缩的计算服务,助您降低 IT 成本,提升运维效率,使您更专注于核心业务创新。产品详情: https://www.aliyun.com/product/ecs
目录
相关文章
|
26天前
|
XML Java 应用服务中间件
SpringBoot项目打war包流程
本文介绍了将Spring Boot项目改造为WAR包并部署到外部Tomcat服务器的步骤。主要内容包括:1) 修改pom.xml中的打包方式为WAR;2) 排除Spring Boot内置的Tomcat依赖;3) 添加Servlet API依赖;4) 改造启动类以支持WAR部署;5) 打包和部署。通过这些步骤,可以轻松地将Spring Boot应用转换为适合外部Tomcat服务器的WAR包。
124 64
SpringBoot项目打war包流程
|
30天前
基于springboot+thymeleaf+Redis仿知乎网站问答项目源码
基于springboot+thymeleaf+Redis仿知乎网站问答项目源码
130 36
|
10天前
|
SQL 存储 关系型数据库
【SQL技术】不同数据库引擎 SQL 优化方案剖析
不同数据库系统(MySQL、PostgreSQL、Doris、Hive)的SQL优化策略。存储引擎特点、SQL执行流程及常见操作(如条件查询、排序、聚合函数)的优化方法。针对各数据库,索引使用、分区裁剪、谓词下推等技术,并提供了具体的SQL示例。通用的SQL调优技巧,如避免使用`COUNT(DISTINCT)`、减少小文件问题、慎重使用`SELECT *`等。通过合理选择和应用这些优化策略,可以显著提升数据库查询性能和系统稳定性。
64 9
|
21天前
|
自然语言处理 IDE Java
SpringBoot start.aliyun.com创建项目,解决properties乱码的问题
通过确保文件和开发环境的编码一致,配置 Maven 编码,设置 Spring Boot 应用和嵌入式服务器的编码,可以有效解决 properties 文件的乱码问题。以上步骤可以帮助开发者确保在 Spring Boot 项目中正确处理和显示多语言字符,避免因编码问题导致的乱码现象。
35 5
|
24天前
|
JavaScript NoSQL Java
基于SpringBoot+Vue实现的大学生就业服务平台设计与实现(系统源码+文档+数据库+部署等)
面向大学生毕业选题、开题、任务书、程序设计开发、论文辅导提供一站式服务。主要服务:程序设计开发、代码修改、成品部署、支持定制、论文辅导,助力毕设!
63 6
|
24天前
|
JavaScript NoSQL Java
基于SpringBoot+Vue的班级综合测评管理系统设计与实现(系统源码+文档+数据库+部署等)
✌免费选题、功能需求设计、任务书、开题报告、中期检查、程序功能实现、论文辅导、论文降重、答辩PPT辅导、会议视频一对一讲解代码等✌
40 4
|
24天前
|
JavaScript NoSQL Java
基于SpringBoot+Vue实现的大学生体质测试管理系统设计与实现(系统源码+文档+数据库+部署)
面向大学生毕业选题、开题、任务书、程序设计开发、论文辅导提供一站式服务。主要服务:程序设计开发、代码修改、成品部署、支持定制、论文辅导,助力毕设!
36 2
|
28天前
|
前端开发 Java 数据库连接
Java后端开发-使用springboot进行Mybatis连接数据库步骤
本文介绍了使用Java和IDEA进行数据库操作的详细步骤,涵盖从数据库准备到测试类编写及运行的全过程。主要内容包括: 1. **数据库准备**:创建数据库和表。 2. **查询数据库**:验证数据库是否可用。 3. **IDEA代码配置**:构建实体类并配置数据库连接。 4. **测试类编写**:编写并运行测试类以确保一切正常。
52 2
|
1月前
|
存储 缓存 NoSQL
云端问道21期方案教学-应对高并发,利用云数据库 Tair(兼容 Redis®*)缓存实现极速响应
云端问道21期方案教学-应对高并发,利用云数据库 Tair(兼容 Redis®*)缓存实现极速响应
|
24天前
|
JavaScript NoSQL Java
基于SpringBoot+Vue实现的冬奥会科普平台设计与实现(系统源码+文档+数据库+部署)
面向大学生毕业选题、开题、任务书、程序设计开发、论文辅导提供一站式服务。主要服务:程序设计开发、代码修改、成品部署、支持定制、论文辅导,助力毕设!
41 0

热门文章

最新文章