源码位置:MyBatis_demo
上篇文章我们学习了MyBatis的定义以及增删查改操作,并且学习了如何在xml文件中编写SQL时使用#{}
的方式将参数和对象的属性映射到SQL语句中,上篇的内容已经足以应对大部分场景,本篇文章我们就要学习一下MyBatis的进阶操作,拿捏MyBatis实战。
1. 参数占位符 ------ #{} 和 ${}
MyBatis中有两种参数占位符,分别是"#{}"
和"${}"
,上节课我们学习了"#{}"
的使用,以上节课根据id查询用户的业务为例:
接口以及xml文件中的SQL实现如下:
java
public UserInfo getUserById(@Param("uid") Integer id);
把xml文件中的"#{}"
改为"${}"
:
xml
<select id="getUserById" resultType="com.chenshu.mybatis_demo.model.UserInfo">
select * from userinfo where id = ${uid}
</select>
调用测试方法后成功查到数据:
我们发现将"#{}"
改为"${}"
后同样能够查出数据,那不是玩我呢吗?
不要着急,这时我们再编写一个通过username
查询的业务,接口以及xml文件中的SQL实现如下:
java
public List<UserInfo> getUserByUsername(@Param("username") String username);
xml
<select id="getUserByUsername" resultType="com.chenshu.mybatis_demo.model.UserInfo">
select * from userinfo where username = #{username}
</select>
单元测试:
java
@Test
void getUserByUsername() {
List<UserInfo> userInfoList = userMapper.getUserByUsername("zhangsan");
System.out.println(userInfoList);
}
在使用"#{}"
参数占位符的情况下成功取到数据:
改为"${}"
后居然报错了,报错信息:在where子句中有未知的列'zhangsan':
为什么呢会这样呢?我明明是想要查询username
为zhangsan
的列呀,他为什么说没找到名字为"zhangsan"
的列呢?原因很简单,两种占位符的区别如下:
1.1 #{}:预编译处理
#{}
是预编译处理,等同于JDBC中的"?"
占位符,预编译的SQL语句不是有具体数值的语句,而是用(?)来代替具体数据,然后在执行的时候再调用setXXX()
方法把具体的数据传入。
xml
<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语句中。
xml
<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改为下面的形式:
xml
<select id="getUserByUsername" resultType="com.chenshu.mybatis_demo.model.UserInfo">
select * from userinfo where username = '${username}'
</select>
也成功查询到了数据:
1.3 ${}的缺陷:SQL注入
以登录功能为例,正常情况下,用户在前端需要传一个账号和密码给后端,后端再把账号和密码放到where子句
中查询数据库,如果查到对应的数据,就证明用户名和密码输入正确,登录成功,SQL语句如下:
sql
select * from userinfo where username = 'zhangsan' and password = '123';
输入正确就能查询到数据:
否则就查不到数据:
这时我们来使用${}
实现一下登录功能:
接口:
java
public UserInfo userLogin(@Param("username") String username, @Param("password") String password);
xml编写SQL:
xml
<select id="userLogin" resultType="com.chenshu.mybatis_demo.model.UserInfo">
select * from userinfo where username = '${username}' and password = '${password}'
</select>
单元测试:
java
@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注入语句:
java
@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改写为#{}
的形式:
xml
<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实现如下:
java
public List<ArticleInfo> sortArticle(@Param("sort") String sort);
xml
<select id="sortArticle" resultType="com.chenshu.mybatis_demo.model.ArticleInfo">
select * from articleinfo order by ${sort}
</select>
单元测试:根据createtime
字段排序
java
@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如下:
这时我们就可以使用${}
,接口以及实现如下:
java
public List<ArticleInfo> searchArticle(@Param("title") String title);
xml
<select id="searchArticle" resultType="com.chenshu.mybatis_demo.model.ArticleInfo">
select * from articleinfo where title like '%${title}%'
</select>
单元测试:
java
@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语句:
xml
<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
:
java
@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
定义一个映射规则来解决字段名和属性名不同的情况:
xml
<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
:
xml
<select id="getAll" resultMap="userInfoMap">
select * from userinfo
</select>
再次测试,成功拿到了name(对应数据库中的username)的值:
3. 多表查询
- 连接表需要查询的字段添加到实体类
- 编写接口的
getAll()
方法以及xml文件中的SQL
java
public List<ArticleInfo> getAll();
xml
<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>
- 单元测试并得到结果
java
@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>
标签
java
public int add(UserInfo userInfo);
xml
<insert id="add">
insert into userinfo(username, photo, password)
values (
#{name},
#{photo},
#{password})
</insert>
单元测试代码:
java
@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标签里面的内容,否则就不拼接:
xml
<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值):
java
@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标签
的地方放在参数的中间位置,如果位于最后面的话,会出现问题,像下面这样:
xml
<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
参数
xml
<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
,因此在使用时至少传入一个参数。
单元测试:
java
@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
来查询文章的操作时,又不确定传不传入的情况下:
sql
select * from articleinfo where title = 'MyBatis入门' and uid = 1;
java
public List<ArticleInfo> selectByCondition(ArticleInfo articleInfo);
where标签
的写法十分简单,作用如下:
- 根据
where标签
的内容,决定要不要拼接where
- 去掉最首个参数的
and
关键字,让sql符合数据库的执行标准
也可以使用<trim prefix = "where" prefixOverrides = "and">
标签替换
xml
<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
java
@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. 什么参数也不传:
java
@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:
java
@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. 传多个参数:
java
@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=",">
替换。
具体使用:
java
public void updateUserInfo(UserInfo userInfo);
xml
<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
字段:
java
@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语句:
sql
delete from articleinfo where id in(3,4,5)
具体实现:
java
public int deleteByIds(@Param("ids") List<Integer> ids);
注意事项:foreach标签
中的collection
中的值对应接口方法中定义的别名
xml
<delete id="deleteByIds">
delete from articleinfo
where id in
<foreach collection="ids" item="id" open="(" close=")" separator=",">
#{id}
</foreach>
</delete>
此时表数据如下,我想要删除id字段为7,8,9文章:
单元测试:
java
@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
成功删除了三条记录: