文章目录
在前面我们已经学了这个Spring、Spring Boot Spring MVC三个框架,然后接下来就是学习MyBatis框架,他也是核心框架之一,将前端发送的数据存储起来,或者根据前端请求的数据来进行查询之类的操作
MyBatis是什么?
MyBatis 是⼀款优秀的持久层框架,它⽀持⾃定义 SQL、存储过程以及⾼级映射。MyBatis 去除了⼏乎 所有的 JDBC 代码以及设置参数和获取结果集的⼯作。MyBatis 可以通过简单的 XML 或注解来配置和 映射原始类型、接⼝和 Java POJO(Plain Old Java Objects,普通⽼式 Java 对象)为数据库中的记 录。
实际上MyBatis最大的用处就是祛除了几乎所有的JDBC代码以及设置参数和获取结果集的工作,之前我们在自己手写JDBC代码的时候就会觉得很冗杂,很多代码都是重复的,这大大导致我们的开发效率变得极低,MyBatis就帮我们简化了这一部分,但实际上它还是基于JDBC来实现的,接下来的介绍会更清晰明朗
简单来说 MyBatis 是更简单完成程序和数据库交互的⼯具,也就是更简单的操作和读取数据库⼯具
为什么要学MyBatis?
对于后端开发来说,最重要的两部分就是:
- 后端处理逻辑
- 数据库
而我们还需要去将这两部分联系起来,就需要通过数据库连接工具那数据库连接⼯具有哪些?⽐如之前我们 学习的 JDBC,还有今天我们将要介绍的 MyBatis,那已经有了 JDBC 了,为什么还要学习 MyBatis? 这是因为 JDBC 的操作太繁琐了,我们回顾⼀下 JDBC 的操作流程:
- 创建数据库连接池 DataSource
- 通过 DataSource 获取数据库连接 Connection
- 编写要执⾏带 ? 占位符的 SQL 语句
- 通过 Connection 及 SQL 创建操作命令对象 Statement
- 替换占位符:指定要替换的数据库字段类型,占位符索引及要替换的值
- 使⽤ Statement 执⾏ SQL 语句
- 查询操作:返回结果集 ResultSet,更新操作:返回更新的数量
- 处理结果集
- 释放资源
演示JDBC代码
-- 创建数据库
create database if not exists `library` default character set utf8mb4;
-- 使⽤数据库
use library;
-- 创建表
create table if not exists `soft_bookrack` (
`book_name` varchar(32) NOT NULL,
`book_author` varchar(32) NOT NULL,
`book_isbn` varchar(32) NOT NULL primary key
) ;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
public class SimpleJdbcOperation {
private final DataSource dataSource;
public SimpleJdbcOperation(DataSource dataSource) {
this.dataSource = dataSource;
}
/**
* 添加⼀本书
*/
public void addBook() {
Connection connection = null;
PreparedStatement stmt = null;
try {
//获取数据库连接
connection = dataSource.getConnection();
//创建语句
stmt = connection.prepareStatement(
"insert into soft_bookrack (book_name, book_author,
book_isbn) values (?,?,?);"
);
//参数绑定
stmt.setString(1, "Spring in Action");
stmt.setString(2, "Craig Walls");
stmt.setString(3, "9787115417305");
//执⾏语句
stmt.execute();
} catch (SQLException e) {
//处理异常信息
} finally {
//清理资源
try {
if (stmt != null) {
stmt.close();
}
if (connection != null) {
connection.close();
}
} catch (SQLException e) {
}
}
}
由此知道 JDBC是很麻烦的,所以我们换一种连接工具( MyBatis)它可以帮助我们更⽅便、更快速的操作数据 库
学习MyBatis
- 配置MyBatis开发环境
- 使⽤ MyBatis 模式和语法操作数据库。
配置框架和搭建环境
这是新项目去添加MyBatis的框架和具体的数据库驱动
老项目的话还是pom.xml中右键gengerate就好了
在添加完项目进去后需要先不要急着启动项目!!!
咱们先去配置文件 dev 中去配置数据库信息,然后才能启动
这个url后面的&serverTimezone=Asia/Shanghai等会再去解释
这里我的应该是大于5.x,不然不加cj的话 我这里会报错操作到这里其实参不多配置好了,然后注意的是数据库的信息是在dev开发配置文件中写入的,在主配置文件中需要去声明一下,这都是前面的知识了
接下来就需要具体讲讲MyBatis的具体知识才能执行下面的操作
MyBatis到底是如何去作为数据库连接工具的呢?
它本身也是基于JDBC的,之前我们是手动去写JDBC代码才显得那么麻烦,而MyBatis它本身就有有两大要点
- interface接口去声明CURD方法
- xxx.xml文件去实现具体的方法
配置xml文件的路径
接下来我们去配置xxx.xml的存放路径,这个xml文件不是一般的xml文件,它是专门来重写interface接口类中的方法的,所以我们需要给这些xml文件设置存放路径
设置存放路径 首先需要在全局配置文件中声明,因为不管你是开发环境还是生产环境,这个存放路径都是不变的,在这个项目的都是某个地方
其次这个格式可以记一下,resource包中建一个mybatis包
classpath就是指路径,mybatis包下的**以Mapper.xml为结尾的文件,这都是规范,不会报错,但是不一致的话不符合规范
xml的配置路径也完成了
定义接口
关于接口的定义,我们最好是建一个包,该包下都存放接口类
咱们就建一个Mapper包,里面去定义接口
关于接口这边有一个@Mapper注解,在注释中已经解释了,目前里面只有一个声明方法getUserById,通过id去查询这个对象,在参数里面还有一个注解是@Param,这个参数里面的id就是对应xml里面的id,如果不加上这个注解,可能有少部分的JDK用户是会报错的,所以加上更加保险实体类也已经创建好了
接口这里也结束了,接下来开始创建xml文件
创建xml文件
之前我们已经声明了xml文件的创建路径是在mybatis包下创建
其余的格式都是固定的
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!-- namespace 要设置是实现接口的具体包名加类名 -->
<mapper namespace="">
</mapper>
从上图中可以看到,这个xml文件是针对一张表进行CURD操作的,这个xml可以去实现一个接口中多个方法的重写,重写的格式就是上图所示
那么这里的 #{id} 就是前端先去通过controller ,然后controller再去调用service,service调用接口UserMapper,这个接口类中的方法又被重写了,xml和interface共同结合生成SQL语句调用JDBC,去数据库里进行查询,然后得结果还是按照顺序被返回回去了
以下是对以上标签的说明: 标签:需要指定 namespace 属性,表示命名空间,值为 mapper 接⼝的全限定 名,包括全包名.类名。 查询标签:是⽤来执⾏数据库的查询操作的: id:是和 Interface(接⼝)中定义的⽅法名称⼀样的,表示对接⼝的具体实现⽅法。 resultType:是返回的数据类型,也就是开头我们定义的实体类
service层其实就是作为中间商去联系着Controller和Mapper层
记住,在验证参数的合理性的时候是在controller层验证的,而service就是组合参数层
但是当我们写完mapper里面的代码进行测试的话,就很麻烦,成本是很高的,因为上面测试查询结果已经演示一遍了,你需要把interface和xml的代码写完了之后,还要去写service和controller的代码再去游览器输入url来测试,这对我们测试某一个接口测试的很麻烦
所以就有了单元测试的由来
单元测试
单元测试(unit testing),是指对软件中的最⼩可测试单元进⾏检查和验证的过程就叫单元测试
这个最小可测试单元在Spring Boot项目中指的是方法级别
每个方法都有相应的实现,你完全可以写一个方法去测试一个方法这是最小的可测试单元
单元测试是开发者编写的⼀⼩段代码,⽤于检验被测代码的⼀个很⼩的、很明确的(代码)功能是否正 确。执⾏单元测试就是为了证明某段代码的执⾏结果是否符合我们的预期。如果测试结果符合我们的预 期,称之为测试通过,否则就是测试未通过(或者叫测试失败)。
单元测试的好处
- 单元测试不⽤启动 tomcat;
- 如果中途改动了代码,在项⽬打包的时候会发现错误,因为打包的时候会 ⾃动执⾏单元测试,单元 测试错误就会发现。
- 可以不去污染数据库
其中第⼆点最重要。
关于这个不用启动tomcat有点争议(应该是针对其他项目),因为tomcat本身就是内置在spring框架中,这个我们不管
但是第二点是很重要的,因为他先去执行单元测试,如果测试通过就打包成功,否则就会打包失败
在写完一个功能进行测试的时候,是可以去快速的检测这个功能是否是正确的
在不使用单元测试的话,是会污染数据库的,而使用单元测试的时候,我们可以通过添加/修改数据等等来进行单元测试,并且还可以不对数据库进行修改
实际上就是数据库本身就是关闭事务的,当你在使用单元测试的时候可以开启事务,然后测试完进行回滚就行,后面再去演示 …
Spring Boot的单元测试
因为这是Spring Boot项目,本身也就内置了单元测试框架
⽽ spring-boot-starter-test 的 MANIFEST.MF(Manifest ⽂件是⽤来定义扩展或档案打包的相关信 息的)⾥⾯有具体的说明,如下信息所示:
所以我们是可以直接进行测试的,实际上测试很简单,我就直接奔入主题了
生成单元测试类
断言返回的结果本身是布尔类型,为true就是测试通过,为false就是测试失败
断⾔:如果断⾔失败,则后⾯的代码都不会执⾏。
后面别的测试还会继续去进行断言测试
这就将上面的哪个区问题解决明白了
前面已经把select查询了 接下来就是把update/add/delete都操作一下
update
接口
xml
测试
终极测试
写到这里我有有个问题…
我把@Param(username) 改成 name行不行呢???
在测试方法上,还可以去加上@Transaction注解,作用就是执行完自动回滚事务
不污染/修改 数据库的数据
因为我们数据库本身是默认关闭事物的,其实他是先开启事务,然后再去进行rollback操作的
delete
添加了@Transactional之后,发生了事务回滚,所以数据不变
add
上面的添加返回的是受影响的行数
add还有第二种方法稍微麻烦点
Controller实现
接口实现
mapper.xml实现
useGeneratedKeys:这会令 MyBatis 使⽤ JDBC 的 getGeneratedKeys ⽅法来取出由数据 库内部⽣成的主键(⽐如:像 MySQL 和 SQL Server 这样的关系型数据库管理系统的⾃动递 增字段),默认值:false。
keyColumn:设置⽣成键值在表中的列名,在某些数据库(像 PostgreSQL)中,当主键列 不是表中的第⼀列的时候,是必须设置的。如果⽣成列不⽌⼀个,可以⽤逗号分隔多个属性 名称。
keyProperty:指定能够唯⼀识别对象的属性,MyBatis 会使⽤ getGeneratedKeys 的返回值 或 insert 语句的 selectKey ⼦元素设置它的值,默认值:未设置(unset)。如果⽣成列不⽌ ⼀个,可以⽤逗号分隔多个属性名称。
参数占位符 #{} 和 ${}
- #{} 预编译处理
- ${} 字符直接替换
预编译处理是指:MyBatis 在处理#{}时,会将 SQL 中的 #{} 替换为?号,使⽤ PreparedStatement 的 set ⽅法来赋值。直接替换:是MyBatis 在处理 ${} 时,就是把 ${} 替换成变量的值
预编译处理和字符串替换的区别故事(头等舱和经济舱乘机分离的故事): 在坐⻜机的时候头等舱和经济舱的区别是很⼤的,如下图所示:
⼀般航空公司乘机都是头等舱和经济舱分离的,头等舱的⼈先登机,登机完之后,封闭经济舱,然后再 让经济舱的乘客登机,这样的好处是可以避免浑⽔摸⻥,经济舱的⼈混到头等舱的情况,这就相当于预 处理,可以解决程序中不安全(越权处理)的问题。
⽽直接替换的情况相当于,头等舱和经济舱不分离的情况,这样经济舱的乘客在通过安检之后可能越权 摸到头等舱,如下图所示
这就相当于参数直接替换,它的问题是可能会带来越权查询和操作数据等问题,⽐如后⾯会讲的 SQL 注 ⼊问题。
接下来我们通过代码演示一下
这句话很重要
之前我们update操作是没问题的,当时时通过id去修改用户名
但是当时的用户名是通过${username} 来表示的,现在我改过来了,改成 ${username}来表示
结果测试就出来问题,因为 通过 ${} 来表示的参数,如果传过来的是 String类型,他是不会加上单引号的
从这条报错语句中看出来, username = zhangsan 是没有加上单引号的,此时SQL语句就会报错, 而后面的id是预处理 直接转换成了 ? 来表示
既然 ${} 来表示字符串的时候不会加上单引号,那么 ${} 有啥用呢???
接下来我们看另外一个场景
此时要去查询排序的数据,但是前端传过来的参数是正序还是反序 ,后端的处理逻辑要进行改变了
上面是测试 ${} 这个参数占位符, 上面咱们说的 ${} 是不加单引号的,上面传的参数传的
判断是正序还是倒叙,它虽然是个字符串,但是他却不需要单引号来引起来
那么这种场景将 ${} 的作用发挥了出来
向 $ {} 这样的参数占位符要是要去来传递参数 并不是 sql关键字 但是又要用${} 怎么办??
可以写成这种格式
SQL注入问题
当数据库中只有一条用户信息的时候
并且在xml中还是通过 ${} 来传递参数,但是我们补上了漏洞,在 ${}的两边加上了单引号,按理说是没问题的,但是测试就出了问题
明明传输的密码是错误的,依旧还是登上上去了,这个就是很大的麻烦
这就是SQL注入的一个问题所在,注意 密码的格式是
’ or 1='1,这个格式的密码在控制台打印是这样的
password=‘’ 但是 or = 1=‘1’ 是对的,所以就利用了这个BUG来进行登录成功,但是这个数据库用户表中只有一个用户存在的情况,即使是这样,也是很麻烦的事
所以建议在传递普通参数时,用#{} 传递 SQL关键字参数时,用 ${}
特殊查询 like查询
接下来演示一下特殊查询 (like)
实际上测试是报错的,从报错信息中的SQL语句中发现,
Preparing: select * from userinfo where username like ‘%?%’
因为用的是 #{} ,这边 ? 发现占位符的类型是 String,在替换 ? 的时候实际上会加上单引号
所以最后变成了
Preparing: select * from userinfo where username like ‘%‘a’%’
这样一来,里面的a两边又出现了单引号
那该咋办呢???
那么有的人就会想办法,用#{} 会加上单引号,那么我们就用 ${} 就好了呀
在讲 ${} 那时候,咱们知道 ${} 最好是用来传递 关键字参数比较好,如果一定他要去通过 来传递普通参数的话 , 会出现 S Q L 注入的问题 , 其实在 S Q L 注入问题上 , 有解决办法的 , 就是你传递过来的字段是可以穷举的 , 就比如你在传递 d e s c 还是 a s c 的时候 , 你在 c o n t r o l l e r 层去判断是否是其中两种之一 , 否则的话注入的问题就解决不了那么在 l i k e 这种场景中是无法穷举的 , 所以 S Q L 注入无法解决 , {} 来传递普通参数的话,会出现SQL注入的问题, 其实在SQL注入问题上,有解决办法的,就是你传递过来的字段是可以穷举的,就比如你在传递 desc还是asc的时候,你在controller层去判断是否是其中两种之一,否则的话注入的问题就解决不了 那么在like这种场景中是无法穷举的,所以SQL注入无法解决, 来传递普通参数的话,会出现SQL注入的问题,其实在SQL注入问题上,有解决办法的,就是你传递过来的字段是可以穷举的,就比如你在传递desc还是asc的时候,你在controller层去判断是否是其中两种之一,否则的话注入的问题就解决不了那么在like这种场景中是无法穷举的,所以SQL注入无法解决,{} 也就无法使用
最合适的方法就是使用mysql本身内置的concat函数对 % username % 三个字符串进行拼接即可
这样就解决了like的问题,切记模糊查询是个特例,需要用到concat函数
resultMap VS resultType
这个就是处理当 字段名和对象名不一致的情况下如何解决
假设 字段名是 username 但是对象名是name的话
这就是根据id去查询用户,但是
name 为null,因为数据库中的是username,所以要去设置主键映射和不同属性名的映射,这两点事最基本的
这个时候用resultMap,然后去设置映射处理,先去设置id参数,
id= ‘‘BaseMap’’,resultMap = '‘Based’'这个要对应上,其次就是
设置主键映射,然后就是设置出现问题的字段和属性名的映射,就好了
多表查询
之前我们在xml和接口中用的都是单表查询,但是实际应用中会用到多表查询,接下来演示一下
如果是增、删、改返回搜影响的⾏数,那么在 mapper.xml 中是可以不设置返回的类型的,如下图所 示:
然⽽即使是最简单查询⽤户的名称也要设置返回的类型,否则会出现如下错误。 查询不设置返回类型的错误示例演示
在xml中不设置返回值(结果映射属性) 那么在postman上测试就会报错
显示运⾏了⼀个查询但没有找到结果映射,也就是说对于 查询标签来说⾄少需要两个属性
- id 属性:⽤于标识实现接⼝中的那个⽅法;
- 结果映射属性:结果映射有两种实现标签:resultMap 和 resultType
返回类型:resultType
绝⼤数查询场景可以使⽤ resultType 进⾏返回
它的优点是使⽤⽅便,直接定义到某个实体类即可,resultType的vaule值就是包名+实体类型名(之前有说过)
返回字典映射:resultMapresult
resultMap的应用场景:
- 字段名称和程序中的属性名不同的情况,可使⽤ resultMap 配置映射;
- ⼀对⼀和⼀对多关系可以使⽤ resultMap 映射并查询数据。
字段名和属性名不同的情况
假设一种场景,在数据库中用户名是 username ,而程序中属性名是name
由这个例子就知道当 数据库中的字段名和程序中的属性名不一致的时候就会出错,解决办法就是使用resultMap
可能有人会说,为什么不去改数据库中的字段名或者程序的属性名呢???
实际上这个时候改动不太好,不一定能照顾到全面,还不如就将错就错,但是要让他们两个对应上去就行了
这边查询就是没问题的啦
一对一的表映射
⼀对⼀映射要使⽤ 标签,具体实现如下(⼀篇⽂章只对应⼀个作者):
这里查询不出来文章的作者信息,说明resultType这个小弟不行,我们得喊resultMap这个大哥来解决问题
上面的操作有两点没有注意到,第一就是不应该用resultType,第二就是查询语句也是有问题的,需要改成表连接查询
还有一点就是使⽤ 标签
接下来重新展示一遍
主要是ArticleMapper.xml文件中和UserMapper.xml文件中
ArticleMapper.xml
> UserMapper.xml
假设我不加这个参数
这样才算完满解决…
还有一个细节注意一下:
假设我传入文章id =1 ,然后uid = 2 ,说明这篇文章的作者id 是 2
但是数据库中没有这个用户
接下里看一下查询结果…
按理说没有id = 1 的这个用户,应该要去显示userinfo = null
但是并不全是,原因在于, 文章表和用户表有些字段重复了,那些重复的值被用户表中的字段拿去了,这个细节记住一下
一对多的表映射
到这里就结束了resultMap的知识点
复杂情况:动态SQL使⽤
动态 sql 是Mybatis的强⼤特性之⼀,能够完成不同条件下不同的 sql 拼接。 可以参考官⽅⽂档:MyBatis动态sql
if标签
在注册⽤户的时候,可能会有这样⼀个问题,如下图所示
有一些非必填字段,你可以选择填入也可以选择不填入,这对用户来说是很人性化的操作,但是对于程序员来说是需要掌握动态sql才能解决的
注册分为两种字段:必填字段和⾮必填字段,那如果在添加⽤户的时候有不确定的字段传⼊,程序应该 如何实现呢? 这个时候就需要使⽤动态标签 if标签 来判断了,⽐如添加的时候性别 sex 为⾮必填字段,具体实现如下
假设需要去添加一个用户的信息,但是呢,这个用户的有些信息在sql语句中可能传入了,也可能不传入,这个时候sql语句的改动就很大
语法:
插入成功了,并且此时photo是没传入的,那么咱们再来个传入photo的
trim标签
之前的插⼊⽤户功能,只是有⼀个 sex 字段可能是选填项,如果有多个字段,⼀般考虑使⽤ trim 标签 结合 if 标签,对多个字段都采取动态⽣成的⽅式
调整 UserMapper.xml 的插⼊语句为
where标签
代码展示:
where标签还有个很重要的功能就是去除and字符
set标签
set标签也是结合if标签来使用的,当if中的条件满足是就会拼接上set标签
还有一点就是它会去除最后一个参数后面的逗号,假设这个password是null,那么这个时候会自动去除屌username的逗号
foreach标签
相当于通过一个循环来进行删除,假设我要删除id为(88,99,100)的这三个用户,我需要一步步的删除就比较麻烦,这个时候可以通过foreach标签