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

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

文/朱季谦

 

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

 

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

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

相关实践学习
如何在云端创建MySQL数据库
开始实验后,系统会自动创建一台自建MySQL的 源数据库 ECS 实例和一台 目标数据库 RDS。
全面了解阿里云能为你做什么
阿里云在全球各地部署高效节能的绿色数据中心,利用清洁计算为万物互联的新世界提供源源不断的能源动力,目前开服的区域包括中国(华北、华东、华南、香港)、新加坡、美国(美东、美西)、欧洲、中东、澳大利亚、日本。目前阿里云的产品涵盖弹性计算、数据库、存储与CDN、分析与搜索、云通信、网络、管理与监控、应用服务、互联网中间件、移动服务、视频服务等。通过本课程,来了解阿里云能够为你的业务带来哪些帮助     相关的阿里云产品:云服务器ECS 云服务器 ECS(Elastic Compute Service)是一种弹性可伸缩的计算服务,助您降低 IT 成本,提升运维效率,使您更专注于核心业务创新。产品详情: https://www.aliyun.com/product/ecs
目录
相关文章
|
19天前
|
Java 数据库连接 测试技术
SpringBoot入门(4) - 添加内存数据库H2
SpringBoot入门(4) - 添加内存数据库H2
38 4
SpringBoot入门(4) - 添加内存数据库H2
|
13天前
|
缓存 关系型数据库 MySQL
高并发架构系列:数据库主从同步的 3 种方案
本文详解高并发场景下数据库主从同步的三种解决方案:数据主从同步、数据库半同步复制、数据库中间件同步和缓存记录写key同步,旨在帮助解决数据一致性问题。关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。
高并发架构系列:数据库主从同步的 3 种方案
|
21天前
|
Java 数据库连接 测试技术
SpringBoot入门(4) - 添加内存数据库H2
SpringBoot入门(4) - 添加内存数据库H2
29 2
SpringBoot入门(4) - 添加内存数据库H2
|
13天前
|
Java 数据库连接 测试技术
SpringBoot入门(4) - 添加内存数据库H2
SpringBoot入门(4) - 添加内存数据库H2
53 13
|
8天前
|
Java 数据库连接 测试技术
SpringBoot入门(4) - 添加内存数据库H2
SpringBoot入门(4) - 添加内存数据库H2
23 4
|
9天前
|
XML Java 数据库连接
SpringBoot集成Flowable:打造强大的工作流管理系统
在企业级应用开发中,工作流管理是一个核心组件,它能够帮助我们定义、执行和管理业务流程。Flowable是一个开源的工作流和业务流程管理(BPM)平台,它提供了强大的工作流引擎和建模工具。结合SpringBoot,我们可以快速构建一个高效、灵活的工作流管理系统。本文将探讨如何将Flowable集成到SpringBoot应用中,并展示其强大的功能。
32 1
|
23天前
|
SQL Java 数据库
Spring Boot与Flyway:数据库版本控制的自动化实践
【10月更文挑战第19天】 在软件开发中,数据库的版本控制是一个至关重要的环节,它确保了数据库结构的一致性和项目的顺利迭代。Spring Boot结合Flyway提供了一种自动化的数据库版本控制解决方案,极大地简化了数据库迁移管理。本文将详细介绍如何使用Spring Boot和Flyway实现数据库版本的自动化控制。
23 2
|
24天前
|
SQL JavaScript 关系型数据库
node博客小项目:接口开发、连接mysql数据库
【10月更文挑战第14天】node博客小项目:接口开发、连接mysql数据库
|
9天前
|
XML 存储 Java
SpringBoot集成Flowable:构建强大的工作流引擎
在企业级应用开发中,工作流管理是核心功能之一。Flowable是一个开源的工作流引擎,它提供了BPMN 2.0规范的实现,并且与SpringBoot框架完美集成。本文将探讨如何使用SpringBoot和Flowable构建一个强大的工作流引擎,并分享一些实践技巧。
26 0
|
10天前
|
存储 安全 Java
springboot当中ConfigurationProperties注解作用跟数据库存入有啥区别
`@ConfigurationProperties`注解和数据库存储配置信息各有优劣,适用于不同的应用场景。`@ConfigurationProperties`提供了类型安全和模块化的配置管理方式,适合静态和简单配置。而数据库存储配置信息提供了动态更新和集中管理的能力,适合需要频繁变化和集中管理的配置需求。在实际项目中,可以根据具体需求选择合适的配置管理方式,或者结合使用这两种方式,实现灵活高效的配置管理。
10 0