经过前⾯文章的学习,咱们
Spring 系列的基本操作已经实现的差不多了,接下来,让我们来一起学习更重要的知识——将前端传递的数据存储起来,或者查询数据库⾥⾯的数据。
MyBatis 是⼀款优秀的ORM(对象关系映射)持久层框架,它⽀持⾃定义 SQL、存储过程以及⾼级映射。MyBatis 去除了几乎所有的 JDBC代码以及设置参数和获取结果集的⼯作。MyBatis 可以通过简单的 XML 或注解来配置和 映射原始类型、接⼝和 Java POJO(Plain Old Java Objects,普通⽼式 Java 对象)为数据库中的记录。MyBatis官网
简单来说:MyBatis 是更简单完成程序和数据库交互的⼯具,也就是更简单的操作和读取数据库⼯具。
对于后端开发来说,程序是由以下两个重要的部分组成的:

⽽这两个重要的组成部分要通讯,就要依靠数据库连接⼯具,那数据库连接⼯具有哪些❓ 比如之前我们学习的 JDBC,还有今天我们将要介绍的 MyBatis,那么已经有了 JDBC 了,为什么还要学习 MyBatis呢❓
这是因为 JDBC 的操作太繁琐了,我们先来回顾⼀下 JDBC 的操作流程:
从上述操作流程可以看出,对于 JDBC 来说,整个操作⾮常的繁琐,我们不但要拼接每⼀个参数,⽽且还要按照模板代码的⽅式,⼀步步的操作数据库,并且在每次操作完,还要⼿动关闭连接等,⽽所有的这些操作步骤都需要在每个⽅法中重复书写。
于是我们就想,那有没有⼀种方法,可以更简单、更方便的操作数据库呢?
答案是肯定的,这就是我们要学习 MyBatis 的真正原因,它可以帮助我们更方便、更快速的操作数据库。
MyBatis 学习只分为两部分:
开始搭建 MyBatis 之前,我们先来看⼀下 MyBatis 在整个框架中的定位,框架交互流程图:

MyBatis 也是⼀个 ORM 框架,ORM(Object Relational Mapping),即对象关系映射。在⾯向对
象编程语⾔中,将关系型数据库中的数据与对象建⽴起映射关系,进⽽⾃动的完成数据与对象的
互相转换:
ORM 把数据库映射为对象:
⼀般的 ORM 框架,会将数据库模型的每张表都映射为⼀个 Java 类。
也就是说使⽤ MyBatis 可以像操作对象⼀样来操作数据库中的表,可以实现对象和数据库表之间 的转换,接下来我们来看 MyBatis 的使⽤吧。
接下来我们要实现的功能是:使⽤ MyBatis 的⽅式来读取⽤户表中的所有⽤户,我们使⽤个⼈博
客的数据库和数据包,具体 SQL 如下:
-- 创建数据库
drop database if exists mycnblog;
create database mycnblog DEFAULT CHARACTER SET utf8mb4;
-- 使用数据数据
use mycnblog;
-- 创建表[用户表]
drop table if exists userinfo;
create table userinfo(
id int primary key auto_increment,
username varchar(100) not null,
password varchar(32) not null,
photo varchar(500) default '',
createtime datetime default now(),
updatetime datetime default now(),
`state` int default 1
) default charset 'utf8mb4';
-- 创建文章表
drop table if exists articleinfo;
create table articleinfo(
id int primary key auto_increment,
title varchar(100) not null,
content text not null,
createtime datetime default now(),
updatetime datetime default now(),
uid int not null,
rcount int not null default 1,
`state` int default 1
)default charset 'utf8mb4';
-- 创建视频表
drop table if exists videoinfo;
create table videoinfo(
vid int primary key,
`title` varchar(250),
`url` varchar(1000),
createtime datetime default now(),
updatetime datetime default now(),
uid int
)default charset 'utf8mb4';
-- 添加一个用户信息
INSERT INTO `mycnblog`.`userinfo` (`id`, `username`, `password`, `photo`, `createtime`, `updatetime`, `state`) VALUES
(1, 'admin', 'admin', '', '2021-12-06 17:10:48', '2021-12-06 17:10:48', 1);
-- 文章添加测试数据
insert into articleinfo(title,content,uid)
values('Java','Java正文',1);
-- 添加视频
insert into videoinfo(vid,title,url,uid) values(1,'java title','http://www.baidu.com',1);





