解決Springboot項目啟動后自動創(chuàng)建多表關(guān)聯(lián)的數(shù)據(jù)庫與表的方案
熬夜寫完,尚有不足,但仍在努力學習與總結(jié)中,而您的點贊與關(guān)注,是對我最大的鼓勵!
在一些本地化項目開發(fā)當中,存在這樣一種需求,即開發(fā)完成的項目,在第一次部署啟動時,需能自行構(gòu)建系統(tǒng)需要的數(shù)據(jù)庫及其對應的數(shù)據(jù)庫表。
若要解決這類需求,其實現(xiàn)在已有不少開源框架都能實現(xiàn)自動生成數(shù)據(jù)庫表,如mybatis plus、spring JPA等,但您是否有想過,若要自行構(gòu)建一套更為復雜的表結(jié)構(gòu)時,這種開源框架是否也能滿足呢,若滿足不了話,又該如何才能實現(xiàn)呢?
我在前面寫過一篇 Activiti工作流學習筆記(三)——自動生成28張數(shù)據(jù)庫表的底層原理分析 ,里面分析過工作流Activiti自動構(gòu)建28數(shù)據(jù)庫表的底層原理。在我看來,學習開源框架的底層原理,其中一個原因是,須從中學到能為我所用的東西。故而,在分析理解完工作流自動構(gòu)建28數(shù)據(jù)庫表的底層原理之后,我決定也寫一個基于Springboot框架的自行創(chuàng)建數(shù)據(jù)庫與表的demo。我參考了工作流Activiti6.0版本的底層建表實現(xiàn)的邏輯,基于Springboot框架,實現(xiàn)項目在第一次啟動時可自動構(gòu)建各種復雜如多表關(guān)聯(lián)等形式的數(shù)據(jù)庫與表的。
整體實現(xiàn)思路并不復雜,大概是這樣:先設(shè)計一套完整創(chuàng)建多表關(guān)聯(lián)的數(shù)據(jù)庫sql腳本,放到resource里,在springboot啟動過程中,自動執(zhí)行sql腳本。
首先,先一次性設(shè)計一套可行的多表關(guān)聯(lián)數(shù)據(jù)庫腳本,這里我主要參考使用Activiti自帶的表做實現(xiàn)案例,因為它內(nèi)部設(shè)計了眾多表關(guān)聯(lián),就不額外設(shè)計了。
sql腳本的語句就是平常的create建表語句,類似如下:
create table ACT_PROCDEF_INFO ( ID_ varchar(64) not null, PROC_DEF_ID_ varchar(64) not null, REV_ integer, INFO_JSON_ID_ varchar(64), primary key (ID_) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE utf8_bin;
增加外部主鍵、索引——
create index ACT_IDX_INFO_PROCDEF on ACT_PROCDEF_INFO(PROC_DEF_ID_); alter table ACT_PROCDEF_INFO add constraint ACT_FK_INFO_JSON_BA foreign key (INFO_JSON_ID_) references ACT_GE_BYTEARRAY (ID_); alter table ACT_PROCDEF_INFO add constraint ACT_FK_INFO_PROCDEF foreign key (PROC_DEF_ID_) references ACT_RE_PROCDEF (ID_); alter table ACT_PROCDEF_INFO add constraint ACT_UNIQ_INFO_PROCDEF unique (PROC_DEF_ID_);
整體就是設(shè)計一套符合符合需求場景的sql語句,保存在.sql的腳本文件里,最后統(tǒng)一存放在resource目錄下,類似如下:

接下來,就是實現(xiàn)CommandLineRunner的接口,重寫其run()的bean回調(diào)方法,在run方法里開發(fā)能自動建庫與建表邏輯的功能。
目前,我已將開發(fā)的demo上傳到了我的github,感興趣的童鞋,可自行下載,目前能直接下下來在本地環(huán)境運行,可根據(jù)自己的實際需求針對性參考使用。
首先,在解決這類需求時,第一個先要解決的地方是,Springboot啟動后如何實現(xiàn)只執(zhí)行一次建表方法。
這里需要用到一個CommandLineRunner接口,這是Springboot自帶的,實現(xiàn)該接口的類,其重寫的run方法,會在Springboot啟動完成后自動執(zhí)行,該接口源碼如下:
@FunctionalInterface
public interface CommandLineRunner {
/**
*用于運行bean的回調(diào)
*/
void run(String... args) throws Exception;
}
擴展一下,在Springboot中,可以定義多個實現(xiàn)CommandLineRunner接口類,并且可以對這些實現(xiàn)類中進行排序,只需要增加@Order,其重寫的run方法就可以按照順序執(zhí)行,代碼案例驗證:
@Component
@Order(value=1)
public class WatchStartCommandSqlRunnerImpl implements CommandLineRunner {
@Override
public void run(String... args) throws Exception {
System.out.println("第一個Command執(zhí)行");
}
@Component
@Order(value = 2)
public class WatchStartCommandSqlRunnerImpl2 implements CommandLineRunner {
@Override
public void run(String... args) throws Exception {
System.out.println("第二個Command執(zhí)行");
}
}
控制臺打印的信息如下:
第一個Command執(zhí)行
第二個Command執(zhí)行
根據(jù)以上的驗證,因此,我們可以通過實現(xiàn)CommandLineRunner的接口,重寫其run()的bean回調(diào)方法,用于在Springboot啟動后實現(xiàn)只執(zhí)行一次建表方法。實現(xiàn)項目啟動建表的功能,可能還需實現(xiàn)判斷是否已經(jīng)有相應數(shù)據(jù)庫,若無,則應先新建一個數(shù)據(jù)庫,同時,得考慮還沒有對應數(shù)據(jù)庫的情況,因此,我們通過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軟件連接后,先創(chuàng)建一個Statement對象,該對象是jdbc中可用于執(zhí)行靜態(tài) SQL 語句并返回它所生成結(jié)果的對象,這里可以使用它來執(zhí)行查找?guī)炫c創(chuàng)建庫的作用。
//創(chuàng)建Statement對象
Statement statment=conn.createStatement();
/**
使用statment的查詢方法executeQuery("show databases like \"fte\"")
檢查MySql是否有fte這個數(shù)據(jù)庫
**/
ResultSet resultSet=statment.executeQuery("show databases like \"fte\"");
//若resultSet.next()為true,證明已存在;
//若false,證明還沒有該庫,則執(zhí)行statment.executeUpdate("create database fte")創(chuàng)建庫
if(resultSet.next()){
log.info("數(shù)據(jù)庫已經(jīng)存在");
}else {
log.info("數(shù)據(jù)庫未存在,先創(chuàng)建fte數(shù)據(jù)庫");
if(statment.executeUpdate("create database fte")==1){
log.info("新建數(shù)據(jù)庫成功");
}
}
在數(shù)據(jù)庫fte自動創(chuàng)建完成后,就可以在該fte庫里去做建表的操作了。
我將建表的相關(guān)方法都封裝到SqlSessionFactory類里,相關(guān)建表方法同樣需要用到j(luò)dbc的Connection連接到數(shù)據(jù)庫,因此,需要把已連接的Connection引用變量當做參數(shù)傳給SqlSessionFactory的初始構(gòu)造函數(shù):
public void createTable(Connection conn,Statement stat) throws SQLException {
try {
String url="jdbc:mysql://127.0.0.1:3306/fte?useUnicode=true&characterEncoding=UTF-8&ueSSL=false&serverTimezone=GMT%2B8";
conn=DriverManager.getConnection(url,"root","root");
SqlSessionFactory sqlSessionFactory=new SqlSessionFactory(conn);
sqlSessionFactory.schemaOperationsBuild("create");
} catch (SQLException e) {
e.printStackTrace();
}finally {
stat.close();
conn.close();
}
}
初始化new SqlSessionFactory(conn)后,就可以在該對象里使用已進行連接操作的Connection對象了。
public class SqlSessionFactory{
private Connection connection ;
public SqlSessionFactory(Connection connection) {
this.connection = connection;
}
......
}
這里傳參可以有兩種情況,即“create”代表創(chuàng)建表結(jié)構(gòu)的功能,“drop”代表刪除表結(jié)構(gòu)的功能:
sqlSessionFactory.schemaOperationsBuild("create");
進入到這個方法里,會先做一個判斷——
public void schemaOperationsBuild(String type) {
switch (type){
case "drop":
this.dbSchemaDrop();break;
case "create":
this.dbSchemaCreate();break;
}
}
若是this.dbSchemaCreate(),執(zhí)行建表操作:
/**
* 新增數(shù)據(jù)庫表
*/
public void dbSchemaCreate() {
if (!this.isTablePresent()) {
log.info("開始執(zhí)行create操作");
this.executeResource("create", "act");
log.info("執(zhí)行create完成");
}
}
this.executeResource("create", "act")代表創(chuàng)建表名為act的數(shù)據(jù)庫表——
public void executeResource(String operation, String component) {
this.executeSchemaResource(operation, component, this.getDbResource(operation, operation, component), false);
}
其中 this.getDbResource(operation, operation, component)是獲取sql腳本的路徑,進入到方法里,可見——
public String getDbResource(String directory, String operation, String component) {
return "static/db/" + directory + "/mysql." + operation + "." + component + ".sql";
}
接下來,讀取路徑下的sql腳本,生成輸入流字節(jié)流:
public void executeSchemaResource(String operation, String component, String resourceName, boolean isOptional) {
InputStream inputStream = null;
try {
//讀取sql腳本數(shù)據(jù)
inputStream = IoUtil.getResourceAsStream(resourceName);
if (inputStream == null) {
if (!isOptional) {
log.error("resource '" + resourceName + "' is not available");
return;
}
} else {
this.executeSchemaResource(operation, component, resourceName, inputStream);
}
} finally {
IoUtil.closeSilently(inputStream);
}
}
最后,整個執(zhí)行sql腳本的核心實現(xiàn)在this.executeSchemaResource(operation, component, resourceName, inputStream)方法里——
/**
* 執(zhí)行sql腳本
* @param operation
* @param component
* @param resourceName
* @param inputStream
*/
private void executeSchemaResource(String operation, String component, String resourceName, InputStream inputStream) {
//sql語句拼接字符串
String sqlStatement = null;
Object exceptionSqlStatement = null;
try {
/**
* 1.jdbc連接mysql數(shù)據(jù)庫
*/
Connection connection = this.connection;
Exception exception = null;
/**
* 2、分行讀取"static/db/create/mysql.create.act.sql"里的sql腳本數(shù)據(jù)
*/
byte[] bytes = IoUtil.readInputStream(inputStream, resourceName);
/**
* 3.將sql文件里數(shù)據(jù)分行轉(zhuǎn)換成字符串,換行的地方,用轉(zhuǎn)義符“\n”來代替
*/
String ddlStatements = new String(bytes);
/**
* 4.以字符流形式讀取字符串數(shù)據(jù)
*/
BufferedReader reader = new BufferedReader(new StringReader(ddlStatements));
/**
* 5.根據(jù)字符串中的轉(zhuǎn)義符“\n”分行讀取
*/
String line = IoUtil.readNextTrimmedLine(reader);
/**
* 6.循環(huán)讀取的每一行
*/
for(boolean inOraclePlsqlBlock = false; line != null; line = IoUtil.readNextTrimmedLine(reader)) {
/**
* 7.若下一行l(wèi)ine還有數(shù)據(jù),證明還沒有全部讀取,仍可執(zhí)行讀取
*/
if (line.length() > 0) {
/**
8.在沒有拼接夠一個完整建表語句時,!line.endsWith(";")會為true,
即一直循環(huán)進行拼接,當遇到";"就跳出該if語句
**/
if ((!line.endsWith(";") || inOraclePlsqlBlock) && (!line.startsWith("/") || !inOraclePlsqlBlock)) {
sqlStatement = this.addSqlStatementPiece(sqlStatement, line);
} else {
/**
9.循環(huán)拼接中若遇到符號";",就意味著,已經(jīng)拼接形成一個完整的sql建表語句,例如
create table ACT_GE_PROPERTY (
NAME_ varchar(64),
VALUE_ varchar(300),
REV_ integer,
primary key (NAME_)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE utf8_bin
這樣,就可以先通過代碼來將該建表語句執(zhí)行到數(shù)據(jù)庫中,實現(xiàn)如下:
**/
if (inOraclePlsqlBlock) {
inOraclePlsqlBlock = false;
} else {
sqlStatement = this.addSqlStatementPiece(sqlStatement, line.substring(0, line.length() - 1));
}
/**
* 10.將建表語句字符串包裝成Statement對象
*/
Statement jdbcStatement = connection.createStatement();
try {
/**
* 11.最后,執(zhí)行建表語句到數(shù)據(jù)庫中
*/
log.info("SQL: {}", sqlStatement);
jdbcStatement.execute(sqlStatement);
jdbcStatement.close();
} catch (Exception var27) {
log.error("problem during schema {}, statement {}", new Object[]{operation, sqlStatement, var27});
} finally {
/**
* 12.到這一步,意味著上一條sql建表語句已經(jīng)執(zhí)行結(jié)束,
* 若沒有出現(xiàn)錯誤話,這時已經(jīng)證明第一個數(shù)據(jù)庫表結(jié)構(gòu)已經(jīng)創(chuàng)建完成,
* 可以開始拼接下一條建表語句,
*/
sqlStatement = null;
}
}
}
}
if (exception != null) {
throw exception;
}
} catch (Exception var29) {
log.error("couldn't " + operation + " db schema: " + exceptionSqlStatement, var29);
}
}
這部分代碼主要功能是,先用字節(jié)流形式讀取sql腳本里的數(shù)據(jù),轉(zhuǎn)換成字符串,其中有換行的地方用轉(zhuǎn)義符“/n”來代替。接著把字符串轉(zhuǎn)換成字符流BufferedReader形式讀取,按照“/n”符合來劃分每一行的讀取,循環(huán)將讀取的每行字符串進行拼接,當循環(huán)到某一行遇到“;”時,就意味著已經(jīng)拼接成一個完整的create建表語句,類似這樣形式——
create table ACT_PROCDEF_INFO ( ID_ varchar(64) not null, PROC_DEF_ID_ varchar(64) not null, REV_ integer, INFO_JSON_ID_ varchar(64), primary key (ID_) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE utf8_bin;
這時,就可以先將拼接好的create建表字符串,通過 jdbcStatement.execute(sqlStatement)語句來執(zhí)行入庫了。當執(zhí)行成功時,該ACT_PROCDEF_INFO表就意味著已經(jīng)創(chuàng)建成功,接著以BufferedReader字符流形式繼續(xù)讀取下一行,進行下一個數(shù)據(jù)庫表結(jié)構(gòu)的構(gòu)建。
整個過程大概就是這個邏輯,可以在此基礎(chǔ)上,針對更為復雜的建表結(jié)構(gòu)sql語句進行設(shè)計,在項目啟動時,自行執(zhí)行相應的sql語句,來進行建表。
該demo代碼已經(jīng)上傳git,可直接下載運行:https://github.com/z924931408/Springboot-AutoCreateMySqlTable.git
到此這篇關(guān)于解決Springboot項目啟動后自動創(chuàng)建多表關(guān)聯(lián)的數(shù)據(jù)庫與表的方案的文章就介紹到這了,更多相關(guān)Springboot創(chuàng)建多表關(guān)聯(lián)的數(shù)據(jù)庫與表內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
在IDEA中創(chuàng)建跑得起來的Springboot項目
這篇文章主要介紹了在IDEA中創(chuàng)建跑得起來的Springboot項目的圖文教程,需要的朋友可以參考下2018-04-04
SpringBoot整合MybatisPlus實現(xiàn)增刪改查功能
MybatisPlus是國產(chǎn)的第三方插件,?它封裝了許多常用的CURDapi,免去了我們寫mapper.xml的重復勞動。本文將整合MybatisPlus實現(xiàn)增刪改查功能,感興趣的可以了解一下2022-05-05
在Spring?Boot使用Undertow服務(wù)的方法
Undertow是RedHAT紅帽公司開源的產(chǎn)品,采用JAVA開發(fā),是一款靈活,高性能的web服務(wù)器,提供了NIO的阻塞/非阻塞API,也是Wildfly的默認Web容器,這篇文章給大家介紹了在Spring?Boot使用Undertow服務(wù)的方法,感興趣的朋友跟隨小編一起看看吧2023-05-05

