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

配置属性与数据

获取配置文件属性

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

使用实体类封装配置属性

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

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

定义属性

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

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

封装属性

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

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依赖。

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

打印属性内容

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

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

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

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配置文件定义一下属性:

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

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

引入依赖

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程序入口编写代码:

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

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

打印属性内容

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

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,如下:

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

定义第三方Bean

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

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

运行错误

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

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属性名

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

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

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官方推荐使用烤肉串模式,也就是中划线模式。

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

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表示永不超时。

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

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提供的校验框架来作为实现进行数据校验。

导入依赖

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>

添加属性

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

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

开启校验

这里和上面一样

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;
}

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

补充:数据类型注意

问题来源

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

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

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

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帮我们创建好了测试类

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的配置文件添加如下信息:

1
hikki.username="zhangsan"

测试

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

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);
}
}

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

1
"zhangsan"

添加properties属性

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

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属性设置的值优先级比配置文件的优先级要高,其实也可以理解,因为要是在注解上的优先级要低的话,这不是白忙活了嘛。

1
'lisi'

使用args设置属性‘

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

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'

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优先级最高。

1
'wangwu'

在命令行添加属性

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

加载测试专用配置

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

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

创建配置类

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

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类,并且自动注入配置类的方法。

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);
}
}

运行测试方法

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

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

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

Web环境模拟测试

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

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

配置测试Web环境

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

02-web-20230503-126

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

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,通过自动装配的形式初始化对象

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

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

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对象发送对应请求

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请求了。但请求发送出去了,得校验结果才有用啊,不然只是发送请求,不知道结果没用啊,下面就来讲一下怎么比对结果。

响应状态匹配

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

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);
}
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);
}
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);
}

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

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就会认为这是一个测试程序,无需提交事务,所以也就可以避免事务的提交

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数据源技术,不使用数据源运行项目时可以在控制台看到如下信息:

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数据源,

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数据源

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数据源:

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的启动依赖,导入启动依赖。

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

自动装配

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

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

}

使用JdbcTemplate进行增删改查

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

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的类,再进行测试。

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的依赖,其他依赖可根据自己的需求添加

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>

数据库配置

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实体类

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接口

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对象

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)的格式,如下:

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

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

CrudRepository接口
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接口
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接口
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给我们封装了不少的方法,但需求是多变的,我们总会用到一些需要自定义的方法,下面带你实现一下:

查询
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注解来执行。

修改
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注解表示数据变更。

删除
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语句写完了,自然是要开始单元测试了。

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实体类

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属性,不然会报错,因为当你查询系别下全部专业时,专业中又有系别,但系别下又有专业,这样会形成无线套娃,会爆内存。

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方法名。

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查询所有专业。

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接口

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容器管理。

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);
}
}
测试结果
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语言开发的。

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

导入依赖

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依赖,就需要添加一下。

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

配置H2 数据库

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