Spring Boot实现第一次启动时自动初始化数据库

本文涉及的产品
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
云数据库 RDS MySQL,集群版 2核4GB 100GB
推荐场景:
搭建个人博客
云数据库 RDS MySQL,高可用版 2核4GB 50GB
简介: 本文以Spring Boot + Mybatis为例,使用MySQL数据库,实现了SSM应用程序第一次启动时自动检测并完成数据库初始化的功能,理论上上述方式适用于所有的关系型数据库,大家稍作修改即可。

在现在的后端开发中,只要是使用关系型数据库,相信SSM架构(Spring Boot + MyBatis)已经成为首选。

不过在我们第一次运行或者部署项目的时候,通常要先手动连接数据库,执行一个SQL文件以创建数据库以及数据库表格完成数据库的初始化工作,这样我们的SSM应用程序才能够正常工作。

这样也对实际部署或者是容器化造成了一些麻烦,必须先手动初始化数据库再启动应用程序。

那能不能让我们的SSM应用程序第一次启动时,自动地帮我们执行SQL文件以完成数据库初始化工作呢?

这样事实上是没问题的,今天就以Spring Boot + MyBatis为例,使用MySQL作为数据库,完成上述的数据库初始化功能。

1,整体思路

我们可以编写一个配置类,在一个标注了@PostConstruct注解的方法中编写初始化数据库的逻辑,这样应用程序启动时,就会执行该方法帮助我们完成数据库的初始化工作。

那么这个初始化数据库的逻辑大概是什么呢?可以总结为如下步骤:

  1. 首先尝试连接用户配置的地址,若连接抛出异常SQLException并且异常代码为1049时,则说明地址中指定的数据库不存在,需要创建数据库并初始化数据,否则就不需要初始化,直接退出初始化逻辑
  2. 若要执行初始化,首先重新组装用户配置的连接地址,使得本次连接不再是连接至具体的数据库,并执行create database语句完成数据库创建
  3. 创建完成数据库后,再次使用用户配置的连接地址,这时数据库创建完成就可以成功连接上了!这时再执行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异常:

image.png

这里抛出的异常消息是Unknown database,其异常代码是1049,可以通过异常对象的getErrorCode方法查看异常代码。

所以在这里可以将是否抛出SQLException异常并且代码是否为1049作为判断应用程序是否是第一次部署启动的条件。

好的,既然数据库不存在,我们就要创建数据库,但是上述地址连接不上啊!怎么创建呢?

正是因为上述地址中指定了要连接的具体数据库,而数据库又不存在,才会连接失败,那能不能连接时不指定数据库,仅仅是连接到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,因为我们第二步需要改变连接地址。

下面,我们就来实现一下。

2,具体实现

首先是在本地或者其它地方搭建好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) {
            // 若连接抛出异常且错误代码为1049,则说明连接URL中指定数据库不存在
            if (e.getErrorCode() == 1049) {
                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();
        }
    }

    /**
     * 创建数据库
     */
    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异常且代码为1049,则判断配置的地址中指定的数据库是不存在的,这里的代码主要是实现了上述初始化逻辑中的第一步
  • 上述createDatabase方法用于重新组装用户的连接地址,使其不再是连接到指定数据库,然后执行SQL语句完成数据库的创建,我们使用Java的URI类解析用户配置的连接地址,便于我们拆分然后组装连接地址,并获取用户要使用的数据库名,对其进行创建,这里的代码实现了上述初始化逻辑中的第二步
  • 上述initDatabase方法是会被自动执行的,它调用了currentDatabaseExistscreateDatabase方法,组合起来所有的步骤,在其中完成了第一步和第二步后,重新使用用户配置的地址发起连接并执行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的库,启动程序试试:

image.png

可见成功地完成了数据库的检测、初始化工作,也可见ScriptRunner在执行SQL的时候会在控制台输出执行的语句。

现在再重新启动一下程序试试:

image.png

可见第二次启动时,名为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注解,这个注解可以控制UserServiceDemoDatabaseInitialize初始化之后再进行初始化:

@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库,再次启动应用程序:

image.png

可见在初始化数据库后,又成功地在启动时访问了数据库。

3,总结

本文以Spring Boot + Mybatis为例,使用MySQL数据库,实现了SSM应用程序第一次启动时自动检测并完成数据库初始化的功能,理论上上述方式适用于所有的关系型数据库,大家稍作修改即可。

本文仅仅是我自己提供的思路,以及部分内容也是和“机器朋友”交流后的结果,如果大家对此有更好的思路,欢迎在评论区提出您的建议。

相关实践学习
如何在云端创建MySQL数据库
开始实验后,系统会自动创建一台自建MySQL的 源数据库 ECS 实例和一台 目标数据库 RDS。
全面了解阿里云能为你做什么
阿里云在全球各地部署高效节能的绿色数据中心,利用清洁计算为万物互联的新世界提供源源不断的能源动力,目前开服的区域包括中国(华北、华东、华南、香港)、新加坡、美国(美东、美西)、欧洲、中东、澳大利亚、日本。目前阿里云的产品涵盖弹性计算、数据库、存储与CDN、分析与搜索、云通信、网络、管理与监控、应用服务、互联网中间件、移动服务、视频服务等。通过本课程,来了解阿里云能够为你的业务带来哪些帮助 &nbsp; &nbsp; 相关的阿里云产品:云服务器ECS 云服务器 ECS(Elastic Compute Service)是一种弹性可伸缩的计算服务,助您降低 IT 成本,提升运维效率,使您更专注于核心业务创新。产品详情: https://www.aliyun.com/product/ecs
相关文章
|
11月前
|
Java 关系型数据库 MySQL
Spring Boot实现第一次启动时自动初始化数据库
在现在的后端开发中,只要是使用关系型数据库,相信SSM架构(Spring Boot + MyBatis)已经成为首选。 不过在我们第一次运行或者部署项目的时候,通常要先手动连接数据库,执行一个SQL文件以创建数据库以及数据库表格完成数据库的初始化工作,这样我们的SSM应用程序才能够正常工作。 这样也对实际部署或者是容器化造成了一些麻烦,必须先手动初始化数据库再启动应用程序。 那能不能让我们的SSM应用程序第一次启动时,自动地帮我们执行SQL文件以完成数据库初始化工作呢? 这样事实上是没问题的,今天就以Spring Boot + MyBatis为例,使用MySQL作为数据库,完成上述的数
|
2月前
|
存储 安全 Java
数据库轻松切换:解读Spring中的AbstractRoutingDataSource
数据库轻松切换:解读Spring中的AbstractRoutingDataSource
83 0
|
3月前
|
Java 测试技术 数据库
SpringBoot启动时设置不加载数据库
SpringBoot启动时设置不加载数据库
243 0
|
9月前
|
缓存 Java 数据库
Springboot项目启动时加载数据库数据到内存
Springboot项目启动时加载数据库数据到内存
121 0
|
9月前
|
缓存 NoSQL Java
springboot 启动加载数据库数据到redis缓存
springboot 启动加载数据库数据到redis缓存
320 0
|
Java 数据库连接 数据库
SpringBoot2学习(四):数据库场景的自动配置与整合
SpringBoot2学习(四):数据库场景的自动配置与整合
SpringBoot2学习(四):数据库场景的自动配置与整合
|
Java 数据库 Spring
spring中bean配置8.0版本数据库(使用属性文件配置)
spring中bean配置8.0版本数据库(使用属性文件配置)
|
Java 数据库 Spring
spring中bean配置8.0版本数据库,时区解决问题!
spring中bean配置8.0版本数据库,时区解决问题!
|
Java Spring 容器
Spring Boot 启动时自动执行代码的几种方式。。
Spring Boot 启动时自动执行代码的几种方式。。
519 0
Spring Boot 启动时自动执行代码的几种方式。。
|
Java 关系型数据库 MySQL
Spring boot 数据源未配置异常
Spring boot 数据源未配置异常
190 0