【SpringBoot 系列】03 - 测试与数据库操作

配置属性与数据

获取配置文件属性

在前面我们有学习过,在配置文件添加属性,使用 @ConfigurationProperties 获取配置文件属性的值,并且通过实体类来封装这些数据,实体类需要有 set 方法来进行自动注入。

使用实体类封装配置属性

使用实体类来封装配置文件的属性需要以下三个步骤:

  1. 定义属性
  2. 创建实体类封装属性
  3. 在实体类绑定属性并生成 get、set 方法
  4. 定义实体类为 Bean 对象 (这样实体类才可以被 IOC 容器管理注入信息)

定义属性

比如我们在配置文件添加如下几个属性:

yaml
1
2
3
student:
name: 小码同学
major: 计算机科学与技术

封装属性

然后再使用一个实体类来封装这些信息

java
1
2
3
4
5
6
7
8
9
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;

@Data // 自动生成get、set方法
@ConfigurationProperties(prefix = "student") // 绑定配置文件的属性
public class Student {
private String name;
private String major;
}

这里的 @Data 用到了 lombok 依赖。

xml
1
2
3
4
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>

打印属性内容

我们在 SpringBoot 入口直接打印出来,就不写测试类了,你用测试类打印也是没有问题的。

java
1
2
3
4
5
6
7
8
9
10
11
12
13
@EnableConfigurationProperties(Student.class) //指定Student类为bean,并绑定属性
@SpringBootApplication
public class Springboot08ConfigurationApplication {

public static void main(String[] args) {
ConfigurableApplicationContext run = SpringApplication.run(Springboot08ConfigurationApplication.class, args);
System.out.println("-------封装实体类----------");
Student student = run.getBean(Student.class);
System.out.println(student.getName());
System.out.println(student.getMajor());
System.out.println("-----------------");
}
}

当我们在程序入口输出 student 的属性时,我们还要在当前的类使用 @EnableConfigurationProperties(Student.class) 注解指定 Student 实体类为 Bean 对象。

或者你也可以不使用使用 @EnableConfigurationProperties(Student.class) 注解,你直接在 Student 实体类上使用 @Component 注解,标注当前实体类时一个 bean 对象也是可以的,但推荐使用 @EnableConfigurationProperties 注解,因为这样可以更加清晰的知道你在哪一个类或方法使用了这个属性。

image-20230502163156158

出现这个提示后只需要添加一个坐标此提醒就消失了

xml
1
2
3
4
5
<!--        取消提醒未配置注解处理器提醒 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
</dependency>

属性自动注入第三方 Bean

我们前面说的都是使用自定义的 bean 来封装属性的,如果我们使用第三方 bean 能不能加载属性呢?能不能自动装配到第三方的的属性里面?为什么会提出这个疑问?原因就在于当前 @ConfigurationProperties 注解是写在类定义的上方,而第三方开发的 bean 源代码不是你自己书写的,你也不可能到源代码中去添加 @ConfigurationProperties 注解,这种问题该怎么解决呢?

定义属性

我们继续在 SpringBoot 配置文件定义一下属性:

yaml
1
2
3
4
source:
username: root
password: root
driverClassName: com.mysql.jdbc.Driver

创建第三方 Bean 进行属性绑定

引入依赖

xml
1
2
3
4
5
6
7
8
9
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.20</version>
</dependency>

我们在 SpringBoot 程序入口编写代码:

java
1
2
3
4
5
6
@Bean
@ConfigurationProperties(prefix = "source")
public DruidDataSource dataSource(){
DruidDataSource source = new DruidDataSource();
return source;
}

定义 DruidDataSource 是一个 Bean 对象,同时,还要使用 @ConfigurationProperties 注解来绑定属性,这样的话,当你在配置文件中定义属性,IOC 容器就会自动将属性注入到 DruidDataSource 中

打印属性内容

我们仍然在程序入口打印内容,这样就可以实现第三方获取属性的内容了。

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@SpringBootApplication
public class Springboot08ConfigurationApplication {
@Bean
@ConfigurationProperties(prefix = "source")
public DruidDataSource dataSource(){
DruidDataSource source = new DruidDataSource();
return source;
}

public static void main(String[] args) {
ConfigurableApplicationContext run = SpringApplication.run(Springboot08ConfigurationApplication.class, args);

DruidDataSource bean = run.getBean(DruidDataSource.class);
System.out.println("--------第三方Bean---------");
System.out.println(bean.getDriverClassName());
System.out.println(bean.getUsername());
System.out.println(bean.getPassword());
System.out.println("-----------------");
}
}

宽松绑定 / 松散绑定

定义属性

在进行属性绑定时,可能会遇到如下情况,为了进行标准命名,开发者会将属性名严格按照驼峰命名法书写,在 yml 配置文件中将 datasource 修改为 dataSource,如下:

yaml
1
2
dataSource:
driverClassName: com.mysql.jdbc.Driver

定义第三方 Bean

此时程序可以正常运行,然后又将代码中的前缀 datasource 修改为 dataSource,如下:

java
1
2
3
4
5
6
@Bean
@ConfigurationProperties(prefix = "dataSource")
public DruidDataSource datasource(){
DruidDataSource ds = new DruidDataSource();
return ds;
}

运行错误

此时就发生了编译错误,而且并不是 idea 工具导致的,运行后依然会出现问题,配置属性名 dataSource 是无效的

cmd
1
2
3
4
5
6
7
8
Configuration property name 'dataSource' is not valid:

Invalid characters: 'S'
Bean: datasource
Reason: Canonical names should be kebab-case ('-' separated), lowercase alpha-numeric characters and must start with a letter

Action:
Modify 'dataSource' so that it conforms to the canonical names requirements.

为什么会出现这种问题,这就要来说一说 springboot 进行属性绑定时的一个重要知识点了,有关属性名称的宽松绑定,也可以称为宽松绑定。

什么是宽松绑定?实际上是 springboot 进行编程时人性化设计的一种体现,即配置文件中的命名格式与变量名的命名格式可以进行格式上的最大化兼容。兼容到什么程度呢?几乎主流的命名格式都支持,例如:

在 ServerConfig 中的 ipAddress 属性名

java
1
2
3
4
5
6
@Component
@Data
@ConfigurationProperties(prefix = "servers")
public class ServerConfig {
private String ipAddress;
}

可以与下面的配置属性名规则全兼容

yml
1
2
3
4
5
servers:
ipAddress: 192.168.0.2 # 驼峰模式
ip_address: 192.168.0.2 # 下划线模式
ip-address: 192.168.0.2 # 烤肉串模式
IP_ADDRESS: 192.168.0.2 # 常量模式

也可以说,以上 4 种模式最终都可以匹配到 ipAddress 这个属性名。为什么这样呢?原因就是在进行匹配时,配置中的名称要去掉中划线和下划线后,忽略大小写的情况下去与 java 代码中的属性名进行忽略大小写的等值匹配,以上 4 种命名去掉下划线中划线忽略大小写后都是一个词 ipaddress,java 代码中的属性名忽略大小写后也是 ipaddress,这样就可以进行等值匹配了,这就是为什么这 4 种格式都能匹配成功的原因。不过 springboot 官方推荐使用烤肉串模式,也就是中划线模式。

到这里我们掌握了一个知识点,就是命名的规范问题。再来看开始出现的编程错误信息

cmd
1
2
3
4
5
6
7
8
Configuration property name 'dataSource' is not valid:

Invalid characters: 'S'
Bean: datasource
Reason: Canonical names should be kebab-case ('-' separated), lowercase alpha-numeric characters and must start with a letter

Action:
Modify 'dataSource' so that it conforms to the canonical names requirements.

其中 Reason 描述了报错的原因,规范的名称应该是烤肉串 (kebab) 模式 (case),即使用 - 分隔,使用小写字母数字作为标准字符,且必须以字母开头

常用计量单位绑定

我们在配置文件书写了如下配置值,其中第三项超时时间 timeout 描述了服务器操作超时时间,当前值是 - 1 表示永不超时。

yml
1
2
3
4
servers:
ip-address: 192.168.0.1
port: 2345
timeout: -1

但是每个人都这个值的理解会产生不同,比如线上服务器完成一次主从备份,配置超时时间 240,这个 240 如果单位是秒就是超时时间 4 分钟,如果单位是分钟就是超时时间 4 小时。面对一次线上服务器的主从备份,设置 4 分钟,简直是开玩笑,别说拷贝过程,备份之前的压缩过程 4 分钟也搞不定,这个时候问题就来了,怎么解决这个误会?

解决

除了加强约定之外,springboot 充分利用了 JDK8 中提供的全新的用来表示计量单位的新数据类型,从根本上解决这个问题。以下模型类中添加了两个 JDK8 中新增的类,分别是 Duration 和 DataSize

java
1
2
3
4
5
6
7
8
9
10
@Component
@Data
@ConfigurationProperties(prefix = "servers")
public class ServerConfig {
@DurationUnit(ChronoUnit.HOURS)
private Duration serverTimeOut;
@DataSizeUnit(DataUnit.MEGABYTES)
private DataSize dataSize;
}

常用单位

Duration:表示时间间隔,可以通过 @DurationUnit 注解描述时间单位,例如上例中描述的单位为小时(ChronoUnit.HOURS)

DataSize:表示存储空间,可以通过 @DataSizeUnit 注解描述存储空间单位,例如上例中描述的单位为 MB(DataUnit.MEGABYTES)

使用上述两个单位就可以有效避免因沟通不同步或文档不健全导致的信息不对称问题,从根本上解决了问题,避免产生误读。

Druation 常用单位如下:

image-20220222173911102

DataSize 常用单位如下:

image-20230502164700652

校验

目前我们在进行属性绑定时可以通过松散绑定规则在书写时放飞自我了,但是在书写时由于无法感知模型类中的数据类型,就会出现类型不匹配的问题,比如代码中需要 int 类型,配置中给了非法的数值,例如写一个 “a",这种数据肯定无法有效的绑定,还会引发错误。 SpringBoot 给出了强大的数据校验功能,可以有效的避免此类问题的发生。在 JAVAEE 的 JSR303 规范中给出了具体的数据校验标准,开发者可以根据自己的需要选择对应的校验框架,此处使用 Hibernate 提供的校验框架来作为实现进行数据校验。

导入依赖

xml
1
2
3
4
5
6
7
8
9
10
<!--1.导入JSR303规范-->
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
</dependency>
<!--使用hibernate框架提供的校验器做实现-->
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
</dependency>

添加属性

在配置文件下添加如下属性:

yaml
1
2
3
4
student:
name: 小码同学
major: 计算机科学与技术
age: 20

开启校验

这里和上面一样

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import org.springframework.validation.annotation.Validated;
import javax.validation.constraints.Max;
import javax.validation.constraints.Min;

//@Component
@Data // 自动生成get、set方法
@ConfigurationProperties(prefix = "student") // 绑定配置文件的属性
@Validated
public class Student {
private String name;
private String major;
@Max(value = 120,message = "最大值不能超过120")
@Min(value = 0,message = "最小值不能小于0")
private Integer age;
}

通过设置数据格式校验,就可以有效避免非法数据加载,其实使用起来还是挺轻松的,基本上就是一个格式。

补充:数据类型注意

问题来源

连接数据库出错,提示用户名和密码不匹配。

bash
1
java.sql.SQLException: Access denied for user 'root'@'localhost' (using password: YES)

我们在配置数据库的信息如下:

yaml
1
2
3
4
5
6
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/ssm_db?serverTimezone=UTC
username: root
password: 0127

