目录
1.什么是MyBatis
2.第一个MyBatis查询
2.1 准备工作
2.2 实际操作
2.2.1 定义接口
2.2.2 创建XML实现上述接口
2.3 单元测试
2.3.1 单元测试的优势
2.3.2 创建并使用单元测试
2.3.3 有关断言
3.增删改的基本操作
3.1 插入操作
特殊的添加:返回自增 id
3.2 修改操作
3.3 删除操作
4.查询操作
4.1 单表查询
4.1.1 参数占位符 #{} 和 ${}
4.1.2 ${} 优点
4.1.3 SQL注入问题
4.1.4 like查询
4.2 多表查询
4.2.1 resultType
4.2.2 resultMap
4.2.3 多表查询一对一关系
4.2.4 多表查询一对多关系
1.什么是MyBatis
MyBatis 是⼀款优秀的持久层框架,它⽀持⾃定义 SQL、存储过程以及⾼级映射。MyBatis 去除了⼏ 乎所有的 JDBC 代码以及设置参数和获取结果集的⼯作。MyBatis 可以通过简单的 XML 或注解来配置 和映射原始类型、接⼝和 Java POJO(Plain Old Java Objects,普通⽼式 Java 对象)为数据库中的 记录。
简单来说 MyBatis 是更简单完成程序和数据库交互的⼯具,也就是更简单的操作和读取数据库⼯具。
其实MyBatis本质上还是会生成SQL语句并去执行,但是它帮助我们省去了jdbc繁琐复杂的操作,而MyBatis帮助我们简化了这些操作,这也是我们学习MyBatis的一个重要原因
2.第一个MyBatis查询
2.1 准备工作
在使用MyBatis之前,我们首先需要引入MyBatis的相关依赖。
之后我们需要在配置文件配置数据库的连接信息
spring:
datasource:
url: jdbc:mysql://127.0.0.1:3306/mycnblog?characterEncoding=utf8
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
# 开启 MyBatis sql 打印
logging:
level:
com:
example:
mybatisdemo: debug
# 查看执行过程 这段语句可以帮助我们看到具体的sql语句
mybatis:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
配置MyBatis的XML保存路径
# 配置 mybatis xml 保存路径
mybatis:
mapper-locations: classpath:mybatis/**Mapper.xml
2.2 实际操作
首先我们来了解一下MyBatis的模式图
其实MyBatis的模式只包含两个部分:
1.接口(定义方法的声明)
2.xml实现接口中的方法
这两个部分共同完成生成sql并将执行结果映射到程序的对象中
2.2.1 定义接口
首先在我们定义的接口类中一定要写上@Mapper注解,否则MyBatis无法识别。其次对于传入的参数最好使用@Param注解去重命名,否则在某些系统上可能会出现一些奇怪的错误。
2.2.2 创建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.mybatisdemo.mapper.UserMapper">
<!-- 根据 id 查询用户 -->
<select id="getUserById" resultType="com.example.mybatisdemo.model.UserInfo">
select * from userinfo where id=${id}
</select>
</mapper>
除了select标签的语句其他都是固定写法,大家可以保存在代码片段中方便随时使用。
注意我们的namespace要设置是实现接口的具体包名加类名,并且select语句是需要设置返回类型resultType或者resultMap的 ,而其他语句不需要,这个我们在下面会详细讲。
2.3 单元测试
我们写好了相关的代码后该如何去测试它的功能呢?最传统的方法就是我们去模拟生产环境来对其进行测试,但是对于一个spring boot项目来说,它需要至少实现controller,service和repository三个层面的代码,这对于我们测试来说未免有一些麻烦,所以在实际项目中我们通常会使用单元测试来测试相关功能。
2.3.1 单元测试的优势
1、可以⾮常简单、直观、快速的测试某⼀个功能是否正确。
2、使⽤单元测试可以帮我们在打包的时候,发现⼀些问题,因为在打包之前,所以的单元测试必须通 过,否则不能打包成功。
3、使⽤单元测试,在测试功能的时候,可以不污染连接的数据库,也就是可以不对数据库进行任何改 变的情况下,测试功能。
2.3.2 创建并使用单元测试
由于我们的spring boot是自带测试框架的,所以我们不需要去额外配置,可以直接使用。
1.生成单元测试类
我们要先在我们需要测试的类中生成单元测试类,直接在该类中右键->生成->测试即可
2.配置单元测试类
这里我们只关心框内的选项即可,我们需要测试哪一个方法在前面✔即可
3.运行单元测试类
这里我们要注意@SpringBootTest这个注解需要我们手动添加,代表当前测试环境是SpringBoot,如果不添加则会报错
package com.example.mybatisdemo.mapper; import com.example.mybatisdemo.model.UserInfo; import lombok.extern.slf4j.Slf4j; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.transaction.annotation.Transactional; import java.util.List; import static org.junit.jupiter.api.Assertions.*; @Slf4j //表示当前单元测试运行在springboot环境下 @SpringBootTest class UserMapperTest { @Autowired private UserMapper userMapper; @Test //添加此注解使得方法执行完后会自动回滚事务 @Transactional void add() { UserInfo userInfo=new UserInfo(); userInfo.setUsername("王五"); userInfo.setPassword("123"); userInfo.setPhoto("default.png"); int result = userMapper.add(userInfo); System.out.println("添加的结果:" + result); //断言操作 Assertions.assertEquals(1, result); } }
最后我们点击左边的绿色小三角就可以运行测试并得到结果了
2.3.3 有关断言
断⾔:如果断⾔失败,则后⾯的代码都不会执行,并且会提示出错
3.增删改的基本操作
简单的查询我们已经在上面讲过了,这里不再赘述,有关多表查询等比较复杂的部分我们将在下面讲解。此处只讲解简单的增删改操作。
对应使⽤ MyBatis 的标签如下:
<insert>标签:插入语句
<update>标签:修改语句
<delete>标签:删除语句
以下为测试数据:
-- 创建数据库
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', '', '2022-11-16 17:10:48', '2022-11-16 17:10:48', 1)
;
-- ⽂章添加测试数据
insert into articleinfo(title,content,uid)
values('Java','Java正⽂',1);
UserInfo类:
package com.example.mybatisdemo.model;
import lombok.Data;
import java.util.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;
}
ArticleInfo类:
package com.example.mybatisdemo.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;
}
3.1 插入操作
插入用户操作:
<insert id="add">
insert into userinfo(username,password,photo,state)
values(#{username},#{password},#{photo},1)
</insert>
@Mapper
public interface UserMapper {
//添加用户
public int add(UserInfo userInfo);
}
特殊的添加:返回自增 id
<!-- 获取自增id -->
<insert id="addGetId" useGeneratedKeys="true" keyProperty="id" keyColumn="id">
insert into userinfo(username,password,photo)
values(#{username},#{password},#{photo})
</insert>
useGeneratedKeys:这会令 MyBatis 使⽤ JDBC 的 getGeneratedKeys ⽅法来取出由数据 库内部⽣成的主键(比如:像 MySQL 和 SQL Server 这样的关系型数据库管理系统的⾃动 递增字段),默认值:false。
keyColumn:设置生成键值在表中的列名,在某些数据库(像 PostgreSQL)中,当主键列 不是表中的第⼀列的时候,是必须设置的。如果⽣成列不⽌⼀个,可以⽤逗号分隔多个属性 名称。
keyProperty:指定能够唯⼀识别对象的属性,MyBatis 会使⽤ getGeneratedKeys 的返回 值或 insert 语句的 selectKey ⼦元素设置它的值,默认值:未设置(unset)。如果⽣成列 不⽌⼀个,可以⽤逗号分隔多个属性名称。(比如在这里我们就可以通过userinfo.getId()来获取到自增的id)
3.2 修改操作
<update id="update">
update userinfo set username=#{name} where id=#{id}
</update>
3.3 删除操作
<!-- 根据用户 id 删除用户 -->
<delete id="del">
delete from userinfo where id=#{id}
</delete>
4.查询操作
4.1 单表查询
下⾯我们来实现⼀下根据⽤户 id 查询⽤户信息的功能
<!-- 根据 id 查询用户 -->
<select id="getUserById" resultType="com.example.mybatisdemo.model.UserInfo">
select * from userinfo where id=${id}
</select>
4.1.1 参数占位符 #{} 和 ${}
#{}:预编译处理
${}:字符直接替换
预编译处理是指:MyBatis 在处理#{}时,会将 SQL 中的 #{} 替换为?号,使⽤ PreparedStatement 的 set ⽅法来赋值。直接替换:是MyBatis 在处理 ${} 时,就是把 ${} 替换成变量的值。
这里我们可以观察一下具体的sql语句:
当我们上述代码使用#{}时,得到的sql如下
可以看到id确实被?替代了,而当我们使用${}则会是以下的结果
id的数据直接被写在了sql中.虽然这种方式简单粗暴,但是他也可能会带来越权访问及sql注入的问题,我们下面会讲到。
4.1.2 ${} 优点
虽然${}是简单粗暴的替换可能会带来诸多意外,但是存在即合理。
我们看到下面的代码:
<select id="getAllBySort" parameterType="java.lang.String" resultType="com.
example.demo.model.User">
select * from userinfo order by id ${sort}
</select>
使用 ${sort} 可以实现排序查询,⽽使⽤ #{sort} 就不能实现排序查询了,因为当使用 #{sort} 查询时, 如果传递的值为 String 则会加单引号,就会导致 sql 错误。因为此时传递的值是desc或者asc,控制升序或者降序。但是#{}会自动将它识别成String类型自动加上单引号导致sql语句出错。
4.1.3 SQL注入问题
我们看到下面的查询语句
<select id="isLogin" resultType="com.example.demo.model.User">
select * from userinfo where username='${name}' and password='${pwd}'
</select>
而假如我们用“' or 1='1”来去替代pwd中的参数,此时pwd恒为真值,所以无论密码正确与否我们都能通过这种方式来获取到用户的信息,这就是所谓的SQL注入。
所以这就提醒我们如果迫不得已需要使用${},那么我们一定要在前端以及controller做好参数合法性校验,避免发生SQL注入的问题。
4.1.4 like查询
对于like我们如果直接使用#{}去传参数是会报错的
<select id="findUserByName2" resultType="com.example.demo.model.User">
select * from userinfo where username like '%#{username}%';
</select>
此时的SQL语句相当于:
select * from userinfo where username like '%'username'%';
注意%附近的两个单引号就是报错的根源,还是因为#{}将其识别成String类型自动加上了单引号导致的。但是在这里我们可以通过使用mysql 的内置函数 concat() 来处理,实现代码如下:
<select id="findUserByName3" resultType="com.example.demo.model.User">
select * from userinfo where username like concat('%',#{username},'%');
</select>
4.2 多表查询
如果是增、删、改返回搜影响的⾏数,那么在 mapper.xml 中是可以不设置返回的类型的,然而即使是最简单查询⽤户的名称也要设置返回的类型,否则会出现错误。
对于 查询标签来说⾄少需要两个属性:
id 属性:⽤于标识实现接⼝中的那个⽅法;
结果映射属性:结果映射有两种实现标签:<resultType>和<resultMap>。
4.2.1 resultType
绝⼤数查询场景可以使⽤ resultType 进⾏返回,如下代码所示:
<select id="getNameById" resultType="java.lang.String">
select username from userinfo where id=#{id}
</select>
它的优点是使⽤⽅便,直接定义到某个实体类即可
4.2.2 resultMap
resultMap 使⽤场景:
字段名称和程序中的属性名不同的情况,可使⽤ resultMap 配置映射;
⼀对⼀和⼀对多关系可以使⽤ resultMap 映射并查询数据。
字段名和属性名不同的情况 :
有时我们数据库中的列名和类中的属性名并对应不上(比如列名叫name属性名叫username),此时如果我们的select语句仍然使用resultType作为结果映射属性会导致查询的值为null,此时我们就需要用到resultMap了。
<!-- 当数据库列名和类的属性名对不上时需要使用resultMap,否则使用resultType即可 -->
<resultMap id="BaseMap" type="com.example.demo.model.UserInfo">
<!-- 主键映射 -->
<!-- column数据库代表列名,property代表对应类属性名 -->
<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>
<result column="state" property="state"></result>
</resultMap>
<select id="getUserById" resultMap="com.example.demo.mapper.UserMapper.BaseMap">
select * from userinfo where id=#{id}
</select>
其中 id="BaseMap"是我们的标识,type代表要映射的实体类
4.2.3 多表查询一对一关系
在多表查询时,如果使⽤ resultType 标签,在⼀个类中包含了另⼀个对象是查询不出来被包含的对象 的,比如以下实体类:
假如我们想要查询一篇文章所对应的作者,但是文章来自于文章表,而作者来自于用户表,此时就涉及到了我们的多表查询了。而又因为文章对应的作者是唯一的,所以是一对一的关系映射。但是看到这里你可能会说那之前在SQL中写多表查询不久好了吗,为什么还要特地拿出来说呢?因为我们如果只是简单地去使用多表查询语句是不行的,而resultMap只映射了一个类的属性,而多表查询设计了多个类的不同属性,所以没被映射到的那个类的属性就会为null。
此时我们就需要使⽤特殊的⼿段来实现联表查询了。
⼀对⼀映射要使用<association>标签,具体实现如下(⼀篇文章只对应⼀个作者):
<resultMap id="BaseMap" type="com.example.demo.model.ArticleInfo">
<id property="id" column="id"></id>
<result property="title" column="title"></result>
<result property="content" column="content"></result>
<result property="createtime" column="createtime"></result>
<result property="updatetime" column="updatetime"></result>
<result property="uid" column="uid"></result>
<result property="rcount" column="rcount"></result>
<result property="state" column="state"></result>
<association property="user"
resultMap="com.example.demo.mapper.UserMapper.BaseMap"
columnPrefix="u_">
</association>
</resultMap>
<select id="getAll" resultMap="BaseMap">
select a.*,u.username u_username from articleinfo a
left join userinfo u on a.uid=u.id
</select>
此处关于<association>标签的具体属性说明:
property 属性:指定 Article 中对应的属性,即⽤户。resultMap 属性:指定关联的结果集映射,将基于该映射配置来组织⽤户数据。
columnPrefix 属性:绑定⼀对⼀对象时,是通过columnPrefix+association.resultMap.column 来映射结果集字段。 association.resultMap.column是指 标签中 resultMap属性,对应的结果集映 射中,column字段。
注意:此处的columnPrefix 属性不能省略,如果省略当联表中如果有相同的字段,那么就会导致查询出错。 比如这里我们ArticleInfo类里有文章id,UserInfo类里也有一个用户id,他们都叫id,如果不加以区分那么这两个id就会被互相给覆盖导致查询结果错误。columnPrefix主要就是通过一个前缀来区分它们的不同。
4.2.4 多表查询一对多关系
比如我们要查询一个作者所写的文章,一个作者可以对应很多篇文章,很显然这是一个一对多的关系映射。实际上一对多关系的查询和一对一关系查询的区别仅仅就是将<association>标签换成了<collection>标签。具体代码如下:
<resultMap id="BaseMap" type="com.example.demo.model.User">
<id column="id" property="id" />
<result column="username" property="username"></result>
<result column="password" property="password"></result>
<result column="photo" property="photo"></result>
<collection property="alist" resultMap="com.example.demo.mapper.ArticleI
nfoMapper.BaseMap"
columnPrefix="a_">
</collection>
</resultMap>
<select id="getUserById" resultMap="BaseMap">
select u.*,a.title a_title from userinfo u
left join articleinfo a on u.id=a.uid where u.id=#{id}
</select>
5.动态SQL
动态 sql 是Mybatis的强⼤特性之⼀,能够完成不同条件下不同的 sql 拼接。
这里放上官方的参考文档mybatis – MyBatis 3 | 动态 SQL
5.1 为什么需要动态SQL
想象一下一个网址在注册的时候总是有一些用户信息是必填的,而有些是非必填的。最后这些信息都将被存储在数据库中,那如果在添加⽤户的时候有不确定的字段传⼊,程序应该如何实现呢?如果我们仅仅是在前端或者后端进行参数判断不仅麻烦而且也不好处理sql语句,此时就需要用到我们的动态sql了。
5.2<if>标签
比如针对我们上面说的那种情况,我们就可以使用<if>标签去解决。比如添加的时候头像photo 为非必填字段,具体实现如下:
<insert id="add">
insert into userinfo(username,password
<if test="photo!=null">
,photo
</if>
) values(#{username},#{password}
<if test="photo!=null">
,#{photo}
</if>
)
</insert>
注意这里传入的是对象的属性而不是数据库中的字段。
5.3<trim>标签
之前的插⼊⽤户功能,只是有⼀个 sex 字段可能是选填项,如果所有字段都是非必填项,就考虑使用<trim>标签结合<if>标签,对多个字段都采取动态⽣成的方式。
为什么不直接使用多个<if>标签呢?
假如我们都使用<if>标签的话就可能会出现多余的逗号之类导致sql语句错误。
<insert id="add">
insert into userinfo
<trim prefix="(" suffix=")" suffixOverrides=",">
<if test="name!=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">
#{name},
</if>
<if test="password!=null">
#{password},
</if>
<if test="photo!=null">
#{photo}
</if>
</trim>
</insert>
比如上面的语句如果我们只使用<if>标签去判断,而此时我们只输入了username和password两个属性,那么password后面多余的一个逗号就会导致sql报错。而配合<trim>标签使用就可以帮助我们解决这个问题。
<trim>标签中有如下属性:
prefix:表示整个语句块,以prefix的值作为前缀
suffix:表示整个语句块,以suffix的值作为后缀
prefixOverrides:表示整个语句块要去除掉的前缀
suffixOverrides:表示整个语句块要去除掉的后缀
而对于我们上述sql来说,会将第⼀个 部分做如下处理:
基于 prefix 配置,开始部分加上 (
基于 suffix 配置,结束部分加上 )
多个<if>组织的语句都以 , 结尾,在最后拼接好的字符串还会以 , 结尾,会基于 suffixO verrides 配置去掉最后⼀个 , 。
5.4 <where>标签
<where>标签就是用在条件查询语句中的,如果用户属性不为null,都为查询条件。
select id="getUserById" resultMap="BaseMap">
select * from userinfo
<where>
<if test="id!=null">
id=#{id}
</if>
<if test="name!=null">
and username=#{name}
</if>
</where>
</select>
使用<where>标签的好处就是它会自动帮我们处理多余的and前缀。
以上<where>标签也可以使⽤ <trim prefix="where" prefixOverrides="and">替换。
5.5 <set>标签
根据传⼊的⽤户对象属性来更新用户数据,可以使⽤标签来指定动态内容,配合update使用。
<update id="update">
update userinfo
<set>
<if test="name!=null">
username=#{name},
</if>
<if test="password!=null">
password=#{password},
</if>
<if test="photo!=null">
photo=#{photo}
</if>
</set>
where id=#{id}
</update>
它和<trim>标签类似,同样可以处理多余的逗号后缀,所以此处我们也可以使用<trim>标签。
5.6 <foreach>标签
提到foreach我们主要会想到Java通过foreach来遍历集合,事实上在MyBatis中同样如此。
<foreach>标签的常见属性
collection:绑定⽅法参数中的集合,如 List,Set,Map或数组对象
item:遍历时的每⼀个对象
open:语句块开头的字符串
close:语句块结束的字符串
separator:每次遍历之间间隔的字符串
比如根据多个用户 id来删除用户数据。
<delete id="delIds">
delete from userinfo where id in
<foreach collection="ids" open="(" close=")" item="id" separator=",">
#{id}
</foreach>
</delete>
此处的ids我们可以理解成集合的名字,而id类似Java中我们使用foreach遍历集合的变量名。