目录
2.3.1 有些人可能会问,为什么不使用${}来进行like查询?
2.3.3 当出现实体类类名与数据库字段名不相同的时候该怎么处理?
前言:本篇出自博主的上一篇博客快速入门MyBatis,以下操作的数据库皆为上章所提及,这里就不再演示。
在介绍单元测试之前,先来看一组操作:
以下是根据所给的 ID 来查询用户名:

注意:这里加上@Param的原因是因为可能存在部分电脑主机获取不到该参数,所以加上@Param这个注解可以确保万无一失。这里注解里面的id是获取的形参名。

如果没有使用单元测试的情况下需要验证该功能的正常性,就只能通过Service调用Mapper层,再根据Controller层调用Service层:

代码实现如下:

单元测试(unit testing),是指对软件中的最⼩可测试单元进⾏检查和验证的过程就叫单元测试。
Spring Boot 项⽬创建时会默认单元测试框架 spring-boot-test,⽽这个单元测试框架主要是依靠另⼀个著名的测试框架 JUnit 实现的:
打开 pom.xml 就可以看到,以下信息是 Spring Boot 项⽬创建是⾃动添加的:

针对第二点:

- spring.datasource.url=jdbc:mysql://localhost:3306/mycnblog?characterEncoding=utf8&useSSL=false
- spring.datasource.username=root
- # 注意!!! password 要写自己本机mysql密码,不是固定的12345678
- spring.datasource.password=12345678
- spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
- # 设置 MyBatis
- mybatis.mapper-locations=classpath:/mybatis/*Mapper.xml
- # 打印 MyBatis 执行 SQL
- mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
- logging.level.com.example.demo=debug



按照上述步骤之后,就会生成如下代码:
记得加上@SpringBootTest注解,随后完善测试方法的具体实现:
该注解表示的是当前测试方法是运行在SpringBoot环境中的。如果没有这个注解,那么也就无法注入UserMapper到该测试方法中。理由上面也说了,这是因为@SpringBootTest注解的存在,让该测试方法运行在SpringBoot中。
- 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);
- }
- }

如果当@Param里面的参数改为uid,其他地方不再进行修改,那么程序会报错嘛?
:会的,以下是运行结果

分析:
具体来说,@Param注解可以应用于方法的参数上,用于指定参数的名称。这个名称会与 SQL 查询语句中的${}或#{}配合使用,以匹配对应的参数。
通俗来说,就是@Param给形参重新起了一个名字,那么形参的名字其实就已经不能用了,所以在
SQL查询语句的时候会报 找不到“id”的错误,这是因为这时候形参已经变成 ”uid“了。如果要让程序不报错,可以把SQL查询时候的 ${id} 改成 ${uid}。

UserMapper.xml 代码如下:

UserMapper.java 如下:
获取动态参数有以下两种方式:
预编译处理 :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

运行以下测试方法进行检查功能是否正确,运行代码如下所示:


运行以下测试方法进行检查功能是否正确,运行代码如下所示:

可能观察了上面这些运行结果,并没有发现这两者之间有什么区别,这是因为之前都是使用int类型进行的传参,当使用String类型进行传参的时候,就会发送改变。
以下为 #{} 占位符模式,可以观察到程序运行正常。

接着我们使用${} 直接替换,观察运行结果,发现程序报错:

细心观察,我们发现:这是因为采用了直接替换的格式,为加任何修饰,于是程序出现了错误,就好比以下SQL代码:

这是因为采用了直接替换,没有使用引号,于是程序出现了问题,正常的sql语句应该如下:
或者可以采取这样的方式来避免刚刚的错误:
而${}所采用的直接替换的方式,容易引入SQL注入问题。 【下文会提到】
除此之外,两者还有性能的区别。
使用 #{} 时,参数值会以预编译参数的形式传递给数据库,数据库会对参数进行安全处理。这样可以提高数据库的执行效率,尤其是对于频繁执行的SQL语句,数据库可以重复使用编译好的执行计划,从而提高查询性能。
而使用 ${} 时,参数值会直接替换到SQL语句中,相当于字符串拼接。这样可能导致SQL语句的执行计划在每次执行时都需要重新编译,降低了数据库的执行效率。
因此,从性能角度考虑,推荐使用#{}来处理参数,尽量避免使用${}。特别是当参数值来自用户输入时,使用${}可能存在安全风险,同时也可能降低数据库的执行效率。
可能有的人会问:既然#{}性能比${}高,而且#{}还能防止SQL注入的问题,那么#{}存在的意义是什么?
${}存在的原因是为了一些特殊场景的需求,它提供了更大的灵活性,例如当某些场景需要根据某个数值进行排序的时候:
这里程序将desc识别为一个String类型的值,但是其实它是SQL关键字。按照程序的逻辑这里应该为这个 "String类型" 的desc加上引号,但是没有加,所以这里报错了。

当使用${}的时候,程序就会正常运行无误:

小结:
总结起来,#{}是安全的参数占位符,能够防止SQL注入攻击,而${}是简单的字符串替换,需要谨慎使用以避免安全风险。在编写SQL语句时,建议使用#{}来处理参数,尽量避免使用${},特别是当参数值来自用户输入时。
ps:在MyBatis中,无论是#{}还是${},底层的实现都是PreparedStatement,而不是Statement(Statemt执行的是不带参数的SQL语句)。
SQL注入常见于登录的时候,以下带来示例:
观察数据库我们可以明白,用户的密码是 admin:

如果在使用${}的情况下,不输入正确密码,而利用SQL注入,便可绕过验证,获取用户信息:

如果是使用#{}则不会出现这样的问题:

这是为什么呢,下面来一组图来解释一下:

需要注意的是,1是等于符号1的,这里可以利用MySQL语句进行进一步验证:

UserMapper.xml如下所示:

UserMapper.java代码如下所示:
测试代码如下:

运行结果:(程序出现错误)

2.3.1 有些人可能会问,为什么不使用${}来进行like查询?
这是因为主要涉及到SQL注入和模糊查询的问题。
其中的SQL注入就不再赘述,这里我们就展开了聊聊模糊查询的问题:
like查询通常用于模糊匹配,用户可以输入各种通配符(如 % 和_)来进行模糊查询。如果使用${}来直接替换用户输入的查询条件,可能会导致通配符不起作用或者不符合预期,因为${}是文本替换,不会对通配符进行特殊处理。而使用 #{} 时,框架会正确处理通配符,确保它们按预期工作。
'%'(百分号): 在 Like 子句中,‘%’表示零个或者多个字符的占位符。它可以匹配任意字符序列,包括空字符。下面是一些示例:
'%apple%' 匹配包含 "apple" 子串的任何字符串,如 "pineapple"、"apples"、"apple pie" 等。
'a%' 匹配以字母 "a" 开头的任何字符串,如 "apple"、"apricot" 等。
'%a' 匹配以字母 "a" 结尾的任何字符串,如 "banana"、"tuna" 等。
'%a%' 匹配包含字母 "a" 的任何位置的字符串,如 "cat"、"bat"、"hat" 等
_(下划线):在 LIKE 子句中,_ 表示一个字符的占位符。它只匹配一个字符。下面是一些示例:
'a_ple' 匹配 "apple",但不匹配 "aple" 或 "apples"。
'c_t' 匹配 "cat",但不匹配 "cart" 。
2.3.2 引入concat解决#{}的问题
可能有人觉得很奇怪,明明操作没有毛病啊,下面我们来解释一下这个原因,当使用#{}进行处理的时候,实际在Mysql中是转换成这样:

concat的介绍
为了避免出现多余的单引号,我们可以使用concat对其进行拼接,以下是示例:

可以观察到,使用concat可以对这些参数进行拼接,这些参数可以是字符串常量、列名或表达式。
以下是使用concat对原先like查询修改后的结果:

观察结果,程序运行正常,且查询到的结果与数据库吻合:
在实际开发的过程中,经常会出现数据库设置的字段名与我们实体类中的名字不相同的情况(需要注意:如果大小写不一致是不影响的):

由于MyBatis是一个ORM框架,如果两者不一致的话(不区分大小写的,例如updateTime就不受影响),是无法进行映射操作的。我们运行测试方法,发现实体类中的pwd属性的结果为空,进一步验证以上观点:

此时有两种解决方案,一种是使用Mysql提供的as关键字,另一种是使用resultMap。
①使用as关键字解决上述问题,如下所示:

②使用resultMap:
这里利用resultMap中的id 和result标签完成映射,拿到了pwd的值:
分析:
1.id 标签:id 标签用于指定主键列的映射关系。它定义了将查询结果中的某一列映射到目标对象的主键属性上。主键属性通常是唯一标识一个对象的属性。在 id 标签中,需要指定两个属性:
2.result 标签:result 标签用于指定普通属性的映射关系。它定义了将查询结果中的某一列映射到目标对象的普通属性上。普通属性是对象中除主键属性外的其他属性。在 result 标签中,需要指定两个属性:
输入文章id得到文章详情信息,以下是ArticleMapper.xml实现代码,以及数据库文章表和作者表的情况:


ArticleMapper.java实现代码:
以下是ArticleInfo和ArticleInfoVo的实现:
运行测试代码,效果如下所示:

分析发现:只有ArticleInfoVo的属性,并没有打印出父类的属性。
初步判断是使用Lombok插件所导致的,让articleinfovo在使用toString方法的时候只打印自己的属性而不打印父类的属性:
接下来我们查看target目标文件夹下的字节码文件来验证:

通过观察,我们确信是因为我们使用Lombok插件所导致的这一现象。
解决方案:在ArticleInfoVo中重写toString方法(在使用Lombok插件时,如果自己重写的方法与插件所提供的方法相冲突,以自己书写的方法为主):

Ps:一定要选择重写类型,否则默认情况下重写的toString方法没有继承父类属性。
再次运行测试方法,即可打印父类属性:

同样的,我们先来看看数据库中两张表的信息:

articleMapper如下:

分析可得,主表应该选择我们的文章表。

当然如果想使用单线程的方式打印,可以使用
list.stream().forEach(System.out::println);
UserMapper.java的代码:

UserMapper.xml的代码:

注:在MyBatis的<update>标签中,不需要设置resultType参数的原因是因为
进行测试,效果如下所示:

由于前面都是使用查询操作,并未涉及修改数据库信息等操作,所以在单元测试中未提及@Transactional注解,这里简单介绍一下:
@Transactional注解是用于声明方法或类需要进行事务管理的注解。它可以应用在方法级别或类级别上。
当@Transactional注解被应用在方法上时,它表示该方法需要在事务控制下执行。事务是一种用于保证一组操作要么全部成功执行,要么全部回滚的机制。在方法执行期间,如果发生异常,事务将回滚,否则,事务将提交。
单元测试的方法在加了该注释后,会自动的进行回滚操作,这样保证测试的方法不会污染数据库的数据:

可以打开Mysql进行进一步验证,上述代码是将用户1的密码123456修改为了1234567:

可以观察到,数据库的数据是未发生变化的。
UserMapper.java如下所示:

UserMapper.xml如下所示:
进行测试,效果如下所示:
同样的
Ps:resultType参数用于指定 SQL 语句执行后返回的结果类型,它通常用于查询操作,用于映射查询结果到 Java 对象或其他类型。对于删除操作来说,我们只关心删除的行数,而不需要获取具体的结果对象,因此不需要设置 resultType参数。
UserMapper.java如下所示:

UserMapper.xml如下所示:

进行单元测试:
查询Mysql进行验证,发现确实新增了一条数据:
