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_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
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连接时,可以先连接它。
代码如下:
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,可直接下载运行:https://github.com/z924931408/Springboot-AutoCreateMySqlTable.git
Springboot项目启动后自动创建多表关联的数据库与表的方案的更多相关文章
- SpringBoot项目启动后自动打开浏览器
编写一个类,注册为Spring的Bean,然后实现CommandLineRunner接口,重写run()方法即可 @Component public class OpenBrowser impleme ...
- Springboot 项目启动后执行某些自定义代码
Springboot 项目启动后执行某些自定义代码 Springboot给我们提供了两种"开机启动"某些方法的方式:ApplicationRunner和CommandLineRun ...
- Springboot项目启动后访问不到Controller
1.搭建一个简单的Springboot项目,最开始将启动类main函数与controller放到一个类里,可以正常启动和访问,但是将两个分开再启动时访问就会报错:This application ha ...
- springboot项目启动后tomcat服务器自动关闭 解决方法
需要在pom.xml中添加 <dependency> <groupId>org.springframework.boot</groupId> <artifac ...
- springboot项目启动无法访问到controller原因之一:引导类位置有问题
新建的springboot项目启动后,无法访问到controller 页面是404错误 查看项目结构,发现是新建工程的启动类位置有问题,controller类应该位于引导类的同级包或者子级包中.需要将 ...
- springboot项目启动-自动创建数据表
很多时候,我们部署一个项目的时候,需要创建大量的数据表.例如mysql,一般的方法就是通过source命令完成数据表的移植,如:source /root/test.sql.如果我们需要一个项目启动后, ...
- SpringBoot启动后自动打开浏览器访问项目
之前我们用SSM或者SSH进行JAVA WEB开发的时候,IDEA 需要配置Tomcat然后把项目放到tomcat运行,tomcat启动的时候会自动打开浏览器去访问项目,但是SpringBoot是内嵌 ...
- springboot 学习之路 9 (项目启动后就执行特定方法)
目录:[持续更新.....] spring 部分常用注解 spring boot 学习之路1(简单入门) spring boot 学习之路2(注解介绍) spring boot 学习之路3( 集成my ...
- springboot项目启动成功后执行一段代码的两种方式
springboot项目启动成功后执行一段代码的两种方式 实现ApplicationRunner接口 package com.lnjecit.lifecycle; import org.springf ...
随机推荐
- 2019牛客多校第六场H Pair(数位DP 多个数相关)题解
题意: 传送门 给你\(A,B,C\),要求你给出有多少对\((x, y)\)满足\(x\in [1,A],y\in [1,B]\),且满足以下任意一个条件:\(x \& y > C\) ...
- 翻译:《实用的Python编程》01_02_Hello_world
目录 | 上一节 (1.1 Python) | 下一节 (1.3 数字) 1.2 第一个程序 本节讨论有关如何创建一个程序.运行解释器和调试的基础知识. 运行 Python Python 程序始终在解 ...
- Subresource Integrity,SRI,Cross-Origin Resource Sharing (CORS),子资源的完整性检查,Subresource Integrity checking,CORS,Ajax
SRI https://code.jquery.com/ SRI是一种新的W3C规范,它允许Web开发人员,以确保托管在第三方服务器上的资源是没有被篡改的.SRI的使用,建议作为最佳实践,每当库从第三 ...
- ES6 Set vs ES5 Array
ES6 Set vs ES5 Array Set https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Ob ...
- taro & querySelector & refs
taro & querySelector & refs delayQuerySelector https://github.com/NervJS/taro-ui/blob/dev/sr ...
- NGK底层技术如何助力SPC子币VAST高价与安全并行?
NGK近来使用了新的侧链技术推出了新的SPC侧链代币,以及SPC的子币VAST---维萨币. NGK使用去中心化和开源区块链数据分布式协议,不断打造高倍币,力求成为生态建设参与者们所信赖的高倍币孵化器 ...
- 智能合约稳定币USDN的价值在哪里?
近几年来,区块链和数字货币市场快速发展,客观上需要价格相对稳定的交易媒介和贮藏手段,从而推动以链上资产或链下资产抵押型稳定币和算法型稳定币出现,以实现币价相对稳定的数字货币.市场上开始出现了诸如USD ...
- PBN离场定高转弯保护区插件发布测试
昨天2月29日,是四年才有一次的日子,本想着应该写点什么,但一测试发现还有问题,只能先放下. 今天是三月份的第一天,一年已经过去了六分之一.疫情的关系,原本并不紧急的工作,现在也开始积压的有些多了,时 ...
- 教你玩转CSS border(边框)
边框样式 边框样式属性指定要显示什么样的边界. border-style属性用来定义边框的样式 border-style的值 代码演示: <!DOCTYPE html> <html ...
- [转]什么是 C 和 C ++ 标准库?
转载地址:https://www.cnblogs.com/findumars/p/9000371.html 简要介绍编写C/C ++应用程序的领域,标准库的作用以及它是如何在各种操作系统中实现的.我已 ...