首先请参考➡️ 准备工作—添加 lombok 到项目中
然后再进行下面的配置:

添加好依赖以后千万不要立即启动项目❗❗千万不要❗❗❗


注意事项: 如果使⽤
MySQL 是 5.x 之前的使⽤的是“com.mysql.jdbc.Driver”,如果是⼤于 5.x使⽤的是“com.mysql.cj.jdbc.Driver”;mysql Driver默认是8.0,如果是8.0以上,则driver-class-name使用的是“com.mysql.cj.jdbc.Driver”,如果是8.0之前的,则使用的是“com.mysql.jdbc.Driver”;如果你创建的是新项目,并且spring boot版本号在2.6.9之前的,那么就只有“com.mysql.cj.jdbc.Driver”这一种写法。

MyBatis模式如图所示:

package com.example.demo.mapper;
import com.example.demo.model.UserInfo;
import org.apache.ibatis.annotations.Mapper;
@Mapper // mybatis interface
public interface UserMapper {
// 根据用户id查询用户
public UserInfo getUserById(Integer id);
}
<?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="com.example.demo.mapper.UserMapper">
<select id="getUserById" resultType="com.example.demo.model.UserInfo">
select * from userinfo where id=${id}
</select>
</mapper>

先添加⽤户的实体类:
package com.example.demo.model;
import lombok.Data;
/**
* 普通实体类
*/
@Data
public class UserInfo {
private int id;
private String username;
private String password;
private String photo;
private String createtime;
private String updatetime;
private int state;
}
服务层实现代码如下:
package com.example.demo.service;
import com.example.demo.mapper.UserMapper;
import com.example.demo.model.UserInfo;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
@Service
public class UserService {
@Resource
private UserMapper userMapper;
public UserInfo getUserById(Integer id){
return userMapper.getUserById(id);
}
}
控制器层的实现代码如下:
package com.example.demo.controller;
import com.example.demo.model.UserInfo;
import com.example.demo.service.UserService;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
@RestController
@RequestMapping("/user")
public class UserController {
@Resource
private UserService userService;
@RequestMapping("/getuserbyid")
public UserInfo getUserById(Integer id){
if(id==null)
return null;
return userService.getUserById(id);
}
}
以上代码写完,整个 MyBatis 的查询功能就实现完了,测试结果如下:

至此我们发现,要验证功能的正确性过于繁琐了,那么如果我们只是想单纯的测试代码的正确性怎么办呢❓🤔这个时候就可以使用SpringBoot单元测试,请参考➡️SpringBoot单元测试
在进行本地调试时,为了方便更加直观的查看跟数据库交互的具体情况,我们可以进行sql打印。
# 开启 MyBatis SQL 打印
logging:
level:
com:
example:
demo: debug
mybatis:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl


接下来,我们来实现⼀下⽤户的增加、删除和修改的操作,对应使⽤ MyBatis 的标签如下:
1、在mapper(interface)里面添加方法声明

2、在xml 中实现添加业务

进行单元测试:

效果图如下:

1、添加方法声明

2、xml 实现方法

进行单元测试:

效果图如下:


1、在interface里面添加修改方法的声明

2、在xml 中添加接口的实现标签和具体的执行sql

进行单元测试:

效果图如下:

在不需要数据的前提下,执行单元测试:(这是单元测试其中的一个好处:使用单元测试,在测试功能的时候,可以不污染连接的数据库,也就是可以不对数据库进行任何改变的情况下,测试功能)


1、在mapper(interface)里面添加删除的代码声明

2、在xml 中添加< delete >标签和删除的sql 编写

进行单元测试:

效果图如下:

使用 #{} 得到JDBC的代码如下(针对int类型的参数):

使用 ${} 得到JDBC的代码如下(针对int类型的参数):

使用 #{} 得到JDBC的代码如下(针对string类型的参数):

使用 ${} 得到JDBC的代码如下(针对string类型的参数):
在直接替换的时候并没有给字符串的value值加上单引号


#{} 和 $ {}的区别:
定义不同:#{} 预处理;$ {} 是直接替换。使用不同: #{}适用于所有类型的参数匹配;但$ {}只适用数值类型。安全性不同: #{}性能高,并且没有安全问题;但$ {}存在SQL注入的安全问题。
当我们使用#{}时,效果图如下:

