Spring?Boot實(shí)現(xiàn)第一次啟動(dòng)時(shí)自動(dòng)初始化數(shù)據(jù)庫(kù)流程詳解
在現(xiàn)在的后端開發(fā)中,只要是使用關(guān)系型數(shù)據(jù)庫(kù),相信SSM架構(gòu)(Spring Boot + MyBatis)已經(jīng)成為首選。
不過在我們第一次運(yùn)行或者部署項(xiàng)目的時(shí)候,通常要先手動(dòng)連接數(shù)據(jù)庫(kù),執(zhí)行一個(gè)SQL文件以創(chuàng)建數(shù)據(jù)庫(kù)以及數(shù)據(jù)庫(kù)表格完成數(shù)據(jù)庫(kù)的初始化工作,這樣我們的SSM應(yīng)用程序才能夠正常工作。
這樣也對(duì)實(shí)際部署或者是容器化造成了一些麻煩,必須先手動(dòng)初始化數(shù)據(jù)庫(kù)再啟動(dòng)應(yīng)用程序。
那能不能讓我們的SSM應(yīng)用程序第一次啟動(dòng)時(shí),自動(dòng)地幫我們執(zhí)行SQL文件以完成數(shù)據(jù)庫(kù)初始化工作呢?
這樣事實(shí)上是沒問題的,下面就以Spring Boot + MyBatis為例,使用MySQL作為數(shù)據(jù)庫(kù),完成上述的數(shù)據(jù)庫(kù)初始化功能。
1,整體思路
我們可以編寫一個(gè)配置類,在一個(gè)標(biāo)注了@PostConstruct
注解的方法中編寫初始化數(shù)據(jù)庫(kù)的邏輯,這樣應(yīng)用程序啟動(dòng)時(shí),就會(huì)執(zhí)行該方法幫助我們完成數(shù)據(jù)庫(kù)的初始化工作。
那么這個(gè)初始化數(shù)據(jù)庫(kù)的邏輯大概是什么呢?可以總結(jié)為如下步驟:
- 首先嘗試連接用戶配置的地址,若連接拋出異常說明地址中指定的數(shù)據(jù)庫(kù)不存在,需要?jiǎng)?chuàng)建數(shù)據(jù)庫(kù)并初始化數(shù)據(jù),否則就不需要初始化,直接退出初始化邏輯
- 若要執(zhí)行初始化,首先重新組裝用戶配置的連接地址,使得本次連接不再是連接至具體的數(shù)據(jù)庫(kù),并執(zhí)行
create database
語(yǔ)句完成數(shù)據(jù)庫(kù)創(chuàng)建 - 創(chuàng)建完成數(shù)據(jù)庫(kù)后,再次使用用戶配置的連接地址,這時(shí)數(shù)據(jù)庫(kù)創(chuàng)建完成就可以成功連接上了!這時(shí)再執(zhí)行SQL文件初始化表格即可
上述邏輯中大家可以會(huì)有下列的疑問:
- 第一步中,為什么連接拋出異常說明地址中指定的數(shù)據(jù)庫(kù)不存在?
- 第二步中,什么是 “使得本次連接不再是連接至具體的數(shù)據(jù)庫(kù)” ?
假設(shè)用戶配置的連接地址是jdbc:mysql://127.0.0.1:3306/init_demo
,相信這個(gè)大家非常熟悉了,它表示:連接的MySQL地址是127.0.0.1
,端口是3306
,并且連接到該MySQL中名為init_demo
的數(shù)據(jù)庫(kù)中。
那么如果MySQL中init_demo
的庫(kù)并不存在,Spring Boot還嘗試連接上述地址的話,就會(huì)拋出SQLException
異常:
所以在這里可以將是否拋出SQLException
異常作為判斷應(yīng)用程序是否是第一次部署啟動(dòng)的條件。
好的,既然數(shù)據(jù)庫(kù)不存在,我們就要?jiǎng)?chuàng)建數(shù)據(jù)庫(kù),但是上述地址連接不上啊!怎么創(chuàng)建呢?
正是因?yàn)樯鲜龅刂分兄付艘B接的具體數(shù)據(jù)庫(kù),而數(shù)據(jù)庫(kù)又不存在,才會(huì)連接失敗,那能不能連接時(shí)不指定數(shù)據(jù)庫(kù),僅僅是連接到MySQL上就行呢?當(dāng)然可以,我們將上述的連接地址改成:jdbc:mysql://127.0.0.1:3306/
,就可以連接成功了!
不過通常SSM應(yīng)用程序中,配置數(shù)據(jù)庫(kù)地址都是要指定庫(kù)名的,因此我們待會(huì)在配置類編寫初始化數(shù)據(jù)庫(kù)邏輯時(shí),重新組裝一下用戶給的配置連接地址即可,即把jdbc:mysql://127.0.0.1:3306/init_demo
通過代碼處理成jdbc:mysql://127.0.0.1:3306/
并發(fā)起連接即可,這就是上述說的第二步。
第二步完成了數(shù)據(jù)庫(kù)的創(chuàng)建,第三步就是完成表格創(chuàng)建了!表格創(chuàng)建就寫在SQL文件里即可,由于數(shù)據(jù)庫(kù)創(chuàng)建好了,我們?cè)诘谌街杏挚梢灾匦率褂糜脩艚o的配置地址jdbc:mysql://127.0.0.1:3306/init_demo
再次連接并執(zhí)行SQL文件完成初始化了!
上述步驟中,我們將使用JDBC自帶的接口完成數(shù)據(jù)庫(kù)連接等等,而不是使用MyBatis的SqlSessionFactory
,因?yàn)槲覀兊诙叫枰淖冞B接地址。
下面,我們就來實(shí)現(xiàn)一下。
2,具體實(shí)現(xiàn)
首先是在本地或者其它地方搭建好MySQL服務(wù)器,這里就不再贅述怎么去搭建MySQL了。
我這里在本地搭建了MySQL服務(wù)器,下面通過Spring Boot進(jìn)行連接。
(1) 創(chuàng)建應(yīng)用程序并配置
首先創(chuàng)建一個(gè)Spring Boot應(yīng)用程序,并集成好MySQL驅(qū)動(dòng)和MyBatis支持,我這里的依賴如下:
<!-- Spring Web --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- MyBatis --> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>3.0.2</version> </dependency> <!-- MySQL連接支持 --> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope> </dependency> <!-- Hutool實(shí)用工具 --> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>5.8.16</version> </dependency> <!-- Lombok注解 --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <scope>provided</scope> </dependency> <!-- Spring Boot測(cè)試 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency>
然后在配置文件application.yml
中加入下列配置:
# 數(shù)據(jù)庫(kù)配置 spring: datasource: url: "jdbc:mysql://127.0.0.1:3306/init_demo?serverTimezone=GMT%2B8" username: "swsk33" password: "dev-2333"
這就是正常的數(shù)據(jù)庫(kù)連接配置,不再過多講述。我這里使用yaml
格式配置文件,大家也可以使用properties
格式的配置文件。
(2) 編寫配置類完成數(shù)據(jù)庫(kù)的檢測(cè)和初始化邏輯
這里先給出這個(gè)配置類的代碼:
package com.gitee.swsk33.sqlinitdemo.config; import cn.hutool.core.io.resource.ClassPathResource; import jakarta.annotation.PostConstruct; import lombok.extern.slf4j.Slf4j; import org.apache.ibatis.jdbc.ScriptRunner; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Configuration; import java.io.BufferedReader; import java.io.FileInputStream; import java.io.InputStream; import java.io.InputStreamReader; import java.net.URI; import java.net.URISyntaxException; import java.nio.charset.StandardCharsets; import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; import java.sql.Statement; /** * 用于第一次啟動(dòng)時(shí),初始化數(shù)據(jù)庫(kù)的配置類 */ @Slf4j @Configuration public class DatabaseInitialize { /** * 讀取連接地址 */ @Value("${spring.datasource.url}") private String url; /** * 讀取用戶名 */ @Value("${spring.datasource.username}") private String username; /** * 讀取密碼 */ @Value("${spring.datasource.password}") private String password; /** * 檢測(cè)當(dāng)前連接的庫(kù)是否存在(連接URL中的數(shù)據(jù)庫(kù)) * * @return 當(dāng)前連接的庫(kù)是否存在 */ private boolean currentDatabaseExists() { // 嘗試以配置文件中的URL建立連接 try { Connection connection = DriverManager.getConnection(url, username, password); connection.close(); } catch (SQLException e) { // 若連接拋出異常則說明連接URL中指定數(shù)據(jù)庫(kù)不存在 return false; } // 正常情況下說明連接URL中數(shù)據(jù)庫(kù)存在 return true; } /** * 執(zhí)行SQL腳本 * * @param path SQL腳本文件的路徑 * @param isClasspath SQL腳本路徑是否是classpath路徑 * @param connection 數(shù)據(jù)庫(kù)連接對(duì)象,通過這個(gè)連接執(zhí)行腳本 */ private void runSQLScript(String path, boolean isClasspath, Connection connection) { try (InputStream sqlFileStream = isClasspath ? new ClassPathResource(path).getStream() : new FileInputStream(path)) { BufferedReader sqlFileStreamReader = new BufferedReader(new InputStreamReader(sqlFileStream, StandardCharsets.UTF_8)); // 創(chuàng)建SQL腳本執(zhí)行器對(duì)象 ScriptRunner scriptRunner = new ScriptRunner(connection); // 使用SQL腳本執(zhí)行器對(duì)象執(zhí)行腳本 scriptRunner.runScript(sqlFileStreamReader); // 最后關(guān)閉文件讀取器 sqlFileStreamReader.close(); } catch (Exception e) { log.error("讀取文件或者執(zhí)行腳本失敗!"); e.printStackTrace(); } } /** * 執(zhí)行SQL腳本以創(chuàng)建數(shù)據(jù)庫(kù) */ private void createDatabase() { try { // 修改連接語(yǔ)句,重新建立連接 // 重新建立的連接不再連接到指定庫(kù),而是直接連接到整個(gè)MySQL // 使用URI類解析并拆解連接地址,重新組裝 URI databaseURI = new URI(url.replace("jdbc:", "")); // 得到連接地址中的數(shù)據(jù)庫(kù)平臺(tái)名(例如mysql) String databasePlatform = databaseURI.getScheme(); // 得到連接地址和端口 String hostAndPort = databaseURI.getAuthority(); // 得到連接地址中的庫(kù)名 String databaseName = databaseURI.getPath().substring(1); // 組裝新的連接URL,不連接至指定庫(kù) String newURL = "jdbc:" + databasePlatform + "://" + hostAndPort + "/"; // 重新建立連接 Connection connection = DriverManager.getConnection(newURL, username, password); Statement statement = connection.createStatement(); // 執(zhí)行SQL語(yǔ)句創(chuàng)建數(shù)據(jù)庫(kù) statement.execute("create database if not exists `" + databaseName + "`"); // 關(guān)閉會(huì)話和連接 statement.close(); connection.close(); log.info("創(chuàng)建數(shù)據(jù)庫(kù)完成!"); } catch (URISyntaxException e) { log.error("數(shù)據(jù)庫(kù)連接URL格式錯(cuò)誤!"); throw new RuntimeException(e); } catch (SQLException e) { log.error("連接失??!"); throw new RuntimeException(e); } } /** * 該方法用于檢測(cè)數(shù)據(jù)庫(kù)是否需要初始化,如果是則執(zhí)行SQL腳本進(jìn)行初始化操作 */ @PostConstruct private void initDatabase() { log.info("開始檢查數(shù)據(jù)庫(kù)是否需要初始化..."); // 檢測(cè)當(dāng)前連接數(shù)據(jù)庫(kù)是否存在 if (currentDatabaseExists()) { log.info("數(shù)據(jù)庫(kù)存在,不需要初始化!"); return; } log.warn("數(shù)據(jù)庫(kù)不存在!準(zhǔn)備執(zhí)行初始化步驟..."); // 先創(chuàng)建數(shù)據(jù)庫(kù) createDatabase(); // 然后再次連接,執(zhí)行腳本初始化庫(kù)中的表格 try (Connection connection = DriverManager.getConnection(url, username, password)) { runSQLScript("/create-table.sql", true, connection); log.info("初始化表格完成!"); } catch (Exception e) { log.error("初始化表格時(shí),連接數(shù)據(jù)庫(kù)失??!"); e.printStackTrace(); } } }
上述代碼中,有下列要點(diǎn):
- 我們使用
@Value
注解讀取了配置文件中數(shù)據(jù)庫(kù)的連接信息,包括連接地址、用戶名和密碼 - 上述
currentDatabaseExists
方法用于嘗試使用配置的地址進(jìn)行連接,如果拋出SQLException
異常則判斷配置的地址中,指定的數(shù)據(jù)庫(kù)是不存在的,這里的代碼主要是實(shí)現(xiàn)了上述初始化邏輯中的第一步 - 上述
createDatabase
方法用于重新組裝用戶的連接地址,使其不再是連接到指定數(shù)據(jù)庫(kù),然后執(zhí)行SQL語(yǔ)句完成數(shù)據(jù)庫(kù)的創(chuàng)建,我們使用Java的URI
類解析用戶配置的連接地址,便于我們拆分然后組裝連接地址,并獲取用戶要使用的數(shù)據(jù)庫(kù)名,對(duì)其進(jìn)行創(chuàng)建,這里的代碼實(shí)現(xiàn)了上述初始化邏輯中的第二步 - 上述
initDatabase
方法是會(huì)被自動(dòng)執(zhí)行的,它調(diào)用了currentDatabaseExists
和createDatabase
方法,組合起來所有的步驟,在其中完成了第一步和第二步后,重新使用用戶配置的地址發(fā)起連接并執(zhí)行SQL腳本以初始化表,這個(gè)方法包含了上述初始化邏輯中的第三步 - 上述
runSQLScript
方法用于連接數(shù)據(jù)庫(kù)后執(zhí)行SQL腳本,其中ScriptRunner
類是由MyBatis提供的運(yùn)行SQL腳本的實(shí)用類,其構(gòu)造函數(shù)需要傳入JDBC的數(shù)據(jù)庫(kù)連接對(duì)象Connection
對(duì)象,然后上述我還設(shè)定了形參isClasspath
,可以讓用戶自定義是讀取文件系統(tǒng)中的SQL腳本還是classpath
中的SQL腳本
上述的初始化表格腳本位于工程目錄的src/main/resources/create-table.sql
,即classpath
中,內(nèi)容如下:
-- 初始化表格前先刪除 drop table if exists `user`; -- 創(chuàng)建表格 create table `user` ( `id` int unsigned auto_increment, `username` varchar(16) not null, `password` varchar(32) not null, primary key (`id`) ) engine = InnoDB default charset = utf8mb4;
好的,現(xiàn)在先保證MySQL數(shù)據(jù)庫(kù)中不存在init_demo
的庫(kù),啟動(dòng)程序試試:
可見成功地完成了數(shù)據(jù)庫(kù)的檢測(cè)、初始化工作,也可見ScriptRunner
在執(zhí)行SQL的時(shí)候會(huì)在控制臺(tái)輸出執(zhí)行的語(yǔ)句。
現(xiàn)在再重新啟動(dòng)一下程序試試:
可見第二次啟動(dòng)時(shí),名為init_demo
的數(shù)據(jù)庫(kù)已經(jīng)存在了,這時(shí)就不需要執(zhí)行初始化邏輯了!
(3) 如果有的Bean初始化時(shí)需要訪問數(shù)據(jù)庫(kù)
假設(shè)現(xiàn)在有一個(gè)類,在初始化為Bean的時(shí)候需要訪問數(shù)據(jù)庫(kù),例如:
// 省略package和import /** * 啟動(dòng)時(shí)需要查詢數(shù)據(jù)庫(kù)的Beans */ @Slf4j @Component public class UserServiceDemo { @Autowired private UserDAO userDAO; @PostConstruct private void init() { log.info("執(zhí)行數(shù)據(jù)庫(kù)測(cè)試訪問..."); userDAO.add(new User(0, "用戶名", "密碼")); List<User> users = userDAO.getAll(); for (User user : users) { System.out.println(user); } } }
這個(gè)類在被初始化為Bean的時(shí)候,就需要訪問數(shù)據(jù)庫(kù)進(jìn)行讀寫操作,那問題來了,如果這個(gè)類UserServiceDemo
在上述數(shù)據(jù)庫(kù)初始化類DatabaseInitialize
之前被初始化了怎么辦呢?這會(huì)導(dǎo)致數(shù)據(jù)庫(kù)還沒有被初始化時(shí),UserServiceDemo
就去訪問數(shù)據(jù)庫(kù),導(dǎo)致初始化失敗。
這時(shí),我們可以使用@DependsOn
注解,這個(gè)注解可以控制UserServiceDemo
在DatabaseInitialize
初始化之后再進(jìn)行初始化:
@Slf4j @Component // 使用@DependsOn注解表示當(dāng)前類依賴于名為databaseInitialize的Bean // 這樣可以使得databaseInitialize這個(gè)Bean(我們的數(shù)據(jù)庫(kù)檢查類)先被初始化,并執(zhí)行完成數(shù)據(jù)庫(kù)初始化后再初始化本類,以順利訪問數(shù)據(jù)庫(kù) @DependsOn("databaseInitialize") public class UserServiceDemo { // 省略這個(gè)類的內(nèi)容 }
在這里我們?cè)?code>UserServiceDemo上標(biāo)注了注解@DependsOn
,并傳入databaseInitialize
作為參數(shù),表示UserServiceDemo
這個(gè)類是依賴于名(id)為databaseInitialize
的Bean的,這樣Spring Boot就會(huì)在DatabaseInitialize
初始化之后再初始化UserServiceDemo
。
標(biāo)注了
@Component
等等的類,默認(rèn)情況下被初始化為Bean的時(shí)候,其名稱是其類名的小駝峰形式,例如上述的DatabaseInitialize
類,初始化為Bean時(shí)名字默認(rèn)為databaseInitialize
,因此上述@DependsOn
注解就傳入databaseInitialize
。
現(xiàn)在刪除init_demo
庫(kù),再次啟動(dòng)應(yīng)用程序:
可見在初始化數(shù)據(jù)庫(kù)后,又成功地在啟動(dòng)時(shí)訪問了數(shù)據(jù)庫(kù)。
3,總結(jié)
本文以Spring Boot + Mybatis為例,使用MySQL數(shù)據(jù)庫(kù),實(shí)現(xiàn)了SSM應(yīng)用程序第一次啟動(dòng)時(shí)自動(dòng)檢測(cè)并完成數(shù)據(jù)庫(kù)初始化的功能,理論上上述方式適用于所有的關(guān)系型數(shù)據(jù)庫(kù),大家稍作修改即可。
本文僅僅是我自己提供的思路,以及部分內(nèi)容也是和“機(jī)器朋友”交流后的結(jié)果,如果大家對(duì)此有更好的思路,歡迎在評(píng)論區(qū)提出您的建議。
以上就是Spring Boot實(shí)現(xiàn)第一次啟動(dòng)時(shí)自動(dòng)初始化數(shù)據(jù)庫(kù)流程詳解的詳細(xì)內(nèi)容,更多關(guān)于Spring Boot初始化數(shù)據(jù)庫(kù)的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
- SpringBoot熱部署啟動(dòng)關(guān)閉流程詳解
- SpringBoot啟動(dòng)流程SpringApplication準(zhǔn)備階段源碼分析
- SpringBoot自定義啟動(dòng)器Starter流程詳解
- SpringBoot超詳細(xì)分析啟動(dòng)流程
- Spring?Boot面試必問之啟動(dòng)流程知識(shí)點(diǎn)詳解
- Springboot2.6.x的啟動(dòng)流程與自動(dòng)配置詳解
- springboot中swagger快速啟動(dòng)流程
- SpringBoot中如何啟動(dòng)Tomcat流程
- Spring Boot 啟動(dòng)流程解析
相關(guān)文章
在springboot中使用注解將值注入?yún)?shù)的操作
這篇文章主要介紹了在springboot中使用注解將值注入?yún)?shù)的操作,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2020-04-04spring的xml文件打開沒有namespace等操作選項(xiàng)的解決方案
這篇文章主要介紹了spring的xml文件打開沒有namespace等操作選項(xiàng)的解決方案,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-09-09mybatis sum(參數(shù)) 列名作為參數(shù)的問題
這篇文章主要介紹了mybatis sum(參數(shù)) 列名作為參數(shù)的問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-01-01