Springboot項目啓動後自動建立多表關聯的數據庫與表的方案

文/朱季謙html

 

熬夜寫完,尚有不足,但仍在努力學習與總結中,而您的點贊與關注,是對我最大的鼓勵!mysql

 

在一些項目開發當中,存在這樣一種需求,即開發完成的項目,在第一次部署啓動時,需能自行構建系統須要的數據庫及其對應的數據庫表。git

若要解決這類需求,其實如今已有很多開源框架都能實現自動生成數據庫表,如mybatis plus、spring JPA等,但您是否有想過,若要自行構建一套可以在系統第一次啓動時自行構建多表關聯等更爲複雜的表結構時,須要如何才能實現呢?github

我在前面寫過一篇 Activiti工做流學習筆記(三)——自動生成28張數據庫表的底層原理分析 ,裏面分析過工做流Activiti自動構建28數據庫表的底層原理。在我看來,學習開源框架的底層原理,其中一個緣由是,須從中學到能爲我所用的東西,故而,在分析理解完 工做流自動構建28數據庫表的底層原理以後,我決定也寫一個基於Springboot框架的自行建立數據庫與表的demo。我參考了工做流Activiti6.0版本的底層建表實現的邏輯,基於Springboot框架,實現項目在第一次啓動時可自動構建各類複雜如多表關聯等形式的數據庫與表的。spring

總體實現思路並不複雜,大概是這樣:先設計一套完整建立多表關聯的數據庫sql腳本,放到resource裏,在springboot啓動過程當中,自動執行sql腳本。sql

首先,先一次性設計一套可行的多表關聯數據庫腳本,這裏我主要參考使用Activiti自帶的表作實現案例,由於它內部設計了衆多表關聯,就不額外設計了。數據庫

sql腳本的語句就是日常的create建表語句,相似以下:springboot

 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;

增長外部主鍵、索引——mybatis

 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 * 二、分行讀取"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

相關文章
相關標籤/搜索