小结:当传递的是一个SQL关键字(SQL 命令)的时候,只能使用${}, 此时如果使用#{}就会认为传递的为一个普通的值,而非SQL命令,所以执行就会报错。
$ {} 注意事项:当不得不使用${}时,那么一定要在业务代码中,对传递的值进行安全效验。
以登录功能来举例:


进行测试:




以上情况均符合我们的预期,用户名和密码都正确才能拿到用户信息,而有一方错误的话,用户信息则为null。但是,当我们把密码改成下面这个样子的时候,就超出我们的预期了
SQL注入演示($()):



当我们使用$()的时候会发生SQL注入问题,那么当我们使用#()会不会出现这个问题呢❓🤔

其他不变,密码还是那个密码,看是否会发生SQL注入。效果图如下:

如上图所示,在密码错误的情况下,并没有拿到用户信息,也就是没有发生SQL注入问题😊😊😊
使用#()时:


进行测试:

效果图如下:

通过上图发现有报错,这是因为使用#(),它会使用 ?进行预处理,而 ?是string 类型的,所以在预处理的时候它会给你拼个单引号,而最终的结果就是:

那么既然使用#()不行,就使用$()试试:

测试结果如下:

上面的结果看起来没有问题,我们也成功的得到了用户信息,但是我们在之前说过一个问题,就是在使用
$()时一定要对这个参数的有效性进行校验,上面我们在进行排序的时候进行校验,要么是升序要么是降序排序,是可以穷举的,但是这次根据名称进行模糊查询,需要校验的可能性太多了,因为你不知道用户会输入什么进行查询,所以在业务层的值无法穷举,那么也就导致了SQL注入的问题。
使用#()报错,但使用$()在业务层的值又不能穷举,那么该如何处理这个问题呢❓🤔
此时我们可以考虑使⽤ mysql 的内置函数 concat() 来处理

测试结果如下:

绝⼤数查询场景可以使⽤ resultType 进⾏返回,如下代码所示:
<!--根据id查询用户-->
<select id="getUserById" resultType="com.example.demo.model.UserInfo">
select * from userinfo where id=${id}
</select>
它的优点是使⽤⽅便,直接定义到某个实体类即可。
resultMap 使⽤场景:
字段名和属性名不同的情况:

测试结果如下:

这个时候就可以使⽤ resultMap 了,resultMap 的使⽤如下:
<resultMap id="BaseMap" type="com.example.demo.model.UserInfo">
<!--主键映射-->
<id column="id" property="id"></id>
<!--主普通属性映射-->
<result column="username" property="name"></result>
</resultMap>

测试结果如下:

⼀对⼀映射要使用 标签,具体实现如下(⼀篇⽂章只对应⼀个作者):
model层:
package com.example.demo.model;
import lombok.Data;
@Data
public class ArticleInfo {
private int id;
private String title;
private String content;
private String createtime;
private String updatetime;
private int uid;
private int rcount;
private int state;
private UserInfo userInfo;// 多了一个外键对象属性
}
mapper层:
package com.example.demo.mapper;
import com.example.demo.model.ArticleInfo;
import org.apache.ibatis.annotations.Mapper;
@Mapper // 一定不要忽略此注解
public interface ArticleMapper {
// 根据文章id查询文章
public ArticleInfo getArticleById(Integer id);
}
XML层(实现上面的接口):
<?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="com.example.demo.mapper.ArticleMapper">
<resultMap id="BaseMapper" type="com.example.demo.model.ArticleInfo">
<id column="id" property="id"></id>
<result column="title" property="title"></result>
<result column="content" property="content"></result>
<result column="createtime" property="createtime"></result>
<result column="updatetime" property="updatetime"></result>
<result column="uid" property="uid"></result>
<result column="rcount" property="rcount"></result>
<result column="state" property="state"></result>
<association property="userInfo" resultMap="com.example.demo.mapper.UserMapper.BaseMap"
columnPrefix="u_"></association>
</resultMap>
<select id="getArticleById" resultMap="BaseMapper">
select a.*,u.id u_id,u.username u_username,u.password u_password,u.photo u_photo,u.createtime u_createtime,u.updatetime u_updatetime,u.state u_state
from articleinfo a left join userinfo u on a.uid=u.id where a.id=#{id}
</select>
</mapper>
单元测试代码:
package com.example.demo.mapper;
import com.example.demo.model.ArticleInfo;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import javax.annotation.Resource;
import static org.junit.jupiter.api.Assertions.*;
@Slf4j
@SpringBootTest
class ArticleMapperTest {
@Resource
private ArticleMapper articleMapper;
@Test
void getArticleById() {
ArticleInfo articleInfo= articleMapper.getArticleById(1);
log.info("文章详情:"+articleInfo);
}
}
结果图如下:

