MyBatis 是一个持久层框架,也就是对整个框架里面的数据进行持久化的,是一个 ORM 框架(对象关系映射)就是连接项目和数据库的,支持自定义 SQL,存储过程 以及 高级映射。几乎去除了所有的 JDBC 代码 和 设置参数和获取结果集的⼯作。更简单的就是,MyBatis 可以更简单的完成项目和数据库的交互。
支持自定义 SQL:就是 增删改查 都可以写,也就是说明 MyBatis 是很灵活的。
存储过程:
a)我们之前写的 SQL,不管是多复杂的 SQL,都是一行写完,因为没有复杂的业务场景。
b)像 for 循环,判断这些都没有。但是存储是 SQL 当中的方法,由一大堆 SQL 构成。
c)这个组成里面,是由循环,判断,分支,变量传递的。就叫做存储过程,也就是 SQL 方法化。
d)也就是说 SQL 方法化的过程所产生的东西,就叫做存储的过程。
高级映射:除了可以实现 数据表 和 程序里面的对象 映射之外,还可以实现 一对一的多表映射 和 一对多的映射。
a)比如说 CSDN,每一篇作者,都有它的作者,文章和作者的关系,就是一对一的关系。
b)对作者来说,可以发很多文章,作者和文章的关系就是一对多的关系。
我们主要通过 XML 来实现 SQL 语句,因为如果用 注解的话,会导致很难写。
使用 JDBC 的话,使用起来很麻烦,很繁琐。使用 MyBatis 的话,可以直接连接,使用方法很简单。我们创建一个数据库:
-- 创建数据库
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);
在创建项目的时候,就添加 MyBatis 相关依赖:
将这两个依赖全部导入就可以了,因为 MyBatis 是操作数据库的,所以也要导入 MySQL 依赖。
如果是之前就有的项目,那么在 pom.xml 里面通过 Edit Starters 导入就好了:
在启动项目之前,一定要配置数据库,不然会报错:
在配置文件当中配置数据库信息:
#配置数据库环境
spring:
datasource:
url: jdbc:mysql://127.0.0.1:3306/mycnblog?characterEncoding=utf8
username: root
password: sjp151
driver-class-name: com.mysql.cj.jdbc.Driver
然后启动项目就不会报错了。
因为 注解方式不好用,所以我们还是用 xml ,放在 resource 目录下,然后创建 mybatis 文件夹,然后放进去,也是为了提升可读性。我们创建一个 UserMapper :
然后在 配置文件 当中指定 mapper 路径:
#配置 mybatis xml 保存路径
mybatis:
mapper-locations: classpath:mybatis/**Mapper.xml
一个项目的流程如下图:
MyBatis 的操作模式,有两个部分:
在 Interface 当中加一个 @Mapper 注解,就表示当前的这个 mybatis 类,就 实现 了对象的映射。
根据用户 id 来查询用户信息的操作,我们之前的数据库操作,就已经在 userinfo 表中插入了一个数据:
然后在 userinfo 当中写一样的属性
@Data
public class UserInfo {
private int id;
private String name;
private String password;
private String photo;
private String createtime;
private String updatetime;
private int state;artList;
}
一定要保证和数据库当中表的内容一模一样,不然是不能操作数据库的。
然后在 Mapper 当中就可以写数据了,在 Mapper 当中的数据,一定要写 @Mapper 注解,起名的时候,和 Mapper 中的方法名称一样,便于可读性提升,创建 xml 的时候,配置文件必须要有下面这些配置文件,直接复制就行了:
#mapper的 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">
<mapper namespace="">
</mapper>
里面的 namespace
写的是包名的路径:
查询的时候,也是 select 标签,然后 id 表明的是哪个 mapper,resultType 是指返回类型,也是要有 包名和类型 。UserMapper 中代码如下:
@Mapper
public interface UserMapper {
public UserInfo getUserById(@Param("id") Integer id);
}
加 @param 注解是为了表示 xml 当中,用 id 可以获取到,就是通过 $ 符和 # 来获取到。
xml 如下:
<select id="getUserById" resultType="com.example.mybatisdemo.model.UserInfo">
select * from userinfo where id=${id}
select>
在 Service 当中获取服务:
@Service
public class UserService {
@Resource
private UserMapper userMapper;
public UserInfo getUserById(Integer id) {
return userMapper.getUserById(id);
}
}
然后在 Controller 当中进行检验:
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
@RequestMapping("/getuserbyid")
public UserInfo getUserById(Integer id) {
if (id == null) {
return null;
}
return userService.getUserById(id);
}
}
前端的程序,通过 Ajax 访问 服务器,发过来的信息,先到 Controller 层,对参数校验之后,然后把数据发给 Service 层,Service 对传输过来的信息,判断需要调用多少个接口,以及调用接口的先后顺序。
Mapper 是单一的,不会有太复杂的业务逻辑。如果 Mapper 中的业务过于复杂,就会导致业务耦合。也就是说 MyBatis 是基于 JDBC 实现的,所以在执行的时候,还是会生成 相应的 jdbc 代码 和 sql 语句,如下:
也就是添加依赖,不过在我们现在使用的 Spring Boot 版本当中,已经自动加了以下依赖了:
所以我们就不需要重新添加了。
只需要在需要单元测试的类当中,一键生成 test 就好了:
点击之后:
生成之后如下:
配置类的时候,要给类加上 @SpringBootTest 注解:
然后添加单元测试的业务代码,通过 @Resource 或者 @Autowired 注解来引入:
@SpringBootTest
class UserMapperTest {
@Autowired
private UserMapper userMapper;
@Test
void getUserById() {
UserInfo userInfo = userMapper.getUserById(1);
System.out.println(userInfo);
}
}
然后直接点击旁边的 运行 按钮就好了:
运行结果如下:
绿色的对勾表示单元测试成功,也就是通过了单元测试。
也可以通过断言来表示单元测试是否成功,常用的断言如下:
断言代码如下:
@SpringBootTest
class UserMapperTest {
@Autowired
private UserMapper userMapper;
@Test
void getUserById() {
UserInfo userInfo = userMapper.getUserById(1);
Assertions.assertNotNull(userInfo);
}
}
查询我们之前已经讲过了,还剩 增删改。
代码如下:
@Mapper
public interface UserMapper {
public int update(@Param("id") Integer id, @Param("username") String username);
}
xml 的 SQL 如下:
然后继续使用 单元测试 来测试就好了:
@SpringBootTest
class UserMapperTest {
@Autowired
private UserMapper userMapper;
@Test
void update() {
int result = userMapper.update(2,"wangwu");
Assertions.assertEquals(1,result);
}
}
就是在方法上面加一个 @Transactional 注解,就表示方法执行完之后,回滚数据库,数据库原始数据如下:
测试代码:
@SpringBootTest
class UserMapperTest {
@Autowired
private UserMapper userMapper;
@Test
void update() {
int result = userMapper.update(2,"zhaoliu");
Assertions.assertEquals(1,result);
}
}
单元测试是通过的,也就是受影响的行数时 1,结果:
数据库并没有被污染,如果不加的话,数据库就被修改(污染)了。
测试代码:
@SpringBootTest
class UserMapperTest {
@Autowired
private UserMapper userMapper;
@Test
void delete() {
int result = userMapper.delete(2);
System.out.println("受影响的行数:" + result);
}
}
先添加 “返回受影响的行数” 的添加方法:
xml 当中 SQL :
测试数据:
返回自增 id,也就是需要多设置 属性名 就好了:
<insert id="addGetId" useGeneratedKeys="true" keyProperty="id" keyColumn="id">
insert into userinfo(username,password,photo) values(#{username},#{password},#{photo});
insert>
先使用 # 来完成测试,针对 Integer 类型的参数:
<select id="getUserById" resultType="com.example.mybatisdemo.model.UserInfo">
select * from userinfo where id=#{id}
select>
测试生成的 SQL 如下:
用 # 就是先生成一个占位符,然后再去给 占位符 设置数据。占位符就是预处理(预查询)。可以很好的提升查询效率,也会更加安全。
根据用户名字来查询:
<select id="getUserByFullName" resultType="com.example.mybatisdemo.model.UserInfo">
select * from userinfo where username=#{username};
select>
现在用 ${} ,针对 Integer 类型的参数,测试生成的 SQL 如下:
SQL 就没有 ?了,而是直接被替换为参数。就是即时查询。
通过用户名字来查询:
<select id="getUserByFullName" resultType="com.example.mybatisdemo.model.UserInfo">
select * from userinfo where username=${username};
select>
换成 ${} 符号之后,来查询,结果报错:
发现 lisi 并没有被引号引起来。因为 ${} 是针对 String 类型的,因为没有 引号,所以导致 SQL 出错。
在我们登录的时候,要传两个参数,就是用户名和密码,只有两个都正确的时候,才能登陆成功。xml 如下:
测试代码:
运行结果如下,成功通过测试案例:
如果使用 SQL 注入的话,测试代码如下:
@Test
void login() {
String username = "lisi";
String password = "' or 1='1";
UserInfo userInfo = userMapper.login(username, password);
log.info("用户信息" + userInfo);
}
也就是在判断的时候,是通过 or 来判断的,后面的 1=1,等式恒为真,所以会成功运行:
密码是不正确的,但是却查询出来了信息,相当于没有取款密码,但是把钱取走了。换为 #{} 就不存在安全漏洞了:
模糊查询
通过 like 来进行模糊查询,方法如下:
public List<UserInfo> getListByName(@Param("username") String username);
测试代码如下:
@Test
void getListByName() {
String username = "a";
List<UserInfo> list = userMapper.getListByName(username);
log.info("用户列表:" + list);
}
成功通过测试:
但是 ${} 一定会有 sql 注入的问题,所以我们就可以通过 concat 来拼接,然后通过 #{} 来完成模糊查询:
<select id="getListByName" resultType="com.example.mybatisdemo.model.UserInfo">
select * from userinfo where username like concat('%',#{username},'%');
select>
如果字段名和数据库的字段名不一致的时候,就会导致查询出来的字段为 null,我们把 userinfo 的 username 换成 name,然后查询:
运行结果为 null:
所以就可以通过 resultMap 来解决这样的问题。
resultMap 有两个参数,有 id 和映射的内容 type :
<resultMap id="BaseMap" type="com.example.mybatisdemo.model.UserInfo">resultMap>
然后在参数挡住设置 id 和 result 就好,id 是主键映射,result 是普通属性映射。
但是这样写的话,在单表查询当中是没问题的,然后在 SQL 语句当中就可以使用 resultMap :
<resultMap id="BaseMap" type="com.example.mybatisdemo.model.UserInfo">
<id column="id" property="id">id>
<result column="username" property="name">result>
resultMap>
sql 如下:
<select id="getListByName" resultMap="BaseMap">
select * from userinfo where username like concat('%',#{username},'%');
select>
多表查询当中,mybatis 是支持 一对一 和一对多 查询的。针对文章信息,写一个实体类:
@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;
}
通过 UserInfo 来和 user 表进行关联。建一个接口:
@Mapper
public interface ArticleMapper {
public ArticleInfo getArticleById (@Param("id") Integer id);
}
XML 中的 SQL 代码如下:
<select id="getArticleById" resultType="com.example.mybatisdemo.model.ArticleInfo">
select a.*,u.id u_id,u.username u_username,u.password u_password from articleinfo a left join userinfo u on a.uid=u.id
where a.id=#{id}
select>
测试代码:
@SpringBootTest
@Slf4j
class ArticleMapperTest {
@Resource
private ArticleMapper articleMapper;
@Test
void getArticleById() {
ArticleInfo articleInfo = articleMapper.getArticleById(1);
log.info("文章详情" + articleInfo);
}
}
运行结果如下:
成功通过测试,但是 userinfo 的结果为 null:
这里的 resultType 没有查询出来,那么就通过 resultMap 来完成查询。
在使用的时候,也要把其它属性映射。但是属性里面还有 userinfo 所以通过 association 来连接,association 就是用来连接表的。因为两张表的信息都要查到,所以另外一张表的 xml 当中也应该写出所有配置。
ArticleMapper 的 xml :
<resultMap id="BaseMap" type="com.example.mybatisdemo.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.mybatisdemo.mapper.UserMapper1.BaseMap">
association>
resultMap>
UserMapper 的 XML :
<resultMap id="BaseMap" type="com.example.mybatisdemo.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>
resultMap>
这里 association 里面的 resultMap 也要保证 userInfo 里面有,因为是通过这个来连接的。
XML 中的 SQL 语句:
<select id="getArticleById" resultMap="BaseMap">
select a.*,u.* from articleinfo a left join userinfo u on a.uid=u.id
where a.id=#{id}
select>
查询结果如下,现在就能查到 userInfo 的信息了:
但是如果字段一样的话,就会导致值的覆盖,也就是不同表的值是一样的,为了解决这种问题,就可以通过设置前缀来解决,设置前缀就相当于是把表的字段重命名了。通过 columnPrefix 来设置前缀:
<association property="userInfo"
resultMap="com.example.mybatisdemo.mapper.UserMapper.BaseMap"
columnPrefix="u_">
association>
这样的话,就不会出现数据覆盖问题了。
查询一个作者的多篇文章,通过 List 来返回:
@Data
public class UserInfo {
private Integer id;
private String username;
private String password;
private String photo;
private String createtime;
private String updatetime;
private int state;
//作为文章列表
private List<ArticleInfo> artList;
}
Mapper 代码:
public UserInfo getUserAndArticleByUid(@Param("uid") Integer uid);
XML 当中,因为要用到 ArticleInfo 表,所以也要使用多表查询,通过 collection 来设置。通过 property 来约束数据类型,和 resultMap 来指定 BaseMap,然后通过 columnPrefix 来对字段重命名:
<resultMap id="BaseMap" type="com.example.mybatisdemo.model.UserInfo">
<id column="id" property="id">id>
<result column="username" property="username">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>
<collection property="artList"
resultMap="com.example.mybatisdemo.mapper.ArticleMapper.BaseMap"
columnPrefix="a_">
collection>
resultMap>
XML 当中的 SQL:
<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 from userinfo u left join
articleinfo a on u.id=a.uid where u.id=#{uid}
select>
动态 sql 是 MyBatis 的强⼤特性之⼀,能够完成不同条件下不同的 sql 拼接。也就是有的参数是非必须传的,所以就可以通过 动态 sql 来拼接。
判断一个参数是否是有值的,如果没值,那么就会隐藏 if 中的 SQL,也就是针对一些非必传参数,如果没有参数的话,就不执行 SQL,语法如下:
<if test="username!=null">
username=#{username}
if>
表达式的意思是:username 的参数是否为空。如果结果是 true,那么拼接的 sql 就会加上 username=#{username} 。比如说对于非必传参数,就可以直接用 if 来选中了:
<insert id="add2">
insert into userinfo(username,password
<if test="photo!=null">
,photo
</if>
) values (#{name},#{password}
<if test="photo!=null">
,#{photo}
</if>
)
</insert>
@Test
void add2() {
UserInfo userInfo = new UserInfo();
userInfo.setUsername("zhaoliu");
userInfo.setPassword("123");
int result = userMapper.add2(userInfo);
log.info("添加用户结果:" + result);
}
最主要的作用,去除 SQL 语句前后多余的某个字符。一共有四个属性:
语法如下:
<trim prefix="(" suffix=")" prefixOverrides="," suffixOverrides=",">
if< test="xxx">
...
if>
...
trim>
再使用一个 add 方法来添加,SQL 如下:
<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="name!=null">
#{username},
if>
<if test="password!=null">
#{password},
if>
<if test="photo!=null">
#{photo}
if>
trim>
insert>
然后进行测试,运行结果如下:
我们的 sql 语句当中,password 后面是有 逗号 的,但是这里把 逗号 省略了。
主要作用是实现查询中的 where sql 替换的,它可以实现如果没有任何的查询条件,那么它可以隐藏查询中的 where sql,但如果存在 查询条件,那么会生成 where 的 sql 查询,并且使用 where 标签可以自动去除前面的 and 字符。
通过 id 来查询,SQL 如下:
<select id="getUserById" resultMap="BaseMap">
select * from userinfo
<where>
<if test="id!=null">
id=#{id}
if>
where>
select>
测试结果如下:
刚刚好查询,如果不传 id 的话:
运行结果如下,就没有生成 SQL:
根据传⼊的⽤户对象属性来更新⽤户数据,可以使⽤标签来指定动态内容。UserMapper 接⼝中修改⽤户⽅法:根据传⼊的⽤户 id 属性,修改其他不为 null 的属性。set 标签,其实和 where 是一样的,都是替代 SQL 中的关键字。set 是 update 操作所需要使用的关键字。也是和 if 标签 配合使用的,也可以去掉最后一个符号。语法如下:
update table
<set>
<if test="xxx">
...
if>
...
set>
where ...
更新方法:
public int update(UserInfo userInfo);
SQL 如下:
<update id="update">
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 update() {
UserInfo userInfo = new UserInfo();
userInfo.setId(1);
userInfo.setUsername("bbb");
int result = userMapper.update(userInfo);
log.info("update 的修改结果:" + result);
}
更新之后数据库如下:
也说明 set 会自动去除最后一个逗号。set 标签也可以用 trim 来实现。
对集合进行遍历时可以使用该标签,foreach 标签有这些属性:
foreach 标签是 trim 所不能替代的。主要用在数据库当中批量删除数据。先在数据库当中插入数据,数据如下:
XML 当中的 SQL 如下:
<delete id="deleteId">
delete from userInfo where id in
<foreach collection="ids" open="(" close=")" item="id" separator=",">
#{id}
foreach>
delete>
测试代码如下:
@Test
void deleteId() {
List<Integer> list = new ArrayList<>();
list.add(6);
list.add(7);
list.add(8);
int result = userMapper.deleteId(list);
log.info("批量删除的结果:" + result);
}