我们密码使用了 0127,这个问题就处在这里了,因为 `0127 在开发者眼中是一个字符串 “0127”,但是在 springboot 看来,这就是一个数字,而且是一个八进制的数字。当后台使用 String 类型接收数据时,如果配置文件中配置了一个整数值,他是先安装整数进行处理,读取后再转换成字符串。巧了,0127 撞上了八进制的格式,所以最终以十进制数字 87 的结果存在了。

注意:

第一,字符串标准书写加上引号包裹,养成习惯

第二,遇到 0 开头的数据多注意

image-20230502171931128

测试

加载测试专用属性

我们在测试中,有时候想对某个模块进行测试,需要加一些参数,但又不想修改配置文件的参数,我们应该做呢?如果可以在测试类里面自定义添加就好了。

在创建的时候 SpringBoot 帮我们创建好了测试类

java
1
2
3
4
5
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest(classes = Application.class)
class ApplicationTests {

}

并且 SpringBoot 集成了 Junit5,看这个测试类,只有一个 @SpringBootTest 注解,估计集成的东西都在里面了,我们点点进去看看有什么东西。

01-测试专用属性-20230502-583

@SpringBootTest 注解里面,我们可以看到有一个 propertiesargs 的参数,我们使用过 classes 属性,用于指定 Springboot 的程序入口类。

springboot 在创建项目的时候,默认的配位文件类型就是 properties 类型的,这里也有一个 properties 参数名,我们猜测就是设置参数的,我们可以测试一下。

添加配置属性

我们在 springboot 的配置文件添加如下信息:

properties
1
hikki.username="zhangsan"

测试

在测试类中,使用 @Value 注入了配置的属性。

java
1
2
3
4
5
6
7
8
9
10
11
12
13
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest(classes = Application.class)
class ApplicationTests {
@Value("${hikki.username}")
private String username;

@Test
public void test(){
System.out.println(username);
}
}

运行测试方法,在控制到可以看到输出以下:

bash
1
"zhangsan"

添加 properties 属性

我们继续在 @SpringBootTest 注解上添加 properties 属性,设置和配置文件一样的属性,并设置值,看看会不会发生冲突,还是有优先级。

java
1
2
3
4
5
6
7
8
9
10
11
12
13
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest(classes = Application.class,properties = "hikki.username='lisi'")
class ApplicationTests {
@Value("${hikki.username}")
private String username;

@Test
public void test(){
System.out.println(username);
}
}

结果

再次运行测试方法,我们可以看到输出了'lisi',表示没有发生冲突,但有优先级,在 @SpringBootTest 上的 properties 属性设置的值优先级比配置文件的优先级要高,其实也可以理解,因为要是在注解上的优先级要低的话,这不是白忙活了嘛。

bash
1
'lisi'

使用 args 设置属性‘

我们之前有在 SpringBoot 的入口程序设置过参数,就是下面这个东西,创建项目就帮我们创建好了程序入口,我们可以在 args 这个数组添加参数,这里添加参数的优先级最高。

java
1
2
3
4
5
6
@SpringBootApplication()
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}

上面是在程序入口添加参数,我们试试在测试类来添加参数。

我们在 @SpringBootTest 注解上添加 args 的参数,并设置值 --hikki.username='wangwu'

java
1
2
3
4
5
6
7
8
9
10
11
12
13
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest(classes = Application.class,properties = "hikki.username='lisi'",args = "--hikki.username='wangwu'")
class ApplicationTests {
@Value("${hikki.username}")
private String username;

@Test
public void test(){
System.out.println(username);
}
}

运行测试方法

运行测试方法后,可以在控制台看到,输出了'wangwu',说明属性 args 优先级最高。

bash
1
'wangwu'

在命令行添加属性

bash
1
2
3
java -jar XXX-SNAPSHOT.jar 属性=值
# 如:
java -jar .\lab-0.0.1-SNAPSHOT.jar spring.profiles.active=dev

加载测试专用配置

上面我们试了一下,我们可以在测试里面添加运行参数,那能不能也添加一些只有测试能用的配置呢?

答案是可以的,我们之前在学习 Spring 也有使用过这相关的知识,在主配置类使用 @Import() 导入其他的配置类,下面我们来演示一下

创建配置类

这个配置类,只是模拟一下,真实开发不会这样使用,这个配置类,不作任何配置,只是当做一个 Bean,并且返回一个字符串就好。

plaintext
1
2
3
4
5
6
7
8
9
10
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class MsgConfig {
@Bean
public String msg(){
return "bean msg .........";
}
}

编辑测试类

在测试类使用 @Import 注解,导入 MsgConfig 类,并且自动注入配置类的方法。

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Import;
import site.hikki.config.MsgConfig;

@SpringBootTest(classes = Application.class)
@Import({MsgConfig.class})
class ApplicationTests {
@Autowired
private String msg;

@Test
void testConfiguration() {
System.out.println(msg);
}
}

运行测试方法

运行后可以在控制台看到如下信息:

bash
1
bean msg .........

当我们需要在测试类引入其他的配置类,或者是需要一些只有测试才用到的配置,我们就可以使用上面这种方式,创建一个配置类,配置相关信息,然后导入相关信息就可以了。

Web 环境模拟测试

前面使用的测试都是测试服务层或者数据层的,但都没有对表现层进行测试,在开发 Web 端,我们每次测试都要使用 Postman 或者 Swagger 这类 api 第三方测试工具来测试。

如果我们要在测试中对表现层测试,那测试中必须具有能够对 web 请求的能力,不然无法实现 web 功能的测试。所以在测试用例中测试表现层接口这项工作就转换成了两件事,一,如何在测试类中启动 web 测试,二,如何在测试类中发送 web 请求。下面一件事一件事进行,先说第一个。

配置测试 Web 环境

每一个 springboot 的测试类上方都会标准 @SpringBootTest 注解,我们点开这个注解里面看看。

02-web-20230503-126

从注解里面可以看到有个 webEnvironment 属性,并且还有四个枚举值,下面举例用法:

java
1
2
3
4
@SpringBootTest(classes = Application.class,webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class ApplicationTests {

}
  • MOCK:根据当前设置确认是否启动 web 环境,例如使用了 Servlet 的 API 就启动 web 环境,属于适配性的配置
  • DEFINED_PORT:使用自定义的端口作为 web 服务器端口
  • RANDOM_PORT:使用随机端口作为 web 服务器端口
  • NONE:不启动 web 环境

通过上述配置,现在启动测试程序时就可以正常启用 web 环境了,建议大家测试时使用 RANDOM_PORT,避免代码中因为写死设定引发线上功能打包测试时由于端口冲突导致意外现象的出现。就是说你程序中写了用 8080 端口,结果线上环境 8080 端口被占用了,结果你代码中所有写的东西都要改,这就是写死代码的代价。现在你用随机端口就可以测试出来你有没有这种问题的隐患了。

开启 Web 虚拟调用功能

Java 自带的 API 发送请求不太好用,SpringBoot 为了方便开发,对其功能进行了包装,简化开发步骤。

我们新建一个 WebTest 测试类,然后添加 @SpringBootTest 注解。

定义发起虚拟调用的对象 MockMVC,通过自动装配的形式初始化对象

java
1
2
3
4
5
6
7
8
9
10
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.web.servlet.MockMvc;

@AutoConfigureMockMvc //开启虚拟MVC调用
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class WebTest {

}

定义发起虚拟调用的对象 MockMVC

通过自动装配的形式初始化对象

java
1
2
3
4
5
6
7
8
9
10
11
12
13
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.web.servlet.MockMvc;

@AutoConfigureMockMvc //开启虚拟MVC调用
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class WebTest {
@Test
public void testWeb(@Autowired MockMvc mvc){

}
}

创建一个虚拟请求对象

封装请求的路径,并使用 MockMVC 对象发送对应请求

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;

@AutoConfigureMockMvc //开启虚拟MVC调用
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class WebTest {

@Test
void testWeb(@Autowired MockMvc mvc) throws Exception {
//http://localhost:8080/hello
//创建虚拟请求,当前访问/hello
MockHttpServletRequestBuilder builder = MockMvcRequestBuilders.get("/hello");
//执行对应的请求
mvc.perform(builder);
}
}

创建完请求对象,就可以运行测试方法了。

注意:访问路径不用写主机和端口,因为主机和端口使用的就是当前虚拟 web 环境,无需再次指定,只描述请求路径就可以了。

对比请求结果

上面已经模拟出 web 环境了,并且成功发送了 web 请求了。但请求发送出去了,得校验结果才有用啊,不然只是发送请求,不知道结果没用啊,下面就来讲一下怎么比对结果。

响应状态匹配

一般常用的有三种,分别是响应状态匹配、响应体匹配、响应头匹配

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
    /**
* 响应体匹配
* @param mvc
* @throws Exception
*/
@Test
public void testBody(@Autowired MockMvc mvc) throws Exception{
MockHttpServletRequestBuilder builder = MockMvcRequestBuilders.get("/hello");
ResultActions actions = mvc.perform(builder);
//设定预期值 与真实值进行比较,成功测试通过,失败测试失败
//定义本次调用的预期值
ContentResultMatchers content = MockMvcResultMatchers.content();
ResultMatcher resultMatcher = content.string("hello run ......"); //匹配字符串
// ResultMatcher result = content.json("{}"); //匹配json格式数据,因为我返回的格式不是json格式,这里不做测试
actions.andExpect(resultMatcher);
}
java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* 响应状态匹配
* @param mvc
* @throws Exception
*/
@Test
void testStatus(@Autowired MockMvc mvc) throws Exception {
MockHttpServletRequestBuilder builder = MockMvcRequestBuilders.get("/hello");
ResultActions action = mvc.perform(builder);
//设定预期值 与真实值进行比较,成功测试通过,失败测试失败
//定义本次调用的预期值
StatusResultMatchers status = MockMvcResultMatchers.status();
//预计本次调用时成功的:状态200
ResultMatcher ok = status.isOk();
//添加预计值到本次调用过程中进行匹配
action.andExpect(ok);
}
java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* 响应头信息匹配
* @param mvc
* @throws Exception
*/
@Test
public void testContentType(@Autowired MockMvc mvc) throws Exception{
MockHttpServletRequestBuilder builder = MockMvcRequestBuilders.get("/hello");
ResultActions actions = mvc.perform(builder);
//设定预期值 与真实值进行比较,成功测试通过,失败测试失败
//定义本次调用的预期值
HeaderResultMatchers header = MockMvcResultMatchers.header();
ResultMatcher contentType = header.string("Content-Type", "application/json");
//添加预计值到本次调用过程中进行匹配
actions.andExpect(contentType);
}

基本上齐了,头信息,正文信息,状态信息都有了,就可以组合出一个完美的响应结果比对结果了。以下范例就是三种信息同时进行匹配校验,也是一个完整的信息匹配过程。

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Test
void testGetById(@Autowired MockMvc mvc) throws Exception {
MockHttpServletRequestBuilder builder = MockMvcRequestBuilders.get("/hello");
ResultActions action = mvc.perform(builder);

StatusResultMatchers status = MockMvcResultMatchers.status();
ResultMatcher ok = status.isOk();
action.andExpect(ok);

HeaderResultMatchers header = MockMvcResultMatchers.header();
ResultMatcher contentType = header.string("Content-Type", "application/json");
action.andExpect(contentType);

ContentResultMatchers content = MockMvcResultMatchers.content();
ResultMatcher result = content.json("{}"); //因为我返回的格式不是json格式,这里不做测试
action.andExpect(result);
}

设置了比对的结果后,可以对 web 请求进行简单的测试了,对于一些简单的请求还是非常方便的。

数据层测试回滚

需求

当前我们的测试程序可以完美的进行表现层、业务层、数据层接口对应的功能测试了,但是测试用例开发完成后,在打包的阶段由于 test 生命周期属于必须被运行的生命周期,如果跳过会给系统带来极高的安全隐患,所以测试用例必须执行。但是新的问题就呈现了,测试用例如果测试时产生了事务提交就会在测试过程中对数据库数据产生影响,进而产生垃圾数据。这个过程不是我们希望发生的,作为开发者测试用例该运行运行,但是过程中产生的数据不要在我的系统中留痕,这样该如何处理呢?

解决办法

SpringBoot 早就为开发者想到了这个问题,并且针对此问题给出了最简解决方案,在原始测试用例中添加注解 @Transactional 即可实现当前测试用例的事务不提交。当程序运行后,只要注解 @Transactional 出现的位置存在注解 @SpringBootTest,SpringBoot 就会认为这是一个测试程序,无需提交事务,所以也就可以避免事务的提交

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@Transactional
@Rollback(true)
@SpringBootTest
public class RollbackTest {

@Autowired
private BookDao bookDao;

/**
* 不提交事务,默认不修改数据库
*/
@Test
public void testDelBook(){
int i = bookDao.delBookById(8);
System.out.println(i);
}

/**
* 提交事务,即对数据库进行修改
*/
@Test
@Rollback(value = false)
public void testAddBook(){
Book book = new Book();
book.setName("标准日本语");
book.setDescription("这是一本日语初学者常用的课本");
book.setType("外语");
int i = bookDao.addBook(book);
System.out.println(i);
}
}

如果开发者想提交事务,在运行的方法上面添加一个 @RollBack 的注解,设置回滚状态为 false 即可正常提交事务

数据层 - 数据源技术

我们之前整合 Druid+Mybatis+MySQL,这三个技术分别对应数据层操作的三个层面:

  • 数据源技术:Druid

  • 持久化技术:Mybatis

  • 数据库技术:MySQL

我们下面看看这些有什么区别。

前言

我们在开发的时候,使用最多的是 Druid 数据源,但我们不使用 Druid 数据源时,也是可以正常使用了,因为 SpringBoot 有内置的数据源技术,默认是是 HikariCP 数据源技术,不使用数据源运行项目时可以在控制台看到如下信息:

bash
1
2
INFO 31820 --- [           main] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Starting...
INFO 31820 --- [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Start completed.

SpringBoot 内置了三种数据源技术,分别是:

  • HikariCP
  • Tomcat 提供 DataSource
  • Commons DBCP

HikariCP

SpringBoot 内置的数据源,并且是默认使用的数据源,我们不做任何数据源配置就默认使用它了。

Tomcat 提供 DataSource

Tomcat 也有内置的数据源,如果我们不想用 HikariCP 数据源,我们还有可以选择 tomcat 内置的数据源,使用 Tomcat 的数据源主要是,我们在导入 web 的启动依赖时,这个依赖有默认使用了内嵌的 tomcat,而 tomcat 有内置的数据源,所以 SpringBoot 也有多一个数据源选择了。

我们要怎么使用 tomcat 内置的数据源呢?我们只需要把 HikartCP 技术的坐标排除掉就 OK 了。

DBCP

这个数据源就是在上面两个数据源都不使用的情况才会使用这个数据源,使用要求有点严苛,既不使用 HikartCP 也不使用 tomcat 的 DataSource 时,默认给你用这个。

用法

不配置数据源

不配置数据源默认使用 hikari 数据源,

yaml
1
2
3
4
5
6
spring:
datasource:
url: jdbc:mysql://localhost:3306/ssm_db?serverTimezone=UTC
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: root

配置 hikari 数据源

yaml
1
2
3
4
5
6
7
spring:
datasource:
url: jdbc:mysql://localhost:3306/ssm_db?serverTimezone=UTC
hikari:
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: root

配置 druid 数据源:

yaml
1
2
3
4
5
6
7
spring:
datasource:
druid:
url: jdbc:mysql://localhost:3306/ssm_db?serverTimezone=UTC
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: root

数据层 - 持久化技术

我们开发用的最多的是 mybatis,但 SpringBoot 也有提供一个持久化技术给我们,叫 JdbcTemplate,这个技术其实是 Spring 提供的,不是 SpringBoot 的技术,但还是可以使用的,SpringBoot 是对 Spring 的简化开发。

在进行持久化之前,你首先需要配置数据库信息,也即是上面所说的数据源技术。

JdbcTemplate

导入坐标

在 pom.xml 导入 JdbcTemplate 的相关坐标,JdbcTemplate 有 SpringBoot 的启动依赖,导入启动依赖。

xml
1
2
3
4
5
<!--        JdbcTemplate-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>

自动装配

在 Dao 层创建一个 BookDaoTemplate 类,进行自动装配。

java
1
2
3
4
5
6
@Repository
public class BookDaoTemplate {
@Autowired
JdbcTemplate jdbcTemplate;

}

使用 JdbcTemplate 进行增删改查

使用 JdbcTemplate 进行增删改查仍然是在 BookDaoTemplate 类中,JdbcTemplate 对 Jdbc 的封装,让我们在 SpringBoot 更方便的使用,提高了使用效率。

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.stereotype.Repository;
import site.hikki.pojo.Book;

import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.List;
import java.util.Map;

/**
* @author : 小码同学
* Date: 2023/5/10 17:30
* 小码博客 :https://blog.hikki.site
* 小码同学公众号 :小码同学
*/

@Repository
public class BookDaoTemplate {
@Autowired
JdbcTemplate jdbcTemplate;

/**
* 不使用实体类封装,自定义键值对封装
* @return
*/
public List<Map<String,Object>> findBook(){
String sql = "select * from tb_book";
List<Map<String, Object>> maps = jdbcTemplate.queryForList(sql);
return maps;
}

public List<Book> find(){
String sql = "select * from tb_book";
RowMapper<Book> rm = new RowMapper<Book>() {
@Override
public Book mapRow(ResultSet rs, int rowNum) throws SQLException {
Book temp = new Book();
temp.setId(rs.getInt("id"));
temp.setName(rs.getString("name"));
temp.setType(rs.getString("type"));
temp.setDescription(rs.getString("description"));
return temp;
}
};
List<Book> list = jdbcTemplate.query(sql, rm);
return list;
}
}

测试

编写完实现类,我们可以使用 Junit 进行单元测试了,首先自动注入 BookDaoTemplate 的类,再进行测试。

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import site.hikki.dao.BookDaoTemplate;
import site.hikki.pojo.Book;
import java.util.List;
import java.util.Map;

/**
* @author : 小码同学
* Date: 2023/5/9 22:18
* 小码博客 :https://blog.hikki.site
* 小码同学公众号 :小码同学
*/
@SpringBootTest(classes = Application.class)
public class JdbcTemplateTest {
@Autowired
private BookDaoTemplate bookDaoTemplate;

@Test
void testJdbcTemplate(){
List<Map<String, Object>> book = bookDaoTemplate.findBook();
System.out.println(book);
}

@Test
void findJdbcTemplate(){
List<Book> books = bookDaoTemplate.find();
System.out.println(books);
}
}

Spring Data JPA

前言

介绍

Spring Data JPA 是更大的 Spring Data 家族的一部分,可以轻松实现基于 JPA 的存储库。本模块处理对基于 JPA 的数据访问层的增强支持。它使构建使用数据访问技术的 Spring 驱动的应用程序变得更加容易。

提供了增删改查等常用功能,使开发者可以用较少的代码实现数据操作,同时还易于扩展。

定义了独特的 JPQLJava Persistence Query Language),一种和 SQL 非常类似的 中间性对象化查询语言,最终被编译成针对不同底层数据库的 SQL 查询,从而屏蔽不同数据库的差异。JPQL 语句可以是 select 语句、update 语句或 delete 语句,它们都通过 Query 接口封装执行。

特点

  • 基于 Spring 和 JPA 构建存储库的成熟支持
  • 支持查询 谓词,从而实现类型安全的 JPA 查询
  • 域类的透明审计
  • 分页支持、动态查询执行、集成自定义数据访问代码的能力
  • 验证 @Query 引导时带注释的查询
  • 支持基于 XML 的实体映射
  • 基于 JavaConfig 的仓库配置 @EnableJpaRepositories

截止到目前为止,Spring Data JPA 最新版本为 v3.1.0

功能

  1. 支持 XML 注解两种元数据的形式,元数据用来描述对象和表之间的映射关系,框架据此将实体对象持久化到数据库表中
  2. 通过面向对象而非面向数据库的查询语言查询数据,避免程序的 SQL 语句紧密耦合
  3. 用来操作实体对象,执行 CRUD 操作,框架在后台替我们完成所有的事情,开发者从繁琐的 JDBC 和 SQL 代码中解脱出来

接口

使用 Spring Data JPA 自定义 Repository 接口,必须继承 XXRepository<T, ID> 接口,其中 T 表示实体类,ID 表示数据表的主键的类型,一般是使用 Integer 较多。

image-20230521140443542

我们进入 JpaRepository 的类可以看到集成了很多方法,由名字我们大概可以猜到这些就是对数据库操作封装好的方法,比如查询数据库总条数,增删改查操作。

01-继承的类-springboot – JpaRepo20230521-065

话不多说,直接上手操作一下看看

环境准备

导入依赖

其中主要是 Spring Data JPA 的依赖,其他依赖可根据自己的需求添加

xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<!--        Junit5测试-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- Spring Data JPA依赖启动器 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- mysql数据库-->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<version>8.0.33</version>
<scope>runtime</scope>
</dependency>
<!-- Druid-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.2.16</version>
</dependency>

数据库配置

yaml
1
2
3
4
5
6
7
spring:
datasource:
druid:
username: root
driver-class-name: com.mysql.cj.jdbc.Driver
password: root
url: jdbc:mysql://localhost:3306/spring_db?useUnicode=true&characterEncoding=utf-8

创建 ORM 实体类

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

@Entity(name = "tb_student") // 对应表名
public class Student {

@Id // 表主键
@GeneratedValue(strategy = GenerationType.IDENTITY) // ID自增
private Integer num;
private Integer age;
private String name;
private String major;
// 自行添加get set toString方法
}

这里出现了几个注解

注解 参数 描述
@Entity name 设置实体类对应的数据库表名
@Id 设置数据表中的 ID
@GeneratedValue strategy 设置 GenerationType.IDENTITY 表示每次添加数据时自动自增 ID

实现 JPA 接口

java
1
2
3
4
5
6
import org.springframework.data.jpa.repository.JpaRepository;
import site.hikki.domain.Student;

public interface StudentRepositoty extends JpaRepository<Student,Integer> {

}

参数解释:

  • Student:查询数据表对应的实体类
  • Integer:Student 实体类的主键 ID 的类型

测试

我们只需要实现接口,就可以开始测试了,因为 JPA 已经帮我们封装了一部分的方法。

创建一个测试类:

注入 StudentRepositoty 对象

bash
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import site.hikki.domain.Student;
import site.hikki.repository.StudentRepositoty;

import java.util.List;

@SpringBootTest(classes = Application.class)
class ApplicationTests {
@Autowired
private StudentRepositoty studentRepositoty;

}

实现注入后,我们可以尝试实现 studentRepositoty 方法看看,如下图,我们我们可以看到已经实现了很多方法,我们可以直接使用。

02-封装方法-springboot – Applica20230521-466

上面只是一个简单的实现方法,下面开始对 JPA 进行详细的介绍。

JPA 进阶

分析封装方法

从继承的接口来看,我们最终实现的是 JpaRepository,实现了上面一堆的方法,我们下面看看应该怎么用。

image-20230521143559219
Repository 接口

Repository 提供了根据方法名查询方式。

方法的名称要遵循 findBy+ 属性名(首字母大写)+ 查询条件 (首字母大写 Is Equals) 的格式,如下:

java
1
2
findByNameLike(String name)
findByName(String name)

其实就是规范的驼峰命名,注意一下就好了。

CrudRepository 接口
java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@NoRepositoryBean 
public interface CrudRepository<T, ID> extends Repository<T, ID> { 
<S extends T> S save(S entity); //保存 
<S extends T> Iterable<S> save(Iterable<S> entities);  
T findOne(ID id); //根据id查询一个对象,返回对象本身或null  
Iterable<T> findAll(); //查询所有的对象 
Iterable<T> findAll(Iterable<ID> ids); //据id列表查所有对象  
boolean exists(ID id); //根据id 判断对象是否存在
long count(); //计算对象的总个数 
void delete(ID id); //根据id 删除 
void delete(T entity); //删除一个对象
void delete(Iterable<? extendsT> entities); //批量删除集合
void deleteAll(); //删除所有(后台执行时,一条一条删除)
}
PagingAndSortingRepository 接口
java
1
2
3
4
5
@NoRepositoryBean 
public interface PagingAndSortingRepository<T, ID> extends CrudRepository<T, ID> {
    Iterable<T> findAll(Sort sort);// 仅排序 
      Page<T>findAll(Pageable pageable);//分页和排序 

JpaRepository 接口
java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@NoRepositoryBean
public interface JpaRepository<T,ID extendsSerializable> extends PagingAndSortingRepository<T,ID>,QueryByExampleExecutor<T> {  
   List<T>findAll(); //查询所有对象,返回List
   List<T>findAll(Sort sort); //查所有对象并排序,返回List
   List<T>findAll(Iterable<ID> ids); 
   void flush()//强制缓存与数据库同步
   <S extends T> List<S> save(Iterable<S> entities)//批量保存,并返回对
List<S extends T> S saveAndFlush(S entity);//保存并强制同步数据
//批量删集合对象(后台执行时,生成一条语句,多个or条件)
void deleteInBatch(Iterable<T> entities);
//删除所有(执行一条语句,如:delete from user)
void deleteAllInBatch();  
//根据id 查询一个对象,返回对象的引用(区别于findOne)。当对象不存时,返回引用不是null,但各个属性值是null
T getOne(ID id);
//根据实例查询
<S extends T> List<S> findAll(Example<S> example)
//根据实例查询并排序。
<S extends T> List<S> findAll(Example<S> example, Sort sort);
}

上面给出了大部分的封装方法的作用,你可以根据需要选择合适的方法,减少自己编写的额 SQL 语句,造成代码冗余。

自定义方法

虽然 JPA 给我们封装了不少的方法,但需求是多变的,我们总会用到一些需要自定义的方法,下面带你实现一下:

查询
java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.transaction.annotation.Transactional;
import site.hikki.domain.Student;
import java.util.List;

public interface StudentRepositoty extends JpaRepository<Student,Integer> {
/**
* Jpa自带了一部分的封装好的常用方法,我们可以直接使用。
* 如果封装好的方法不能满足你的需求,你可以根据自己的需求来编写SQL语句,比如下面是自定义的方法
*/

/**
* 使用形参的方式有两种,一种是用 :形参名
* 另一种是使用数字表示第几位参数 :?n
*/
@Query("select s from tb_student s where s.num=:num") //使用参数命
// @Query("select s from tb_student s where s.num=?1") // 使用占位符
List<Student> getStudentByNum(Integer num);

/**
* 查询全部数据,使用分页查询
* Pageable 是表示使用分页接收结果,具体可以查看测试方法
*/
@Query("select s from tb_student s") //使用参数命
List<Student> getStudentPage(Pageable pageable);

/**
* nativeQuery = true 表示开启原生SQL,不需要像上面一样使用表别名
*/
@Query(value = "select * from tb_student",nativeQuery = true)
List<Student> getStudentPage2(Pageable pageable);
}

在上面自定义查询语句中,我们只使用了 @Query 注解,该注解表示执行 SQL 语句,之后不管你使用 insert、update、delete 等的 SQL 语句,都可以使用 @Query 注解来执行。

修改
java
1
2
3
4
5
6
7
8
9
/**
* ?n 表示占位符,使用该方法中的第几个参数
* 如 name = ?2 使用形参中的 name 参数
* num = ?1 使用形参中的 num
*/
@Transactional
@Modifying
@Query(value = "update tb_student set name = ?2 where num = ?1",nativeQuery = true)
int updateStudentByNum(Integer num,String name);

上面使用到了两个新的注解,@Transactional 和 @Modifying

注解 描述
@Transactional 开启事务处理
@Modifying 数据变更操作

事务处理

针对数据的变更操作(修改、删除),无论是否使用了 @Query 注解,都必须在方法上方添加 @Transactional 注解进行事务管理,否则程序执行就会出现 InvalidDataAccessApiUsageException 异常。

如果在调用 Repository 接口方法的业务层 Service 类上已经添加了 @Transactional 注解进行事务管理,那么 Repository 接口文件中就可以省略 @Transactional 注解。

数据变更

在自定义的 Repository 接口中,使用 @Query 注解方式执行数据变更操作(修改、删除),除了要使用 @Query 注解,还必须添加 @Modifying 注解表示数据变更。

删除
java
1
2
3
4
5
6
7
8
/**
* 根据num删除学生
* 因为是对数据进行改动,必须添加Transactional和Modifying注解
*/
@Transactional
@Modifying
@Query(value = "delete from tb_student where num = ?1",nativeQuery = true)
int delStudentByNum(Integer num);
综合测试

SQL 语句写完了,自然是要开始单元测试了。

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.domain.PageRequest;
import site.hikki.domain.Student;
import site.hikki.repository.StudentRepositoty;

import java.util.List;

@SpringBootTest(classes = Application.class)
class ApplicationTests {
@Autowired
private StudentRepositoty studentRepositoty;

@Test
public void find() {
List<Student> all = studentRepositoty.findAll();
System.out.println(all);
}

@Test
public void getStudentByNum(){
List<Student> page = studentRepositoty.getStudentByNum(3);
System.out.println(page);
}

@Test
public void getStudentPage(){
PageRequest request = PageRequest.of(1, 2);// 查询第一页,每页2条数据
List<Student> studentPage = studentRepositoty.getStudentPage(request);
for (Student s :studentPage) {
System.out.println(s);
}
}

@Test
public void getStudentPage2(){
PageRequest request = PageRequest.of(1, 2);// 查询第一页,每页2条数据
List<Student> studentPage = studentRepositoty.getStudentPage2(request);
for (Student s :studentPage) {
System.out.println(s);
}
}

/**
* 修改学生
*/
@Test
public void updateStudentByNum(){
int i = studentRepositoty.updateStudentByNum(12, "小黑子");
System.out.println(i);
}

/**
* 删除学生
*/
@Test
public void delStudentByNum(){
int i = studentRepositoty.delStudentByNum(12);
System.out.println(i);
}
}

关联查询

下面演示一个一对多关联查询。

tb_dept 表:

字段 类型 描述
dept_id int 主键 ID
dept_name varchar 系名

tb_major 表:

字段 类型 描述
major_id int 主键 ID
major_name varchar 专业名
tuition varchar 学费
dept_id int 外键系 ID
需求

需求一:根据系编号 (dept_id) 查询该系的全部专业

需求二:根据专业名称 (major_name,模糊查询) 查询;

创建实体类

创建实体类非常重要,因为你需要在实体类描述表和表之间的关系,如果描述不清楚,查询不会成功,所以,这个描述符非常重要

Dept 实体类

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
import javax.persistence.*;
import java.util.List;

@Entity(name = "tb_dept") // 绑定数据表
public class Dept {
@Id // 表示该属性是主键
@GeneratedValue(strategy = GenerationType.IDENTITY) // ID自增
private Integer deptId;
private String deptName;

@OneToMany(
mappedBy = "dept", //定义在关系的被维护端,指向关系的维护端;@ManyToOne不存在该属性,只有@OneToOne,@OneToMany,@ManyToMany上才有该属性。
targetEntity = Major.class, // 关联类的类型,默认是该成员属性对应的类类型
fetch = FetchType.EAGER //急加载,加载一个实体时,会立即从数据库中加载
)
private List<Major> majors;


@Override
public String toString() {
return "Dept{" +
"deptId=" + deptId +
", deptName='" + deptName + '\'' +
", majorList=" + majors +
'}';
}

public Integer getDeptId() {
return deptId;
}

public void setDeptId(Integer deptId) {
this.deptId = deptId;
}

public String getDeptName() {
return deptName;
}

public void setDeptName(String deptName) {
this.deptName = deptName;
}

public List<Major> getMajorList() {
return majors;
}

public void setMajorList(List<Major> majorList) {
this.majors = majorList;
}
}
Major 实体类

在 Major 实体类中,重写 toString 方法,不能添加 dept 属性,不然会报错,因为当你查询系别下全部专业时,专业中又有系别,但系别下又有专业,这样会形成无线套娃,会爆内存。

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
import com.fasterxml.jackson.annotation.JsonIgnore;
import javax.persistence.*;
import java.io.Serializable;

@Entity(name = "tb_major") // 绑定数据表
public class Major implements Serializable {
@Id // 表示该属性是主键
@GeneratedValue(strategy = GenerationType.IDENTITY) // ID自增
private Integer majorId;
private String majorName;
private Integer tuition;

@ManyToOne(
fetch = FetchType.EAGER, //急加载,加载一个实体时,会立即从数据库中加载
targetEntity = Dept.class // 关联类的类型,默认是该成员属性对应的类类型
)
@JoinColumn(
name = "deptId",
referencedColumnName = "deptId"
)
@JsonIgnore
private Dept dept;


@Override
public String toString() {
return "Major{" +
"majorId=" + majorId +
", majorName='" + majorName + '\'' +
", tuition=" + tuition +
'}';
}


public void setDept(Dept dept) {
this.dept = dept;
}

public Integer getMajorId() {
return majorId;
}

public void setMajorId(Integer majorId) {
this.majorId = majorId;
}

public String getMajorName() {
return majorName;
}

public void setMajorName(String majorName) {
this.majorName = majorName;
}

public Integer getTuition() {
return tuition;
}

public void setTuition(Integer tuition) {
this.tuition = tuition;
}

public Dept getDeptId() {
return dept;
}

public void setDeptId(Dept deptId) {
this.dept = deptId;
}
}
继承 JpaRepository 类

MajorRepository 接口

根据 JPA 的查询命名规范,我们需要模糊查询专业名称 MajorName,可以得出使用 findByMajorNameContains 方法名。

java
1
2
3
4
5
6
7
8
import org.springframework.data.jpa.repository.JpaRepository;
import site.hikki.domain.Major;
import java.util.List;

public interface MajorRepository extends JpaRepository<Major,Integer> {
List<Major> findByMajorNameContains(String majorName);

}

DeptRepository 接口

需求是根据系别 ID 查询所有专业。

java
1
2
3
4
5
6
import org.springframework.data.jpa.repository.JpaRepository;
import site.hikki.domain.Dept;

public interface DeptRepository extends JpaRepository<Dept,Integer> {
Dept findByDeptId(Integer id);
}
Service 层

DeptAndMajorService 接口

java
1
2
3
4
5
6
7
8
import site.hikki.domain.Dept;
import site.hikki.domain.Major;
import java.util.List;

public interface DeptAndMajorService {
Dept findDeptWithMajor(Integer id);
List<Major> findByMajorNameContains(String majorName);
}

实现 DeptAndMajorService 接口

记得给该类添加 @Service 注解,受 Ioc 容器管理。

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import site.hikki.domain.Dept;
import site.hikki.domain.Major;
import site.hikki.repository.DeptRepository;
import site.hikki.repository.MajorRepository;
import site.hikki.service.DeptAndMajorService;
import java.util.List;

@Service
public class DeptAndMajorServiceImpl implements DeptAndMajorService {
@Autowired
private DeptRepository deptRepository;

@Autowired
private MajorRepository majorRepository;

@Override
public Dept findDeptWithMajor(Integer id) {
return deptRepository.findByDeptId(id);
}

@Override
public List<Major> findByMajorNameContains(String majorName) {
return majorRepository.findByMajorNameContains(majorName);
}
}
测试结果
java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
package site.hikki;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import site.hikki.domain.*;
import site.hikki.service.DeptAndMajorService;

import java.util.List;

/**
* @author : 小码同学
* Date: 2023/5/22 1:21
* 小码博客 :https://blog.hikki.site
* 小码同学公众号 :小码同学
*/
@SpringBootTest(classes = Application.class)
public class OneToManyTest {

@Autowired
DeptAndMajorService deptAndMajorService;


/**
* 根据专业名称(major_name,模糊查询)查询;
*/
@Test
public void MajorByMajorName(){
List<Major> contains = deptAndMajorService.findByMajorNameContains("工程");
for (Major m :contains) {
System.out.println(m);
}
}

/**
* 根据系编号(dept_id)查询
*/
@Test
public void findDeptWithMajor(){
Dept major = deptAndMajorService.findDeptWithMajor(407);
System.out.println(major);
}
}

数据层 - 数据库技术

SpringBoot 提供了内置数据源、持久化技术的解决方案,自然数据库也有提供。

SpringBoot 提供了 3 款内置的数据库,分别是

  • H2
  • HSQL
  • Derby

以上三款数据库除了可以独立安装之外,还可以像是 tomcat 服务器一样,采用内嵌的形式运行在 spirngboot 容器中。内嵌在容器中运行,那必须是 java 对象啊,这三款数据库底层都是使用 java 语言开发的。

任何技术的出现都是有需求,我们在做测试的时候,我们希望测试的数据不要留在磁盘中,最好是每次测试完,关闭测试后,测试过程中的数据都消失不见,这样测试就不会对真实数据库产生影响,这样可以极大的方便测试工作。

导入依赖

xml
1
2
3
4
5
6
7
8
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>

如果你已经导入过 web 工程依赖,就不需要添加了了,如果还没添加 web 依赖,就需要添加一下。

xml
1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

配置 H2 数据库

yaml
1
2
3
4
5
6
7
8
9
10
11
spring:
h2: # H2数据库
console:
enabled: true
path: /h2
datasource: # H2数据库
url: jdbc:h2:~/spring_db
hikari:
driver-class-name: org.h2.Driver
username: sa
password: 123456

打开数据库

打开 H2 数据库:http://127.0.0.1:8080/h2

打开后可以看到这样的页面,我们在数据库配置的初始用户名为 sa,密码为 123456,数据库名需要改一下,我们设置的 spring_db,图片中的数据库名是 test,我们需要修改为 spring_db。

03-H2数据库-20230516-045

熟悉 H2 数据库

我们可以在终端数据库输入 sql 命令,我们尝试创建一个数据表。

05-创建数据表-20230517-601

创建数据表后,我们可以在左侧看到数据表,我们添加一条数据。

添加数据时候需要注意,字段使用 '' 单引号,不能使用双引号

上方白色的终端输入,下方是查询结果展示区。

06-插入新数据-20230517-938

总结

使用 H2 进行测试可以减少对磁盘的数据进行操作,不对真实数据库进行改动,对测试非常友好。

本文章来源于我的博客:https://blog.hikki.site