以上使⽤ 标签,表示⼀对⼀的结果映射:
property 属性:指定 Article 中对应的属性,即⽤户。resultMap 属性:指定关联的结果集映射,将基于该映射配置来组织⽤户数据。columnPrefix 属性:绑定⼀对⼀对象时,是通过columnPrefix+association.resultMap.column 来映射结果集字段。association.resultMap.column是指 标签中 resultMap属性,对应的结果集映射中,column字段。注意事项: column不能省略

⼀对多需要使⽤ 标签,⽤法和 相同,如下所示:
model层:
package com.example.demo.model;
import lombok.Data;
import java.util.List;
/**
* 普通实体类
*/
@Data
public class UserInfo {
private Integer id;
private String name;
private String password;
private String photo;
private String createtime;
private String updatetime;
private int state;
private List<ArticleInfo> artlist;// 增加了文章这个外键对象属性
}
mapper层:
// 根据用户id查询用户及用户发表的所有文章
public UserInfo getUserAndArticleByUid(@Param("uid") Integer uid);
XML层(实现上面的接口):
<resultMap id="BaseMap" type="com.example.demo.model.UserInfo">
<!--主键映射-->
<id column="id" property="id"></id>
<!--主普通属性映射-->
<result column="username" property="name"></result>
<result column="password" property="password"></result>
<result column="photo" property="photo"></result>
<result column="createtime" property="createtime"></result>
<result column="updatetime" property="updatetime"></result>
<result column="state" property="state"></result>
<collection property="artlist" resultMap="com.example.demo.mapper.ArticleMapper.BaseMapper"
columnPrefix="a_"></collection>
</resultMap>
<select id="getUserAndArticleByUid" resultMap="BaseMap">
select u.*,a.id a_id,a.title a_title,a.content a_content, a.createtime a_createtime,
a.updatetime a_updatetime,a.uid a_uid,a.rcount a_rcount,a.state a_state
from userinfo u left join articleinfo a on u.id=a.uid where u.id=#{uid}
</select>
单元测试代码:
@Test
void getUserAndArticleByUid() {
UserInfo userInfo= userMapper.getUserAndArticleByUid(1);
log.info("用户详情:"+userInfo);
}
结果图如下:

动态 sql 是Mybatis的强⼤特性之⼀,能够完成不同条件下不同的 sql 拼接。
可以参考官⽅⽂档:Mybatis动态sql
在注册⽤户的时候,可能会有这样⼀个问题,注册分为两种字段:必填字段和非必填字段,那如果在添加⽤户的时候有不确定的字段传⼊,程序应该如何实现呢❓🤔 这个时候就需要使⽤动态标签 来判断了:
判断一个参数是否有值的,如果没有值,那么就会隐藏if中的sql
语法如下:

mapper层:
// 添加用户,添加用户时photo是非必传参数
public int add2(UserInfo userInfo);
XML层(实现上面的接口):
<!-- 添加用户,添加用户时photo是非必传参数-->
<insert id="add2">
insert into userinfo(username,password
<if test="photo!=null">
,photo
</if>
) values(#{username},#{password}
<if test="photo!=null">
,#{photo}
</if>
)
</insert>
测试代码(传photo值时):
@Test
void add2() {
UserInfo userInfo=new UserInfo();
userInfo.setUsername("潘潘");
userInfo.setPassword("123");
userInfo.setPhoto("jiao.png");
int result= userMapper.add2(userInfo);
log.info("添加用户的结果:"+result);
}

测试代码(不传photo值时):
@Test
void add2() {
UserInfo userInfo=new UserInfo();
userInfo.setUsername("一一");
userInfo.setPassword("123456");
//userInfo.setPhoto("jiao.png");
int result= userMapper.add2(userInfo);
log.info("添加用户的结果:"+result);
}

数据库中相对应的数据:

最主要的作用:去除SQL语句前后多余的某个字符的
之前的插入⽤户功能,只是有⼀个 photo字段可能是选填项,如果有多个字段,⼀般考虑使⽤标签结合标签,对多个字段都采取动态⽣成的方式。
标签中有如下属性:
prefix:表示整个语句块,以prefix的值作为前缀suffix:表示整个语句块,以suffix的值作为后缀prefixOverrides:表示整个语句块要去除掉的前缀suffixOverrides:表示整个语句块要去除掉的后缀语法如下:

由于上面为了演示字段名称和程序中的属性名不同的情况,可使⽤ resultMap 配置映射的情况,把UserInfo类中的username改成了name,为了下面编写方便,现又重新更改为username
mapper层:
// 添加用户,其中username,password.photo 都是非必传参数,但至少会传递一个参数
public int add3(UserInfo userInfo);
XML层(实现上面的接口):
<insert id="add3">
insert into userinfo
<trim prefix="(" suffix=")" suffixOverrides=",">
<if test="username!=null">
username,
</if>
<if test="password!=null">
password,
</if>
<if test="photo!=null">
photo
</if>
</trim>
values
<trim prefix="(" suffix=")" suffixOverrides=",">
<if test="username!=null">
#{username},
</if>
<if test="password!=null">
#{password},
</if>
<if test="photo!=null">
#{photo}
</if>
</trim>
</insert>
测试代码:
@Test
void add3() {
UserInfo userInfo=new UserInfo();
userInfo.setUsername("穗穗");
userInfo.setPassword("145");
userInfo.setPhoto("apan.png");
int result= userMapper.add3(userInfo);
log.info("添加用户的结果:"+result);
}


主要作用:就是实现查询中的 where sql 替换的,它可以实现如果没有任何的查询条件,那么它可以隐藏查询中的 where sql,但是如果存在查询条件,那么会生成 where 的 sql 查询,并且使用 where 标签可以自动的去除最前面一个 and 字符
首先由于设置的只能返回一个,所以数据库中只保留了一条数据:

mapper层:
// 根据用户id查询用户
public UserInfo getUserById(@Param("id") Integer id);
XML层(实现上面的接口):
<select id="getUserById" resultMap="BaseMap">
select * from userinfo
<where>
<if test="id!=null">
id=#{id}
</if>
</where>
</select>
测试代码:
@Test
void getUserById() {
UserInfo userInfo=userMapper.getUserById(null);
log.info("用户信息"+userInfo);
}

当我们的条件参数有多个时,要用and来连接,那么加上and会是怎样呢❓ 🤔

测试代码:


以上标签也可以使⽤ 替换。
作用: 在进行修改操作时,配合 if 来处理非必传输的,它的特点就是会自动去除最后一个英文逗号
语法如下:

mapper层:
public int update2(UserInfo userInfo);
XML层(实现上面的接口):
<update id="update2">
update userinfo
<set>
<if test="username!=null">
username=#{username},
</if>
<if test="password!=null">
password=#{password},
</if>
<if test="photo!=null">
photo=#{photo}
</if>
</set>
where id=#{id}
</update>
测试代码:
@Test
void update2() {
UserInfo userInfo=new UserInfo();
userInfo.setId(1);
userInfo.setUsername("潘潘");
int result= userMapper.update2(userInfo);
log.info("update2 修改的结果为:"+result);
}


当最后没有英文逗号时:



以上标签也可以使⽤ 替换。
作用: 主要就是对集合进行循环的
标签有如下属性:
collection:绑定⽅法参数中的集合,如 List,Set,Map或数组对象item:遍历时的每⼀个对象open:语句块开头的字符串close:语句块结束的字符串separator:每次遍历之间间隔的字符串示例:根据多个⽂章 id 来删除⽂章数据
mapper层:
int delIds(List<Integer> list);
XML层(实现上面的接口):
<delete id="delIds">
delete from userinfo where id in
<foreach collection="list" open="(" close=")" item="id" separator=",">
#{id}
</foreach>
</delete>
为方便观察,我们可以现在数据库中加几条数据:

测试代码:
@Test
void delIds() {
List<Integer>list=new ArrayList<>();
list.add(11);
list.add(12);
list.add(12);
int result= userMapper.delIds(list);
log.info("批量删除的结果:"+result);
}


