在现在的后端开发中,只要是使用关系型数据库,相信 SSM 架构(Spring Boot + MyBatis)已经成为首选。
不过在我们第一次运行或者部署项目的时候,通常要先手动连接数据库,执行一个 SQL 文件以创建数据库以及数据库表格完成数据库的初始化工作,这样我们的 SSM 应用程序才能够正常工作。
这样也对实际部署或者是容器化造成了一些麻烦,必须先手动初始化数据库再启动应用程序。
那能不能让我们的 SSM 应用程序第一次启动时,自动地帮我们执行 SQL 文件以完成数据库初始化工作呢?
这样事实上是没问题的,今天就以 Spring Boot + MyBatis 为例,使用 MySQL 作为数据库,完成上述的数据库初始化功能。
一、整体思路
我们可以编写一个配置类,在一个标注了@PostConstruct
注解的方法中编写初始化数据库的逻辑,这样应用程序启动时,就会执行该方法帮助我们完成数据库的初始化工作。
那么这个初始化数据库的逻辑大概是什么呢?可以总结为如下步骤:
首先尝试连接用户配置的地址,若连接抛出异常说明地址中指定的数据库不存在,需要创建数据库并初始化数据,否则就不需要初始化,直接退出初始化逻辑
若要执行初始化,首先重新组装用户配置的连接地址,使得本次连接不再是连接至具体的数据库,并执行create database
语句完成数据库创建
创建完成数据库后,再次使用用户配置的连接地址,这时数据库创建完成就可以成功连接上了!这时再执行 SQL 文件初始化表格即可
上述逻辑中大家可以会有下列的疑问:
假设用户配置的连接地址是jdbc:mysql://127.0.0.1:3306/init_demo
,相信这个大家非常熟悉了,它表示:连接的 MySQL 地址是127.0.0.1
,端口是3306
,并且连接到该 MySQL 中名为init_demo
的数据库中。
那么如果 MySQL 中init_demo
的库并不存在,Spring Boot 还尝试连接上述地址的话,就会抛出SQLException
异常:
所以在这里可以将是否抛出SQLException
异常作为判断应用程序是否是第一次部署启动的条件。
好的,既然数据库不存在,我们就要创建数据库,但是上述地址连接不上啊!怎么创建呢?
正是因为上述地址中指定了要连接的具体数据库,而数据库又不存在,才会连接失败,那能不能连接时不指定数据库,仅仅是连接到 MySQL 上就行呢?当然可以,我们将上述的连接地址改成:jdbc:mysql://127.0.0.1:3306/
,就可以连接成功了!
不过通常 SSM 应用程序中,配置数据库地址都是要指定库名的,因此我们待会在配置类编写初始化数据库逻辑时,重新组装一下用户给的配置连接地址即可,即把jdbc:mysql://127.0.0.1:3306/init_demo
通过代码处理成jdbc:mysql://127.0.0.1:3306/
并发起连接即可,这就是上述说的第二步。
第二步完成了数据库的创建,第三步就是完成表格创建了!表格创建就写在 SQL 文件里即可,由于数据库创建好了,我们在第三步中又可以重新使用用户给的配置地址jdbc:mysql://127.0.0.1:3306/init_demo
再次连接并执行 SQL 文件完成初始化了!
上述步骤中,我们将使用 JDBC 自带的接口完成数据库连接等等,而不是使用 MyBatis 的SqlSessionFactory
,因为我们第二步需要改变连接地址。
下面,我们就来实现一下。
二、具体实现
首先是在本地或者其它地方搭建好 MySQL 服务器,这里就不再赘述怎么去搭建 MySQL 了。
我这里在本地搭建了 MySQL 服务器,下面通过 Spring Boot 进行连接。
1、创建应用程序并配置
首先创建一个 Spring Boot 应用程序,并集成好 MySQL 驱动和 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实用工具 -->
<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测试 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
复制代码
然后在配置文件application.yml
中加入下列配置:
# 数据库配置
spring:
datasource:
url: "jdbc:mysql://127.0.0.1:3306/init_demo?serverTimezone=GMT%2B8"
username: "swsk33"
password: "dev-2333"
复制代码
这就是正常的数据库连接配置,不再过多讲述。我这里使用yaml
格式配置文件,大家也可以使用properties
格式的配置文件。
2、编写配置类完成数据库的检测和初始化逻辑
这里先给出这个配置类的代码:
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;
/**
* 用于第一次启动时,初始化数据库的配置类
*/
@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;
/**
* 检测当前连接的库是否存在(连接URL中的数据库)
*
* @return 当前连接的库是否存在
*/
private boolean currentDatabaseExists() {
// 尝试以配置文件中的URL建立连接
try {
Connection connection = DriverManager.getConnection(url, username, password);
connection.close();
} catch (SQLException e) {
// 若连接抛出异常则说明连接URL中指定数据库不存在
return false;
}
// 正常情况下说明连接URL中数据库存在
return true;
}
/**
* 执行SQL脚本
*
* @param path SQL脚本文件的路径
* @param isClasspath SQL脚本路径是否是classpath路径
* @param connection 数据库连接对象,通过这个连接执行脚本
*/
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));
// 创建SQL脚本执行器对象
ScriptRunner scriptRunner = new ScriptRunner(connection);
// 使用SQL脚本执行器对象执行脚本
scriptRunner.runScript(sqlFileStreamReader);
// 最后关闭文件读取器
sqlFileStreamReader.close();
} catch (Exception e) {
log.error("读取文件或者执行脚本失败!");
e.printStackTrace();
}
}
/**
* 执行SQL脚本以创建数据库
*/
private void createDatabase() {
try {
// 修改连接语句,重新建立连接
// 重新建立的连接不再连接到指定库,而是直接连接到整个MySQL
// 使用URI类解析并拆解连接地址,重新组装
URI databaseURI = new URI(url.replace("jdbc:", ""));
// 得到连接地址中的数据库平台名(例如mysql)
String databasePlatform = databaseURI.getScheme();
// 得到连接地址和端口
String hostAndPort = databaseURI.getAuthority();
// 得到连接地址中的库名
String databaseName = databaseURI.getPath().substring(1);
// 组装新的连接URL,不连接至指定库
String newURL = "jdbc:" + databasePlatform + "://" + hostAndPort + "/";
// 重新建立连接
Connection connection = DriverManager.getConnection(newURL, username, password);
Statement statement = connection.createStatement();
// 执行SQL语句创建数据库
statement.execute("create database if not exists `" + databaseName + "`");
// 关闭会话和连接
statement.close();
connection.close();
log.info("创建数据库完成!");
} catch (URISyntaxException e) {
log.error("数据库连接URL格式错误!");
throw new RuntimeException(e);
} catch (SQLException e) {
log.error("连接失败!");
throw new RuntimeException(e);
}
}
/**
* 该方法用于检测数据库是否需要初始化,如果是则执行SQL脚本进行初始化操作
*/
@PostConstruct
private void initDatabase() {
log.info("开始检查数据库是否需要初始化...");
// 检测当前连接数据库是否存在
if (currentDatabaseExists()) {
log.info("数据库存在,不需要初始化!");
return;
}
log.warn("数据库不存在!准备执行初始化步骤...");
// 先创建数据库
createDatabase();
// 然后再次连接,执行脚本初始化库中的表格
try (Connection connection = DriverManager.getConnection(url, username, password)) {
runSQLScript("/create-table.sql", true, connection);
log.info("初始化表格完成!");
} catch (Exception e) {
log.error("初始化表格时,连接数据库失败!");
e.printStackTrace();
}
}
}
复制代码
上述代码中,有下列要点:
我们使用@Value
注解读取了配置文件中数据库的连接信息,包括连接地址、用户名和密码
上述currentDatabaseExists
方法用于尝试使用配置的地址进行连接,如果抛出SQLException
异常则判断配置的地址中,指定的数据库是不存在的,这里的代码主要是实现了上述初始化逻辑中的第一步
上述createDatabase
方法用于重新组装用户的连接地址,使其不再是连接到指定数据库,然后执行 SQL 语句完成数据库的创建,我们使用 Java 的URI
类解析用户配置的连接地址,便于我们拆分然后组装连接地址,并获取用户要使用的数据库名,对其进行创建,这里的代码实现了上述初始化逻辑中的第二步
上述initDatabase
方法是会被自动执行的,它调用了currentDatabaseExists
和createDatabase
方法,组合起来所有的步骤,在其中完成了第一步和第二步后,重新使用用户配置的地址发起连接并执行 SQL 脚本以初始化表,这个方法包含了上述初始化逻辑中的第三步
上述runSQLScript
方法用于连接数据库后执行 SQL 脚本,其中ScriptRunner
类是由 MyBatis 提供的运行 SQL 脚本的实用类,其构造函数需要传入 JDBC 的数据库连接对象Connection
对象,然后上述我还设定了形参isClasspath
,可以让用户自定义是读取文件系统中的 SQL 脚本还是classpath
中的 SQL 脚本
上述的初始化表格脚本位于工程目录的src/main/resources/create-table.sql
,即classpath
中,内容如下:
-- 初始化表格前先删除
drop table if exists `user`;
-- 创建表格
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;
复制代码
好的,现在先保证 MySQL 数据库中不存在init_demo
的库,启动程序试试:
可见成功地完成了数据库的检测、初始化工作,也可见ScriptRunner
在执行 SQL 的时候会在控制台输出执行的语句。
现在再重新启动一下程序试试:
可见第二次启动时,名为init_demo
的数据库已经存在了,这时就不需要执行初始化逻辑了!
3、如果有的 Bean 初始化时需要访问数据库
假设现在有一个类,在初始化为 Bean 的时候需要访问数据库,例如:
// 省略package和import
/**
* 启动时需要查询数据库的Beans
*/
@Slf4j
@Component
public class UserServiceDemo {
@Autowired
private UserDAO userDAO;
@PostConstruct
private void init() {
log.info("执行数据库测试访问...");
userDAO.add(new User(0, "用户名", "密码"));
List<User> users = userDAO.getAll();
for (User user : users) {
System.out.println(user);
}
}
}
复制代码
这个类在被初始化为 Bean 的时候,就需要访问数据库进行读写操作,那问题来了,如果这个类UserServiceDemo
在上述数据库初始化类DatabaseInitialize
之前被初始化了怎么办呢?这会导致数据库还没有被初始化时,UserServiceDemo
就去访问数据库,导致初始化失败。
这时,我们可以使用@DependsOn
注解,这个注解可以控制UserServiceDemo
在DatabaseInitialize
初始化之后再进行初始化:
@Slf4j
@Component
// 使用@DependsOn注解表示当前类依赖于名为databaseInitialize的Bean
// 这样可以使得databaseInitialize这个Bean(我们的数据库检查类)先被初始化,并执行完成数据库初始化后再初始化本类,以顺利访问数据库
@DependsOn("databaseInitialize")
public class UserServiceDemo {
// 省略这个类的内容
}
复制代码
在这里我们在UserServiceDemo
上标注了注解@DependsOn
,并传入databaseInitialize
作为参数,表示UserServiceDemo
这个类是依赖于名(id)为databaseInitialize
的 Bean 的,这样 Spring Boot 就会在DatabaseInitialize
初始化之后再初始化UserServiceDemo
。
标注了@Component
等等的类,默认情况下被初始化为 Bean 的时候,其名称是其类名的小驼峰形式,例如上述的DatabaseInitialize
类,初始化为 Bean 时名字默认为databaseInitialize
,因此上述@DependsOn
注解就传入databaseInitialize
。
现在删除init_demo
库,再次启动应用程序:
可见在初始化数据库后,又成功地在启动时访问了数据库。
三、总结
本文以 Spring Boot + Mybatis 为例,使用 MySQL 数据库,实现了 SSM 应用程序第一次启动时自动检测并完成数据库初始化的功能,理论上上述方式适用于所有的关系型数据库,大家稍作修改即可。
本文仅仅是我自己提供的思路,以及部分内容也是和“机器朋友”交流后的结果,如果大家对此有更好的思路,欢迎在评论区提出您的建议。
作者:守望时空 33
链接:https://juejin.cn/post/7238522776055103544
来源:稀土掘金
评论