源码位置:MyBatis_demo
上篇文章我们学习了MyBatis的定义以及增删查改操作,并且学习了如何在xml文件中编写SQL时使用#{}
的方式将参数和对象的属性映射到SQL语句中,上篇的内容已经足以应对大部分场景,本篇文章我们就要学习一下MyBatis的进阶操作,拿捏MyBatis实战。
1. 参数占位符 —— #{} 和 ${}
MyBatis中有两种参数占位符,分别是"#{}"
和"${}"
,上节课我们学习了"#{}"
的使用,以上节课根据id查询用户的业务为例:
接口以及xml文件中的SQL实现如下:
public UserInfo getUserById(@Param("uid") Integer id);
把xml文件中的"#{}"
改为"${}"
:
<select id="getUserById" resultType="com.chenshu.mybatis_demo.model.UserInfo">
select * from userinfo where id = ${uid}
</select>
调用测试方法后成功查到数据:
我们发现将"#{}"
改为"${}"
后同样能够查出数据,那不是玩我呢吗?
不要着急,这时我们再编写一个通过username
查询的业务,接口以及xml文件中的SQL实现如下:
public List<UserInfo> getUserByUsername(@Param("username") String username);
<select id="getUserByUsername" resultType="com.chenshu.mybatis_demo.model.UserInfo">
select * from userinfo where username = #{username}
</select>
单元测试:
@Test
void getUserByUsername() {
List<UserInfo> userInfoList = userMapper.getUserByUsername("zhangsan");
System.out.println(userInfoList);
}
在使用"#{}"
参数占位符的情况下成功取到数据:
改为"${}"
后居然报错了,报错信息:在where子句中有未知的列’zhangsan’:
为什么呢会这样呢?我明明是想要查询username
为zhangsan
的列呀,他为什么说没找到名字为"zhangsan"
的列呢?原因很简单,两种占位符的区别如下:
1.1 #{}:预编译处理
#{}
是预编译处理,等同于JDBC中的"?"
占位符,预编译的SQL语句不是有具体数值的语句,而是用(?)来代替具体数据,然后在执行的时候再调用setXXX()
方法把具体的数据传入。
<select id="getUserByUsername" resultType="com.chenshu.mybatis_demo.model.UserInfo">
select * from userinfo where username = #{username}
</select>
预编译的处理下,在数据库中执行的SQL语句为:select * from userinfo where username = "zhangsan";
也就是查询username
为zhangsan
的数据。
1.2 ${}:字符直接替换
${}
是字符直接替换,会直接将传入参数的值放入SQL语句中。
<select id="getUserByUsername" resultType="com.chenshu.mybatis_demo.model.UserInfo">
select * from userinfo where username = ${username}
</select>
在数据库中执行的SQL语句为:select * from userinfo where username = zhangsan;
,这时就变成了在表中查找出所有userinfo
和zhangsan
这两个字段值相同的数据;
了解后了两者的差异后,我们直接将xml中的SQL改为下面的形式:
<select id="getUserByUsername" resultType="com.chenshu.mybatis_demo.model.UserInfo">
select * from userinfo where username = '${username}'
</select>
也成功查询到了数据:
1.3 ${}的缺陷:SQL注入
以登录功能为例,正常情况下,用户在前端需要传一个账号和密码给后端,后端再把账号和密码放到where子句
中查询数据库,如果查到对应的数据,就证明用户名和密码输入正确,登录成功,SQL语句如下:
select * from userinfo where username = 'zhangsan' and password = '123';
输入正确就能查询到数据:
否则就查不到数据:
这时我们来使用${}
实现一下登录功能:
接口:
public UserInfo userLogin(@Param("username") String username, @Param("password") String password);
xml编写SQL:
<select id="userLogin" resultType="com.chenshu.mybatis_demo.model.UserInfo">
select * from userinfo where username = '${username}' and password = '${password}'
</select>
单元测试:
@Test
void userLogin() {
UserInfo userInfo = userMapper.userLogin("zhangsan", "123");
if (userInfo == null) {
System.out.println("用户名或密码错误,请重新登录!");
} else {
System.out.println("登录成功!");
System.out.println(userInfo);
}
}
在输入正确的账号和密码后,打印了下面信息:
输入错误的密码后,打印了下面信息:
1.3.1 SQL注入
当有别有用心之人在其不知道密码的情况下,恶意利用SQL语句的特性登录成功,就是SQL注入,下面给大家举个例子:
SQL注入代码:“’ or 1='1”
将传入的密码改为上面的SQL注入语句:
@Test
void userLogin() {
UserInfo userInfo = userMapper.userLogin("zhangsan", "' or 1='1");
if (userInfo == null) {
System.out.println("用户名或密码错误,请重新登录!");
} else {
System.out.println("登录成功!");
System.out.println(userInfo);
}
}
居然真的查询到相应字段了,该用户也就登录成功了:
为什么会出现这种情况呢?
原因是将密码写为' or 1='1
传入数据库查询时,语句就变成了下面这样:
此时不管你输入的密码是什么,都会查询出表中的所有数据,这就是SQL注入。
如果是预编译的话,数据库就只会把' or 1='1
当作一整个字符串去处理,而不会把它当作SQL脚本去处理,也就不会出现SQL注入的问题,我们现在将xml文件中的SQL改写为#{}
的形式:
<select id="userLogin" resultType="com.chenshu.mybatis_demo.model.UserInfo">
select * from userinfo where username = '${username}' and password = #{password}
</select>
查不到任何数据,登录失败!
1.4 ${}的使用场景
1.4.1 排序查询
既然预处理这么好用,为什么会存在两种占位符呢?存在即合理,有些特殊情况就是需要使用"${}"
来处理的。
当后端需要传一些关键字,比如专栏中给用户提供了选择通过数据库的某一列进行排序文章的功能,实现这个功能就需要使用${}
来完成
这时我们在数据库中就需要使用order by xxx
作为排序的依据,如果使用"#{}"
的方式,由于后端传入了一个String
,就会预编译order by 'xxx'
,
因此此时需要使用${}
来实现排序查询,具体的接口与SQL实现如下:
public List<ArticleInfo> sortArticle(@Param("sort") String sort);
<select id="sortArticle" resultType="com.chenshu.mybatis_demo.model.ArticleInfo">
select * from articleinfo order by ${sort}
</select>
单元测试:根据createtime
字段排序
@Test
void sortArticle() {
List<ArticleInfo> articleList= articleMapper.sortArticle("createtime");
for (ArticleInfo articleInfo : articleList) {
System.out.println(articleInfo);
}
}
运行结果:成功根据createtime
字段的升序打印所有文章列表
1.4.2 like 模糊查询
a) 使用${}
like 使用 #{}
会报错,因为预编译后的SQL如下:
这时我们就可以使用${}
,接口以及实现如下:
public List<ArticleInfo> searchArticle(@Param("title") String title);
<select id="searchArticle" resultType="com.chenshu.mybatis_demo.model.ArticleInfo">
select * from articleinfo where title like '%${title}%'
</select>
单元测试:
@Test
void searchArticle() {
List<ArticleInfo> articleList = articleMapper.searchArticle("MyBatis");
for (ArticleInfo articleInfo : articleList) {
System.out.println(articleInfo);
}
}
成功查询到 title 中带 ‘MyBatis’ 的数据:
b) 使用concat()
concat()
是mysql内置函数,可以用作字符串的拼接:
修改xml中的SQL语句:
<select id="userLogin" resultType="com.chenshu.mybatis_demo.model.UserInfo">
select * from userinfo where username = '${username}' and password = #{password}
</select>
再次进行单元测试:
2. resultMap的使用
上一篇文章我们提到过resultType
的使用,作用是将数据库返回的记录和类映射起来,但是当类的属性名与数据库中的列名不同时,就会出现无法映射的问题,这里我来举个例子:
将UserInfo原来的属性名username
修改为name
:
@Data
public class UserInfo {
private int id;
private String username;
private String password;
private String photo;
private Timestamp createTime;
private Timestamp updateTime;
private int state;
}
此时再调用一下单元测试的getAll()
方法,发现name的值为null:
这里我们就需要使用resultMap
定义一个映射规则来解决字段名和属性名不同的情况:
<resultMap id="userInfoMap" type="com.chenshu.mybatis_demo.model.UserInfo">
<id column="id" property="id"/>
<result column="username" property="name"/>
</resultMap>
解释下上面的标签和属性分别代表什么:
resultMap标签
中的id
就是你给该映射规则自定义的名称;resultMap标签
中的type
就对应着你需要映射的类;id标签
是用来映射主键的,使用上推荐不管表中主键字段名和映射类的的属性是否相同,都配置这一项,否则多表查询的时候会出问题;result标签
是用来映射普通字段和映射类的属性的id
和result
标签中都有一个column
和property
属性,是用来配置映射的,分别对应表的字段名和类的属性名
将getAll()
方法对应的SQL中的resultType
改为resultMap
:
<select id="getAll" resultMap="userInfoMap">
select * from userinfo
</select>
再次测试,成功拿到了name(对应数据库中的username)的值:
3. 多表查询
- 连接表需要查询的字段添加到实体类
- 编写接口的
getAll()
方法以及xml文件中的SQL
public List<ArticleInfo> getAll();
<select id="getAll" resultType="com.chenshu.mybatis_demo.model.ArticleInfo">
select a.*, u.username from articleinfo as a
left join userinfo as u
on a.uid = u.id
</select>
- 单元测试并得到结果
@Test
void getAll() {
List<ArticleInfo> list = articleMapper.getAll();
for (ArticleInfo articleInfo : list) {
System.out.println(articleInfo);
}
}
4. 动态SQL
动态SQL是MyBatis的强大特性之一,能够完成不同条件下不同的SQL拼接。
为什么要使用动态SQL呢?给大家引入一个案例:
在注册用户的时候,可能分为必填字段和非必填字段,如果添加用户的时候有不确定的字段传入,程序应该如何实现呢?
这时候就有小伙伴提到:大不了多写几个方法让他们重载呗。
这种方式十分不优雅:如果是一个选填字段的话,就需要去写两个方法,两个选填字段的话就要写四个方法,如果有n个选填字段,那么就需要写2^n个方法…
因此我们就需要要使用MyBatis的重要特性 —— 动态SQL,接下来我们就来讲解一下常用的动态SQL标签的使用。
在讲解动态SQL标签之前可以在配置文件中添加一下log-impl
用于打印SQL语句,以便对比:
4.1 <if>
标签
前面提到的案例就要使用<if>
动态标签来判断了。
这里我们来具体使用下<if>
标签,比如用户在注册的时候,必填字段为username
和password
,选填字段为photo
:
表结构如下:photo字段默认值为''
;
4.1.1 不使用<if>
标签
public int add(UserInfo userInfo);
<insert id="add">
insert into userinfo(username, photo, password)
values (
#{name},
#{photo},
#{password})
</insert>
单元测试代码:
@Test
void add() {
UserInfo userInfo = new UserInfo();
userInfo.setName("zhangsan");
userInfo.setPassword("123");
userMapper.add(userInfo);
}
日志信息如下:我们发现SQL语句被预编译成了Preparing: insert into userinfo(username, photo, password) values ( ?, ?, ?)
,并且photo
字段传入了一个null
==> Preparing: insert into userinfo(username, photo, password) values ( ?, ?, ?)
==> Parameters: zhangsan(String), null, 123(String)
<== Updates: 1
表中插入了如下字段:
4.1.2 使用<if>
标签
需要在test属性
内写判断的语句,如果"photo != null"
,那么就拼接if标签里面的内容,否则就不拼接:
<insert id="add">
insert into userinfo(
username,
<if test="photo != null">
photo,
</if>
password
)
values (
#{name},
<if test="photo != null">
#{photo},
</if>
#{password}
)
</insert>
插入一条"lisi"
的数据(不带photo值):
@Test
void add() {
UserInfo userInfo = new UserInfo();
userInfo.setName("lisi");
userInfo.setPassword("123");
userMapper.add(userInfo);
}
日志信息如下:由于没有传入photo
参数,因此SQL语句此时被编译成了insert into userinfo( username, password ) values ( ?, ? )
,并且只传入了username
和password
两个参数
==> Preparing: insert into userinfo( username, password ) values ( ?, ? )
==> Parameters: lisi(String), 123(String)
<== Updates: 1
对比表中的两条数据,我们发现在不使用if标签
的时候photo
插入的值为null
,使用if标签
的时候photo
的值为默认值''
使用if标签
的意义: 由于NULL
和''
在MySQL中是不同的,因此你要使用一个查询语句select * from userinfo where photo = '';
,就无法查询到zhangsan
这条记录:
甚至当photo
标签的字段上加了nou null
约束的话,在不使用if标签
的情况下会直接报错
4.2 <trim>
标签
前面我们在使用if标签
的时候选择了使用一种取巧的方式,把使用if标签
的地方放在参数的中间位置,如果位于最后面的话,会出现问题,像下面这样:
<insert id="add">
insert into userinfo(
username,
password,
<if test="photo != null">
photo
</if>
)
values (
#{name},
#{password},
<if test="photo != null">
#{photo}
</if>
)
</insert>
如果photo=null
的话,不拼接photo
的内容,此时的SQL语句就变成了insert into userinfo( username, password, ) values ( ?, ?, )
,由于最后一个参数不能以逗号结尾,因此SQL会报语法错误:
这时候我们就需要去使用一个trim标签
去操作,
<trim>
标签中有如下属性:
- prefix:表示整个语句块,以prefix的值为
前缀
- suffix:表示整个语句块,以suffix的值作为
后缀
- prefixOverrides:表示整个语句块要
去除掉的前缀
- suffixOverrides:表示整个语句块要
去除掉的后缀
假设我们三个参数(username, password, photo)都是可选参数,如何巧妙地使用trim标签
来处理呢?
改造xml文件中的SQL:使用trim标签的prefix
、suffix
以及suffixOverrides
参数
<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>
这里我需要多提一嘴,如果trim标签
里的所有if语句都不生效,就不会添加前后缀,而是直接编译成:insert into userinfo values
,因此在使用时至少传入一个参数。
单元测试:
@Test
void add() {
UserInfo userInfo = new UserInfo();
userInfo.setName("wangwu");
userInfo.setPassword("123");
userMapper.add(userInfo);
}
日志信息如下:此时就算把可选字段作为最后一个参数,trim标签
会帮我们吧最后一个','
删掉,并且在开头和结尾分别添加上'('
和')'
==> Preparing: insert into userinfo ( username, password ) values ( ?, ? )
==> Parameters: wangwu(String), 123(String)
<== Updates: 1
4.3 <where>
标签
where标签
自然是为了where
子句而设计的,当where子句中只有一个条件并且不确定传不传入的时候,可以使用一个if标签
把整个where子句包裹起来。
但是当通过多个参数组合查询的时候就需要用到where标签
了,比如用户根据uid+title
来查询文章的操作时,又不确定传不传入的情况下:
select * from articleinfo where title = 'MyBatis入门' and uid = 1;
public List<ArticleInfo> selectByCondition(ArticleInfo articleInfo);
where标签
的写法十分简单,作用如下:
- 根据
where标签
的内容,决定要不要拼接where
- 去掉最首个参数的
and
关键字,让sql符合数据库的执行标准
也可以使用<trim prefix = "where" prefixOverrides = "and">
标签替换
<select id="selectByCondition" resultType="com.chenshu.mybatis_demo.model.ArticleInfo">
select * from articleinfo
<where>
<if test="title != null">
and title = #{title}
</if>
<if test="uid != null">
and uid = #{uid}
</if>
</where>
</select>
使用多种组合查询进行测试,首先要先把实体类中所有int类型的属性的类型改为Integer,否则在不传入参数的时候默认为0,而不是null
@Data
public class ArticleInfo {
private Integer id;
private String title;
private String content;
private Timestamp createtime;
private Timestamp updatetime;
private Integer uid;
private Integer rcount;
private Integer state;
private String username;
}
接下来我将传入不同的参数进行测试。
1. 什么参数也不传:
@Test
void selectByCondition() {
ArticleInfo articleInfo = new ArticleInfo();
List<ArticleInfo> list = articleMapper.selectByCondition(articleInfo);
for (ArticleInfo article : list) {
System.out.println(article);
}
}
打印SQL日志如下:
==> Preparing: select * from articleinfo
==> Parameters:
<== Columns: id, title, content, createtime, updatetime, uid, rcount, state
<== Row: 1, Spring Boot入门, <<BLOB>>, 2024-04-15 01:21:43, 2024-04-15 01:21:43, 1, 1, 1
<== Row: 2, MyBatis入门, <<BLOB>>, 2024-04-15 01:21:45, 2024-04-15 01:21:45, 1, 1, 1
<== Row: 3, MyBatis的插入操作, <<BLOB>>, 2024-04-16 01:07:49, 2024-04-16 01:07:49, 1, 1, 1
<== Row: 4, 修改标题, <<BLOB>>, 2024-04-16 01:29:37, 2024-04-16 01:29:37, 1, 1, 1
<== Row: 6, MyBatis添加并返回自增id, <<BLOB>>, 2024-04-16 01:53:53, 2024-04-16 01:53:53, 1, 1, 1
<== Total: 5
2. 只传uid:
@Test
void selectByCondition() {
ArticleInfo articleInfo = new ArticleInfo();
articleInfo.setUid(1);
List<ArticleInfo> list = articleMapper.selectByCondition(articleInfo);
for (ArticleInfo article : list) {
System.out.println(article);
}
}
打印SQL日志如下:
==> Preparing: select * from articleinfo WHERE uid = ?
==> Parameters: 1(Integer)
<== Columns: id, title, content, createtime, updatetime, uid, rcount, state
<== Row: 1, Spring Boot入门, <<BLOB>>, 2024-04-15 01:21:43, 2024-04-15 01:21:43, 1, 1, 1
<== Row: 2, MyBatis入门, <<BLOB>>, 2024-04-15 01:21:45, 2024-04-15 01:21:45, 1, 1, 1
<== Row: 3, MyBatis的插入操作, <<BLOB>>, 2024-04-16 01:07:49, 2024-04-16 01:07:49, 1, 1, 1
<== Row: 4, 修改标题, <<BLOB>>, 2024-04-16 01:29:37, 2024-04-16 01:29:37, 1, 1, 1
<== Row: 6, MyBatis添加并返回自增id, <<BLOB>>, 2024-04-16 01:53:53, 2024-04-16 01:53:53, 1, 1, 1
<== Total: 5
3. 传多个参数:
@Test
void selectByCondition() {
ArticleInfo articleInfo = new ArticleInfo();
articleInfo.setUid(1);
articleInfo.setTitle("MyBatis入门");
List<ArticleInfo> list = articleMapper.selectByCondition(articleInfo);
for (ArticleInfo article : list) {
System.out.println(article);
}
}
打印SQL日志如下:
==> Preparing: select * from articleinfo WHERE title = ? and uid = ?
==> Parameters: MyBatis入门(String), 1(Integer)
<== Columns: id, title, content, createtime, updatetime, uid, rcount, state
<== Row: 2, MyBatis入门, <<BLOB>>, 2024-04-15 01:21:45, 2024-04-15 01:21:45, 1, 1, 1
<== Total: 1
4.4 <set>
标签
其实set标签与update标签很像,作用是:
- 根据
set标签
里的内容决定要不要拼接"set"
- 去掉最后一个参数的
','
,让sql符合数据库的执行标准
可以使用<trim prefix="set" suffixOverrides=",">
替换。
具体使用:
public void updateUserInfo(UserInfo userInfo);
<update id="updateUserInfo">
update userinfo
<set>
<if test="name != null">
username=#{name},
</if>
<if test="password != null">
password=#{password},
</if>
<if test="photo != null">
photo=#{photo},
</if>
<if test="createtime != null">
createtime = #{createtime},
</if>
<if test="updatetime != null">
updatetime = #{updatetime},
</if>
</set>
where id = #{id}
</update>
单元测试代码如下,修改字段为1的用户的username
、password
以及photo
字段:
@Test
void updateUserInfo() {
UserInfo userInfo = new UserInfo();
userInfo.setId(1);
userInfo.setName("zhang");
userInfo.setPassword("12345");
userInfo.setPhoto("doge.png");
userMapper.updateUserInfo(userInfo);
}
修改前表的数据为以下记录:
运行代码后,日志打印信息如下:
==> Preparing: update userinfo SET username=?, password=?, photo=? where id = ?
==> Parameters: zhang(String), 12345(String), doge.png(String), 1(Integer)
<== Updates: 1
成功修改了下面三个字段:
4.5 <foreach>
标签
当后端传入参数为集合
(如List,Set,Map
或Array
)并需要进行遍历时可以使用该标签,foreach标签
有以下属性:
- collection:绑定方法中用
@Param
定义的别名 - item:用于给遍历时的每一个对象取一个名字
- open:语句块开头的字符串
- close:语句块结束的字符串
- separator:每次遍历之间间隔的字符串
示例:根据多个文章id来删除文章数据
原SQL语句:
delete from articleinfo where id in(3,4,5)
具体实现:
public int deleteByIds(@Param("ids") List<Integer> ids);
注意事项:foreach标签
中的collection
中的值对应接口方法中定义的别名
<delete id="deleteByIds">
delete from articleinfo
where id in
<foreach collection="ids" item="id" open="(" close=")" separator=",">
#{id}
</foreach>
</delete>
此时表数据如下,我想要删除id字段为7,8,9文章:
单元测试:
@Test
void deleteByIds() {
List<Integer> list = new ArrayList<>();
list.add(7);
list.add(8);
list.add(9);
int ret = articleMapper.deleteByIds(list);
System.out.println("删除了:" + ret);
}
运行后打印日志信息如下:
==> Preparing: delete from articleinfo where id in ( ? , ? , ? )
==> Parameters: 7(Integer), 8(Integer), 9(Integer)
<== Updates: 3
成功删除了三条记录: