目录
一、什么是单元测试?
1.1 单元测试的好处
1.2 单元测试的实现步骤
1.2.1 生成单元测试类:
1.2.2 @SpringBootTest注解
1.2.3 检验方法结果:
二、利用MyBatis实现查询操作
2.1单表查询
2.2 参数占位符 #{} 和 ${}
2.2.1 ${} 字符直接替换
2.2.2 #{} 预编译处理
2.2.3 #{} 和 ${} 两者的区别
2.2.4 SQL注入问题
2.3 like查询
2.3.1 引入concat解决#{}的问题
2.3.2 当出现实体类类名与数据库字段名不相同的时候该怎么处理?
2.4 多表查询
三、利用MyBatis实现修改操作
四、利用MyBatis实现删除操作
五、利用MyBatis实现添加操作
前言:本篇出自博主的上一篇博客快速入门MyBatis,以下操作的数据库皆为上章所提及,这里就不再演示。
在介绍单元测试之前,先来看一组操作:
以下是根据所给的 ID 来查询用户名:
如果没有使用单元测试的情况下需要验证该功能的正常性,就只能通过Service调用Mapper层,再根据Controller层调用Service层:
代码实现如下:
一、什么是单元测试?
单元测试(unit testing),是指对软件中的最⼩可测试单元进⾏检查和验证的过程就叫单元测试。
Spring Boot 项⽬创建时会默认单元测试框架 spring-boot-test,⽽这个单元测试框架主要是依靠另⼀个著名的测试框架 JUnit 实现的:
打开 pom.xml 就可以看到,以下信息是 Spring Boot 项⽬创建是⾃动添加的:
1.1 单元测试的好处
- 可以⾮常简单、直观、快速的测试某⼀个功能是否正确。
- 使⽤单元测试可以帮我们在打包的时候,发现⼀些问题,因为在打包之前,所以的单元测试必须通过,否则不能打包成功。
- 使⽤单元测试,在测试功能的时候,可以不污染连接的数据库,也就是可以不对数据库进⾏任何改变的情况下,测试功能。
针对第二点:
1.2 单元测试的实现步骤
1.2.1 生成单元测试类:
按照上述步骤之后,就会生成如下代码:
1.2.2 @SpringBootTest注解
记得加上@SpringBootTest注解,随后完善测试方法的具体实现:
package com.example.demo.mapper;
import com.example.demo.entity.UserEntity;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import static org.junit.jupiter.api.Assertions.*;
@SpringBootTest
class UserMapperTest {
@Autowired
private UserMapper userMapper;
@Test
void getUserById() {
UserEntity user = userMapper.getUserById(1);
System.out.println(user);
}
}
1.2.3 检验方法结果:
如果当@Param里面的参数改为uid,其他地方不再进行修改,那么程序会报错嘛?
:会的,以下是运行结果
分析:
具体来说,@Param注解可以应用于方法的参数上,用于指定参数的名称。这个名称会与 SQL 查询语句中的${}或#{}配合使用,以匹配对应的参数。
二、利用MyBatis实现查询操作
2.1单表查询
UserMapper.xml 代码如下:
UserMapper.java 如下:
2.2 参数占位符 #{} 和 ${}
- #{} : 预编译处理。
- ${}: 字符直接替换。
预编译处理 :MyBatis 在处理#{} 时,会将SQL中的 #{} 替换为 ? 号,使用PreparedStatement的set方法来赋值。
直接替换:是将MyBatis在处理${} 时,就是把${} 替换成变量的值。
为了更好的观察两者的区别,我们需要打印MyBatis执行的SQL语句,在此之前需要在application.properties中完成以下配置:
#配置MyBatis的xml保存路径
mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
#配置打印打印的日志级别(默认的日志级别是info,需要设置为debug才能显示出来)
logging.level.com.example.demo=debug
2.2.1 ${} 字符直接替换
运行以下测试方法进行检查功能是否正确,运行代码如下所示:
2.2.2 #{} 预编译处理
运行以下测试方法进行检查功能是否正确,运行代码如下所示:
2.2.3 #{} 和 ${} 两者的区别
可能观察了上面这些运行结果,并没有发现这两者之间有什么区别,这是因为之前都是使用int类型进行的传参,当使用String类型进行传参的时候,就会发送改变。
以下为 #{} 占位符模式,可以观察到程序运行正常。
接着我们使用${} 直接替换,观察运行结果,发现程序报错:
细心观察,我们发现:这是因为采用了直接替换的格式,为加任何修饰,于是程序出现了错误,就好比以下SQL代码:
这是因为采用了直接替换,没有使用引号,于是程序出现了问题,正常的sql语句应该如下:
或者可以采取这样的方式来避免刚刚的错误:
而${}所采用的直接替换的方式,容易引入SQL注入问题。 【下文会提到】
除此之外,两者还有性能的区别。
使用 #{} 时,参数值会以预编译参数的形式传递给数据库,数据库会对参数进行安全处理。这样可以提高数据库的执行效率,尤其是对于频繁执行的SQL语句,数据库可以重复使用编译好的执行计划,从而提高查询性能。
而使用 ${} 时,参数值会直接替换到SQL语句中,相当于字符串拼接。这样可能导致SQL语句的执行计划在每次执行时都需要重新编译,降低了数据库的执行效率。
因此,从性能角度考虑,推荐使用#{}来处理参数,尽量避免使用${}。特别是当参数值来自用户输入时,使用${}可能存在安全风险,同时也可能降低数据库的执行效率。
可能有的人会问:既然#{}性能比${}高,而且#{}还能防止SQL注入的问题,那么#{}存在的意义是什么?
${}存在的原因是为了一些特殊场景的需求,它提供了更大的灵活性,例如当某些场景需要根据某个数值进行排序的时候:
当使用${}的时候,程序就会正常运行无误:
小结:
总结起来,#{}是安全的参数占位符,能够防止SQL注入攻击,而${}是简单的字符串替换,需要谨慎使用以避免安全风险。在编写SQL语句时,建议使用#{}来处理参数,尽量避免使用${},特别是当参数值来自用户输入时。
ps:在MyBatis中,无论是#{}还是${},底层的实现都是PreparedStatement,而不是Statement(Statemt执行的是不带参数的SQL语句)。
2.2.4 SQL注入问题
SQL注入常见于登录的时候,以下带来示例:
观察数据库我们可以明白,用户的密码是 admin:
如果在使用${}的情况下,不输入正确密码,而利用SQL注入,便可绕过验证,获取用户信息:
如果是使用#{}则不会出现这样的问题:
这是为什么呢,下面来一组图来解释一下:
需要注意的是,1是等于符号1的,这里可以利用MySQL语句进行进一步验证:
2.3 like查询
UserMapper.xml如下所示:
UserMapper.java代码如下所示:
测试代码如下:
运行结果:(程序出现错误)
2.3.1 引入concat解决#{}的问题
可能有人觉得很奇怪,明明操作没有毛病啊,下面我们来解释一下这个原因,当使用#{}进行处理的时候,实际在Mysql中是转换成这样:
concat的介绍
- 在 MySQL 中, concat函数用于将多个字符串连接成一个字符串。它接受两个或多个参数,将它们按顺序连接在一起,并返回连接后的结果。
为了避免出现多余的单引号,我们可以使用concat对其进行拼接,以下是示例:
可以观察到,使用concat可以对这些参数进行拼接,这些参数可以是字符串常量、列名或表达式。
以下是使用concat对原先like查询修改后的结果:
观察结果,程序运行正常,且查询到的结果与数据库吻合:
2.3.2 当出现实体类类名与数据库字段名不相同的时候该怎么处理?
在实际开发的过程中,经常会出现数据库设置的字段名与我们实体类中的名字不相同的情况(需要注意:如果大小写不一致是不影响的):
由于MyBatis是一个ORM框架,如果两者不一致的话(不区分大小写的,例如updateTime就不受影响),是无法进行映射操作的。我们运行测试方法,发现实体类中的pwd属性的结果为空,进一步验证以上观点:
此时有两种解决方案,一种是使用Mysql提供的as关键字,另一种是使用resultMap。
①使用as关键字解决上述问题,如下所示:
②使用resultMap:
这里利用resultMap中的id 和result标签完成映射,拿到了pwd的值:
分析:
1.
id 标签:id 标签用于指定主键列的映射关系。它定义了将查询结果中的某一列映射到目标对象的主键属性上。主键属性通常是唯一标识一个对象的属性。在 id 标签中,需要指定两个属性:
- property:指定目标对象的属性名,即主键属性名。
- column:指定查询结果中的列名,即主键列名。
2.result 标签:result 标签用于指定普通属性的映射关系。它定义了将查询结果中的某一列映射到目标对象的普通属性上。普通属性是对象中除主键属性外的其他属性。在 result 标签中,需要指定两个属性:
- property:指定目标对象的属性名,即普通属性名。
- column:指定查询结果中的列名,即列名。
2.4 多表查询
输入作者id得到文章详情信息,以下是ArticleMapper.xml实现代码:
ArticleMapper.java实现代码:
以下是ArticleInfo和ArticleInfoVo的实现:
运行测试代码,效果如下所示:
分析发现:只有ArticleInfoVo的属性,并没有打印出父类的属性。
初步判断是使用Lombok插件所导致的,让articleinfovo在使用toString方法的时候只打印自己的属性而不打印父类的属性:
接下来我们查看target目标文件夹下的字节码文件来验证:
通过观察,我们确信是因为我们使用Lombok插件所导致的这一现象。
解决方案:在ArticleInfoVo中重写toString方法(在使用Lombok插件时,如果自己重写的方法与插件所提供的方法相冲突,以自己书写的方法为主):
Ps:一定要选择重写类型,否则默认情况下重写的toString方法没有继承父类属性。
再次运行测试方法,即可打印父类属性:
三、利用MyBatis实现修改操作
UserMapper.java的代码:
UserMapper.xml的代码:
注:在MyBatis的<
update>标签中,不需要设置resultType参数的原因是因为<update>标签通常用于执行更新操作(如插入、更新、删除),这些操作的返回结果通常是受影响的行数,而不是具体的结果对象。
进行测试,效果如下所示:
由于前面都是使用查询操作,并未涉及修改数据库信息等操作,所以在单元测试中未提及@Transactional注解,这里简单介绍一下:
@Transactional注解是用于声明方法或类需要进行事务管理的注解。它可以应用在方法级别或类级别上。
当@Transactional注解被应用在方法上时,它表示该方法需要在事务控制下执行。事务是一种用于保证一组操作要么全部成功执行,要么全部回滚的机制。在方法执行期间,如果发生异常,事务将回滚,否则,事务将提交。
单元测试的方法在加了该注释后,会自动的进行回滚操作,这样保证测试的方法不会污染数据库的数据:
可以打开Mysql进行进一步验证,上述代码是将用户1的密码123456修改为了1234567:
可以观察到,数据库的数据是未发生变化的。
四、利用MyBatis实现删除操作
UserMapper.java如下所示:
UserMapper.xml如下所示:
进行测试,效果如下所示:
同样的<delete>标签用于执行删除操作,通常用于删除数据库的记录,由于其不返回任何结果,因此在MyBatis中使用delete标签时,不需要设置’resultType‘参数。
Ps:resultType参数用于指定 SQL 语句执行后返回的结果类型,它通常用于查询操作,用于映射查询结果到 Java 对象或其他类型。对于删除操作来说,我们只关心删除的行数,而不需要获取具体的结果对象,因此不需要设置 resultType参数。
五、利用MyBatis实现添加操作
UserMapper.java如下所示:
UserMapper.xml如下所示:
进行单元测试:
查询Mysql进行验证,发现确实新增了一条数据: