SSM Chapter 03 动态SQL 笔记
本章目标 :
1. 使用动态SQL完成多条件查询
MyBatis 的强大特性之一便是它的动态 SQL。如果你有使用 JDBC 或其它类似框架的经验,你就能体会到根据不同条件拼接 SQL 语句的痛苦。例如拼接时要确保不能忘记添加必要的空格,还要注意去掉列表最后一个列名的逗号。利用动态 SQL 这一特性可以彻底摆脱这种痛苦。
虽然在以前使用动态 SQL 并非一件易事,但正是 MyBatis 提供了可以被用在任意 SQL 映射语句中的强大的动态 SQL 语言得以改进这种情形。
动态 SQL 元素和 JSTL 或基于类似 XML 的文本处理器相似。在 MyBatis 之前的版本中,有很多元素需要花时间了解。MyBatis 3 大大精简了元素种类,现在只需学习原来一半的元素便可。MyBatis 采用功能强大的基于 OGNL 的表达式来淘汰其它大部分元素。
- if : 利用 if 实现简单条件查询
- choose (when , otherwise) : 相当于Java 中的switch 语句,通常与when 和 otherwise 搭配
- where : 简化 SQL 语句 中 where 的条件判断.
- set : 解决动态更新语句.
- trim : 可以灵活地去除多余的关键字.
- foreach : 迭代一个集合, 通常同于 in 条件
1.1 使用 if + where 实现多条件查询
1 . if:
if元素:动态 SQL 通常要做的事情是根据条件包含 where 子句的一部分
回顾上一章的演示示例 ---- 根据角色(角色 id 精确 匹配) 和 用户名称(模糊匹配) 完成对用户表的查询操作,在该示例中,采用的封装User对象入参,进行查询.通过之前的学习,我们了解到在查询条件不是很多 并且条件较为固定的情况下,最好的解决方案是 采用多参数直接入参的方式.这样的代码 比较清晰,可读性强,现在使用if改造一下这个示例.
修改UserMapper.java , 增加findUserList() 方法,并加入查询条件 , 代码如下:
/**
* 根据用户名(模糊查询) 和 用户角色 查询用户列表
* @param userName
* @param userRole
* @return
*/
List<User> findUserList(@Param("userName") String userName,
@Param("userRole") Integer userRole);
在上述代码中,参数使用了@Param注解指定了参数名, 接下来在UserMapper.xml中增加查询语句,代码如下:
<!--根据用户名(模糊查询) 和 用户角色 查询用户列表 -->
<select id="findUserList" resultType="u">
select * from smbms_user
where userName like '%${userName}%' and userRole=#{userRole}
</select>
测试类中添加相应的方法测试,首先测试两个条件均给出的情况下,观察输出结果,UserMapperTest.java部分代码如下:
@Test //测试根据用户名(模糊查询) 角色id 查询用户信息
public void testFindUserList(){
SqlSession sqlSession = MyBatisUtil.createSqlSession();
try {
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
String userName = "孙";
Integer userRole = 3;
List<User> list = userMapper.findUserList(userName, userRole);
list.forEach(logger :: info);
} catch (Exception e) {
e.printStackTrace();
}finally {
MyBatisUtil.close(sqlSession);
}
}
输出结果正常,然后模拟用户没有输入条件的情况下, 比如 只传入 用户名,而传入的用户角色为null,测试方法中修改代码如下:
@Test //测试根据用户名(模糊查询) 角色id 查询用户信息
public void testFindUserList(){
SqlSession sqlSession = MyBatisUtil.createSqlSession();
try {
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
String userName = "孙";
//Integer userRole = 3;
Integer userRole = null;
List<User> list = userMapper.findUserList(userName, userRole);
list.forEach(logger :: info);
} catch (Exception e) {
e.printStackTrace();
}finally {
MyBatisUtil.close(sqlSession);
}
}
通过观察该运行结果发现,查询出来的用户列表为空,这个查询结果是否正确呢? 让我们验证一下:
根据控制台打印出来的日志信息,可以看到MyBatis打印出来的SQL语句是:Preparing: select * from smbms_user where userName like ‘%孙%’ and userRole=? ,打印出来的参数是:Parameters: null,将SQL语句中的?代替为null,然后在MySQL数据库中执行该SQL语句,发现查询结果确实为空,这证明程序输出确实没有问题.
但是根据正确的逻辑思考,当用户没有输入用户角色的情况下,只根据用户名进行模糊查询即可,查询的结果应该是所有userName中包含有"孙"的全部用户信息,SQL语句如下:
select * from smbms_user where userName like '%孙%'
在MySQL 数据库 中 执行此SQL语句,查询的结果如下:
id userCode userName userPassword gender birthday phone address userRole createdBy creationDate modifyBy modifyDate
-- -------- -------- ------------ ------ ---------- ----------- --------------- -------- --------- --------------------- -------- ----------
10 sunlei 孙磊 0000000 2 1981-01-04 13387676765 北京市朝阳区管庄新月小区12楼 3 1 2015-05-06 10:52:07.0 NULL NULL
11 sunxing 孙兴 0000000 2 1978-03-12 13367890900 北京市朝阳区建国门南大街10号 3 1 2016-11-09 16:51:17.0 NULL NULL
根据业务需求这才是正确的查询结果,而示例代码运行的结果并不能达到我们的需求.用于在进行查询的时候,用户不一定会完整的输入所有的查询条件,当然 你也不能要求用户输入所有的查询条件,因此对于类似情况,之前示例代码的语句有漏洞,该如何修改呢?
解决方案是:使用动态SQL的 if 元素来实现多条件查询,具体做法如下:
修改UserMapper.xml,示例代码如下:
<!--根据用户名(模糊查询) 和 用户角色 查询用户列表 -->
<select id="findUserList" resultType="u">
select * from smbms_user
where
<!-- 在参数传入之前 先对其进行非空验证 -->
<if test="userName != null and userName != ''">
userName like '%${userName}%'
</if>
<if test="userRole != null">
and userRole=#{userRole}
</if>
</select>
在上述示例中,我们改造了SQL语句,利用if元素实现了简单的条件判断,if元素的test属性表示进入if内部时需要满足的条件.整个SQL语句的语义就变得非常简单了,若用户提供了userName参数(即: 满足 条件userName != null 并且 userName != ‘’),那么SQL的where条件就需要满足:userName like ‘%${userName}%’.
同理:若用户提供了userRole参数(即 条件 userRole != null 为真),那么where条件就还需要满足 and userRole=#{userRole},最终返回满足where这些where条件的数据列表.
这是一个非常有用的功能,相比之前的JDBC,若要达到同样的选择效果,则需要通过多重if else语句进行SQL的拼接.而MyBatis的动态SQL就要简单许多.
运行testFindUserList()方法,观察控制台输出了SQL语句以及查询结果.而控制台的SQL语句也是根据if元素的条件判断,重新对where 字句进行拼接而得到的,日志的查询结果也是正确的.当然还可以测试多种情况,在此不再逐一进行演示.
修改需求,若用户传入的用户名存为null或者"",而传入的角色id为具体的数值,比如,运行下面的测试代码,会得到什么结果呢?
@Test //测试根据用户名(模糊查询) 角色id 查询用户信息
public void testFindUserList(){
SqlSession sqlSession = MyBatisUtil.createSqlSession();
try {
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
String userName = "";
Integer userRole = 3;
List<User> list = userMapper.findUserList(userName, userRole);
list.forEach(logger :: info);
} catch (Exception e) {
e.printStackTrace();
}finally {
MyBatisUtil.close(sqlSession);
}
}
运行结果如下:
### SQL: select * from smbms_user where and userRole=?
### Cause: com.mysql.jdbc.exceptions.jdbc4.MySQLSyntaxErrorException: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'and userRole=3' at line 6
仔细分析报错信息,发现SQL语句错误,即 where 字句后面多一个"and",造成了SQL错误.
同样对于上述示例,若不输入任何条件,即测试方法中的两个参数均为空值,正常情况下控制台应该输出所有的用户信息,修改测试方法中部分代码如下:
String userName = "";
Integer userRole = null;
List<User> list = userMapper.findUserList(userName, userRole);
运行结果如下:
### SQL: select * from smbms_user where
### Cause: com.mysql.jdbc.exceptions.jdbc4.MySQLSyntaxErrorException: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'and userRole=3' at line 6
与之前运行的结果一样,后台的SQL语句报错,不同的是没有 where 字句,却多了一个where,造成SQL异常错误的原因 也 和之前分析的一样.
综上分析,若要解决此类问题,就需要智能的处理and和where,两种方式:
第一种,可以在SQL语句中拼接 where 1=1 并在第一个if后面加上and
修改UserMapper.xml,代码如下:
<!--根据用户名(模糊查询) 和 用户角色 查询用户列表 -->
<select id="findUserList" resultType="u">
select * from smbms_user
where 1=1
<!-- 在参数传入之前 先对其进行非空验证 -->
<if test="userName != null and userName != ''">
and userName like '%${userName}%'
</if>
<if test="userRole != null">
and userRole=#{userRole}
</if>
</select>
运行测试方法,控制台正常输出结果,可以看到SQL语句的拼写是:select * from smbms_user where 1=1 这种方式可以解决上述问题,但是唯一不足之处就是多了一个where 1=1 永恒为真的条件.
问题来了,有没有一种方式可以智能的拼接where,如果有条件,就拼接where,没有则不用拼接.
答案是肯定的,这就需要用到第二种方式,第二种方式是使用where元素
2 . where :
where 元素主要用来简化SQL语句中的where条件判断,并且能智能的处理and和or,不必担心多余关键字导致的语法错误。
使用第二种方式,修改UserMapper.xml,代码如下:
<!--根据用户名(模糊查询) 和 用户角色 查询用户列表 -->
<select id="findUserList" resultType="u">
select * from smbms_user
<where>
<!-- 在参数传入之前 先对其进行非空验证 -->
<if test="userName != null and userName != ''">
and userName like '%${userName}%'
</if>
<if test="userRole != null">
and userRole=#{userRole}
</if>
</where>
</select>
运行测试方法,控制输出的SQL语句是:select * from smbms_user,MyBatis根据传入的参数进行了智能的拼接,去掉了where.
若将userName参数的值改为null(或者空字符串),userRole参数给定具体的值,运行测试方法,控制台打印出的SQL语句是:select * from smbms_user WHERE userRole=? ,MyBatis也同样根据传入的参数进行了正确的拼接,自动剔除了where自己中的 and
若将userRole参数指定为null,而userName参数的值改为"孙",MyBatis 也同样进行正确的SQL语句拼接.
3 . if + trim:
在MyBatis中除了使用 if + where 实现多条件查询,还有一个更为灵活的元素 trim 可以代替之前的做法.
trim元素也会自动识别其标签内是否有返回值,若有返回值,会在自己包含的内容前加上某些前缀,也可在其后加上某些后缀,与之对应的属性是prefix 和 suffix; 也可把包含内容的首部某些内容覆盖(即:忽略),或者把尾部的某些内容覆盖 或 忽略,与之对应的属性是:prefixOverrides 和 suffixOverrides;
正因为trim有这样强大的功能,我们可以利用trim 来 替代 where 元素,并实现与where元素相同的效果.改造上一个示例,实现多条件用户表的查询操作.
修改UserMapper.xml 示例代码如下:
<select id="findUserList" resultType="u">
select * from smbms_user
<!--使用where关键字做前缀, 去除元素内多余的and或者or -->
<trim prefix="where" prefixOverrides="and | or">
<!-- 在参数传入之前 先对其进行非空验证 -->
<if test="userName != null and userName != ''">
and userName like '%${userName}%'
</if>
<if test="userRole != null">
and userRole=#{userRole}
</if>
</trim>
</select>
通过上述代码,我们来了解一下 trim 属性:
- prefix : 前缀.作用是通过自动识别是否有返回值后,在trim包含的内容上加上前缀,比如此处的 where
- suffix : 后缀.作用是在trim包含的内容上加上后缀
- prefixOverrides : 指定去除多余的前缀内容.如:此处的 and | or
- suffixOverrides : 指定去除多余的后缀内容.
最后运行测试方法,根据传入的不同参数,分别进行智能的SQL语句拼接处理,效果等同于where元素.
1.2 扩展:
MyBatis中$和#号的区别:
这两个符号在mybatis中最直接的区别就是:#
相当于对数据 加上 单引号,$
相当于直接显示数据(只讨论字符串类型的)。
-
#
对传入的参数视为字符串,也就是它会预编译,select * from user where name = #{name}
,比如传一个aaa,那么传过来就是select * from user where name = 'aaa'
; -
$
将不会将传入的值进行预编译,select * from user where name=${name}
,比如传一个aaa,那么传过来就是select * from user where name = aaa
; -
#
的优势就在于它能很大程度的防止sql注入,而$
则不行。
比如:用户进行一个登录操作,后台sql验证式样的:select * from user where username=#{name} and password = #{pwd}
,如果前台传来的用户名是“zhang”,密码是 “1 or 1=1”,用#
的方式就不会出现sql注入,而如果换成$
方式,sql语句就变成了select * from user where username=wang and password = 1 or 1=1
。这样的话就形成了sql注入。 - MyBatis排序时使用
order by
动态参数时需要注意,用$
而不是#
例如:按照某个列名排序时,需要使用order by+动态参数的方式,这时就需要使用$
,ORDER BY ${columnName}
2 . 使用动态SQL实现更新操作
MyBatis 的真正强大在于它的映射语句,这是它的魔力所在。由于它的异常强大,映射器的 XML 文件就显得相对简单。如果拿它跟具有相同功能的 JDBC 代码进行对比,你会立即发现省掉了将近 95% 的代码。MyBatis 为聚焦于 SQL 而构建,以尽可能地为你减少麻烦。
在上一节中 我们 学习使用动态SQL实现多条件查询,对于查询条件多变的情况,动态SQL都可以灵活 智能的进行处理,方便开发.现在我们来学习如何使用动态SQL实现更新操作
2.1 使用 if + set 改造更新操作
回顾之前的演示示例 ----- 修改用户信息操作,在该示例中,采用的是封装User对象入参,根据用户id进行用户信息修改,当操作数据的时候,每个字段都进行了赋值更新.但是在实际项目中,用户在进行信息的更新操作时,并不一定所有的数据都会修改,对于用户没有修改的数据,数据库不需要进行相应的更新操作.即更新用户表数据时,若某个参数传入值为null时,不需要set该字段.
现在就先测试修改用户信息示例,观察是否满足正常的业务需求.
UserMapper.java中添加 modifyUser ()方法,并传入User参数,增加如下代码:
/**
* 修改用户信息
* @param user
* @return
*/
int modifyUser(User user);
修改UserMapper.xml,增加修改语句,代码如下:
<!-- 修改用户信息 -->
<update id="modifyUser" parameterType="u">
update smbms_user set
userCode=#{userCode},userName=#{userName},userPassword=#{userPassword},
gender=#{gender},birthday=#{birthday},phone=#{phone},address=#{address},
userRole=#{userRole},modifyBy=#{modifyBy},modifyDate=#{modifyDate}
where id = #{id}
</update>
修改测试类UserMapperTest.java,增加testModifyUser() 方法, 进行数据的修改,代码如下:
@Test //测试user 入参 修改用户信息
public void testModifyUser() throws IOException {
int count = 0;
SqlSession sqlSession = MyBatisUtil.createSqlSession();
User user = new User();
try {
user.setId(24);
user.setUserName("Rose");
user.setAddress("北京市 怀柔区");
user.setModifyBy(1);
user.setModifyDate(new Date());
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
count = userMapper.modifyUser(user);
//模拟异常,进行回滚操作
//int i = 2/0;
sqlSession.commit();
} catch (Exception e) {
e.printStackTrace();
sqlSession.rollback();
count = 0;
}finally {
MyBatisUtil.close(sqlSession);
}
logger.info("count====>"+count);
}
在上述代码中,对于更新方法(modifyUser()) 的参数User对象,只设置了用户名称(userName),地址(address),更新者(modifyBy),更新时间(modifyDate),用户id(id) 这5个属性.即 数据库只对四个字段(userName,address,modifyBy.modifyDate) 进行相应等更新操作(注:用户id为更新的where条件).
但是运行测试代码之后,查询更新之后的该条数据信息如下:
id userCode userName userPassword gender birthday phone address userRole createdBy creationDate modifyBy modifyDate
-- -------- -------- ------------ ------ -------- ----- ------- -------- --------- --------------------- -------- ---------------------
24 NULL Rose NULL NULL NULL NULL 北京市 怀柔区 NULL 1 2019-07-06 10:59:25.0 1 2017-08-09 17:44:55.0
通过结果发现,除了设值的四个字段被更新之外,其他的字段也被更新了,并且更新为null.通过日志输出的MyBatis的SQL语句和参数,发现未被设置的参数也进行问题呢了set操作.该如何解决问题呢?
使用动态SQL中的set元素来处理.
set元素:主要用于更新操作,它的主要功能和where元素差不多,主要是在包含的语句前输出了一个set,若包含的语句是以逗号结束的,会自动把该逗号忽略掉,再配合if元素就可以动态的更新需要修改的字段;而不需要修改的字段,则可以不再更新.
改造UserMapper.xml中的修改用户信息的语句,代码如下:
<update id="modifyUser" parameterType="u">
update smbms_user
<set>
<if test="userCode != null and userCode != ''">
userCode=#{userCode},
</if>
<if test="userName != null and userName != ''">
userName=#{userName},
</if>
<if test="userPassword != null and userPassword != ''">
userPassword=#{userPassword},
</if>
<if test="gender != null">
gender=#{gender},
</if>
<if test="birthday != null">
birthday=#{birthday},
</if>
<if test="phone != null and phone != '' ">
phone=#{phone},
</if>
<if test="address != null and address != ''">
address=#{address},
</if>
<if test="userRole != null">
userRole=#{userRole},
</if>
<if test="modifyBy != null">
modifyBy=#{modifyBy},
</if>
<if test="modifyDate != null">
modifyDate=#{modifyDate}
</if>
</set>
where id = #{id}
</update>
在上述代码中,使用set标签,不仅可以动态的配置set关键字,还可以剔除追加到条件末尾的任何不相关的逗号(因为在update语句中,使用if标签,若后面的if没有执行,则导致在语句末尾残留多余的逗号).
运行测试方法之后,控制台的日志输出如下:
Preparing: update smbms_user SET userName=?, address=?, modifyBy=?, modifyDate=? where id = ?
Parameters: Rose(String), 北京市 怀柔区(String), 1(Integer), 2017-07-10 07:44:32.723(Timestamp), 24(Integer)
Updates: 1
通过观察控制台日志输出的SQL语句和参数,确认最终的运行结果正确.
经验 : 通过对MyBatis的学习,我们会发现使用MyBatis可以很方便的调试代码.特别是对于SQL错误,或者执行对数据库的操作之后,结果跟预期不一致,我们都可以在控制台找到日志输出的SQL语句 以及 参数,放在数据库中执行,找出问题所在,直观方便
2.2 使用 if+trim 改造更新操作
我们也可以使用trim元素来替代set元素,并实现与set一样的效果,接下来就改造上一个示例来实现用户表的修改操作.
修改UserMapper.xml,示例代码如下:
<update id="modifyUser" parameterType="u">
update smbms_user
<!-- 使用trim 元素代替 set 设置前缀是set,去除掉最后一个多余的逗号-->
<trim prefix="set" suffixOverrides="," >
<if test="userCode !=null and userCode != ''">
userCode=#{userCode},
</if>
<if test="userName !=null and userName != ''">
userName=#{userName},
</if>
<if test="userPassword !=null and userPassword != ''">
userPassword=#{userPassword},
</if>
<if test="gender !=null">
gender=#{gender},
</if>
<if test="birthday !=null">
birthday=#{birthday},
</if>
<if test="phone !=null and phone!='' ">
phone=#{phone},
</if>
<if test="address !=null and address!=''">
address=#{address},
</if>
<if test="userRole !=null">
userRole=#{userRole},
</if>
<if test="modifyBy !=null">
modifyBy=#{modifyBy},
</if>
<if test="modifyDate !=null">
modifyDate=#{modifyDate}
</if>
</trim>
where id = #{id}
</update>
**运行测试方法,运行结果正确. **
2.3 使用if+trim改造插入操作
另外trim元素,也可以用于insert语句
例如:
<!-- 新增一条用户信息 -->
<insert id="add">
insert into smbms_user
<trim prefix="(" suffix=")" suffixOverrides=",">
<if test="userCode !=null and userCode != ''">
userCode,
</if>
<if test="userName !=null and userName != ''">
userName,
</if>
<if test="userPassword !=null and userPassword != ''">
userPassword,
</if>
<if test="gender !=null">
gender,
</if>
<if test="birthday !=null">
birthday,
</if>
<if test="phone !=null and phone!='' ">
phone,
</if>
<if test="address !=null and address!=''">
address,
</if>
<if test="userRole !=null">
userRole,
</if>
<if test="createdBy !=null">
createdBy,
</if>
<if test="creationDate !=null">
creationDate
</if>
</trim>
<trim prefix="values (" suffix=")" suffixOverrides=",">
<if test="userCode !=null and userCode != ''">
#{userCode},
</if>
<if test="userName !=null and userName != ''">
#{userName},
</if>
<if test="userPassword !=null and userPassword != ''">
#{userPassword},
</if>
<if test="gender !=null">
#{gender},
</if>
<if test="birthday !=null">
#{birthday},
</if>
<if test="phone !=null and phone!='' ">
#{phone},
</if>
<if test="address !=null and address!=''">
#{address},
</if>
<if test="userRole !=null">
#{userRole},
</if>
<if test="createdBy !=null">
#{createdBy},
</if>
<if test="creationDate !=null">
#{creationDate}
</if>
</trim>
</insert>
对于trim元素的属性,前面已经介绍过,此处不再说明
经验
在实际项目中,用户操作行为多种多样,比如说当用户进入修改界面 而 不进行任何数据的修改,但是同样单击了 “保存” 操作,那么是不是就不需要进行字段的更新操作呢?
答案是否定的,这是由于但凡用户单击了 “修改” 按钮,进入修改页面,我们就认为用户有进行修改操作的行为,无论他是否进行字段信息的修改,系统设计都需要进行全部字段的更新操作.当然,实际上还有一种用户操作,即 : 用户清空了某些字段信息,按照我们之前的讲解,根据if标签的判断,程序不会进行相应的更新操作,这显然是跟用户的实际需求相悖的.
在实际项目中,一般设计 DAO 层的更新操作, update 的 set 中 不会出现 if 标签,即无论用户是否全部修改,我们都需要更新所有的字段信息(注意:前端POST请求传到后台的User对象内一般情况下,对所有的属性都进行了设值,所以不存在我们写的测试类中出现的某些属性为null的情况).
实际运用中,if 标签一般都是用在 where 标签中,本章介绍 set 中设置 if 标签,目的是便于 初学者 进行 相应的练习和加深对 if 的理解
3 . 使用 foreach 完成复杂查询
在前两个小节中,我们学习了动态SQL中的 if , where , trim 元素 来处理一些简单查询操作, 对于一些 SQL 语句中 含有 in 条件的,需要迭代条件集合来生成的情况, 这是 就需要使用 foreach 标签来实现SQL条件的迭代.
3.1 MyBatis 入参 为数组类型 的 foreach 迭代
我们先了解一下 foreach 的基本用法 和 属性, foreach 主要用在 构建 in 条件中, 它可以在 SQL 语句中迭代一个集合.它的主要属性有: item index collection separator close open. 下面通过一个根据指定角色列表来获取用户信息列表的示例 进行详细介绍.
首先 修改UserMapper.java , 增加接口方法 : 根据用户角色列表获取该角色列表下的用户信息,参数为角色列表(roleIds),该参数类型为整形数组. 示例代码如下:
/**
* 根据用户角色列表 获取该角色列表下 所有的用户信息 - foreach_array
* 演示 foreach 元素 迭代数组类型
* @param roleIds
* @return
*/
List<User> findUserByRoleIdForeachArray(Integer[] roleIds);
其次 根据需求分析,SQL语句应为: select * from smbms_user where userRole in (角色1,角色2,…), in 里面为角色列表. 修改UserMapper.xml,增加相应的findUserByRoleIdForeachArray的select标签,示例代码如下:
<!-- 根据用户角色列表,获取该用户列表下 所有的用户信息
使用foreach元素对参数进行迭代 -->
<select id="findUserByRoleIdForeachArray" resultType="u">
select * from smbms_user
<where>
<if test="array!=null and array.length>0">
userRole in
<!-- collection:参数类型
item : 表示集合中每一个元素进行迭代时的别名
open : 表示该语句以什么开始,既然是in条件语句,所以必然是以"("开始
close : 表示该语句以什么结束, 既然是in条件语句,所以必然是以")"结束
separator:表示每次进行迭代之间以什么符号作为分隔符,既然是in条件语句,
所以以,分割 -->
<foreach collection="array" item="roleId"
open="(" close=")" separator=",">
#{roleId}
</foreach>
</if>
</where>
</select>
对于SQL条件循环(in 语句),需要使用 foreach 标签.通过上述代码,来介绍下 foreach 的基本属性:
-
item : 表示集合中的每一个元素 进行 迭代时的别名 (如 此处的 “roleId”);若参数为Map对象
-
index : 索引(index)变量,指定一个名称,用于表示在迭代过程中,每次迭代到的位置 (此处省略,未指定);
-
open : 表示该语句以什么开始,既然是in条件语句,所以必然是以"("开始;
-
close : 表示该语句以什么结束, 既然是in条件语句,所以必然是以")"结束;
-
separator : 表示每次进行迭代之间以什么符号作为分隔符,既然是in条件语句,所以以 “,” 分割;
-
collection : 最关键 并容易出错的属性,需要格外注意,该属性必须指定,不同情况下,该属性的值是不一样的.主要要三种情况:
- 若入参为单参数 且 参数类型 是一个数组的时候,collection 属性值为array(此处 传入参数 Integer[] roldeIds 为数组类型,故collection的属性值为 “array”);
- 若入参为单参数 且 参数类型 是一个List的时候,collection 属性值为list;
- 若入参为多参数,就需要把它们封装成一个Map 进行 处理,同时Map中的对应的key的value类型必须为list或者array
注意:你可以将任何可迭代对象(如 List、Set 等)、Map 对象或者数组对象传递给 foreach 作为集合参数。当使用可迭代对象或者数组时,index 是当前迭代的次数,item 的值是本次迭代获取的元素。当使用 Map 对象(或者 Map.Entry 对象的集合)时,index 是键,item 是值。
最后 UserMapperTest.java 增加测试方法 示例代码如下:
@Test //测试数组入参 根据用户角色列表 获取该角色列表下 所有的用户信息
public void testFindUserByRoleIdForeachArray(){
SqlSession sqlSession = MyBatisUtil.createSqlSession();
try {
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
Integer[] roleIds = {1,2};
List<User> list = userMapper.findUserByRoleIdForeachArray(roleIds);
list.forEach(logger :: info);
} catch (Exception e) {
e.printStackTrace();
}finally {
MyBatisUtil.close(sqlSession);
}
}
在上述代码中,封装角色列表数组入参,运行测试方法,输出正确结果.
注意: 在上述示例中,我们发现UserMapper.xml中的select : findUserByRoleIdForeachArray 中并没有指定parameterType,这样也是没有问题的.因为配置文件中的parameterType 是可以不配置的,MyBatis会自动把它封装成一个Map传入,但是也需要注意,若入参为Collection时,不能直接传入Collection对象,需要先将其转换成List 或者数组才能传入,具体原因可参看MyBatis 源码
3.2 MyBatis 入参 为List 类型 的 foreach迭代 :
在上一个示例中,实现通过指定角色列表获得相应的用户信息列表,方法参数为一个数组,现在我们更改参数类型,传入一个 List 实例 来实现同样的需求.
首先 修改UserMapper.java,增加接口方法(根据传入的用户角色列表 获取 该角色列表下所有的用户信息),参数为角色列表(roleIds), 不过与上一个示例不同的是,该参数类型为List,示例代码如下:
/**
* 根据用户角色列表 获取该角色列表下 所有的用户信息 - foreach_list
* 演示 foreach 元素 迭代数组类型
* @param roleIds
* @return
*/
List<User> findUserByRoleIdForeachList(List<Integer> roleIds);
其次 修改对应的UserMapper.xml 增加相应的findUserByRoleIdForeachList 的select标签, 代码如下:
<!-- 根据用户角色列表,获取该用户列表下 所有的用户信息
使用foreach元素对参数(List)进行迭代 -->
<select id="findUserByRoleIdForeachList" resultType="u">
select * from smbms_user
<where>
<if test="list!=null and list.size()>0">
userRole in
<!-- collection:参数类型
item : 表示集合中每一个元素进行迭代时的别名
open : 表示该语句以什么开始,既然是in条件语句,所以必然是以"("开始
close : 表示该语句以什么结束, 既然是in条件语句,所以必然是以")"结束
separator:表示每次进行迭代之间以什么符号作为分隔符,既然是in条件语句,
所以以,分割 -->
<foreach collection="list" item="roleId"
open="(" close=")" separator=",">
#{roleId}
</foreach>
</if>
</where>
</select>
最后 修改测试类UserMapperTest.java,增加测试方法,代码如下:
@Test //测试数组入参 根据用户角色列表 获取该角色列表下 所有的用户信息
public void testFindUserByRoleIdForeachList(){
SqlSession sqlSession = MyBatisUtil.createSqlSession();
try {
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
List<Integer> roleIds = Arrays.asList(1,2);
List<User> list = userMapper.findUserByRoleIdForeachList(roleIds);
list.forEach(logger :: info);
} catch (Exception e) {
e.printStackTrace();
}finally {
MyBatisUtil.close(sqlSession);
}
}
该测试方法中,把参数角色列表 roleIds 封装成 List 进行入参即可. 测试运行后,结果正确.
注意 : foreach 元素非常强大,允许我们指定一个集合,并指定开始和结束的字符,也可加入一个分隔符到迭代器中,并能够处理该分隔符,不会出现多余的分隔符
3.3 MyBatis 入参 为 Map 类型的 foreach 迭代
在以上的两个示例中, MyBatis 入参均为一个参数,若多个参数入参该如何处理?
比如说在上一示例中 的 需求更改为增加 一个参数 gender,要求查询出指定性别和用户角色列表下的所有用户信息列表.
除了使用之前章节介绍的 @Param 注解 外, 还可以按照前文中 介绍的 collection 属性的时候, 提到过的第三种情况:若入参为多个参数,就需要把它们封装成一个 Map 进行处理.此处我们就采用这种处理方式来解决此需求.
首先 : 修改UserMapper.java,增加接口方法:根据传入的用户角色列表和性别获取相应的用户信息,示例代码如下:
/**
* 根据用户角色列表和性别(多参数) 获取该角色列表下指定性别的 所有用户信息 -foreach_map
* 演示 foreach 元素 迭代数组类型
* @param conditionMap:性别的key为gender,角色列表的key为roleIds
* @return
*/
List<User> findUserByRoleIdForeachMap(Map<String,Object> conditionMap);
其次 : 修改UserMapper.xml,增加相应的findUserByRoleIdForeachMap 的select元素 代码如下:
<!-- 根据用户角色列表,用户性别,获取该用户列表下 指定性别的所有用户信息
使用foreach元素对参数(Map中的key为List)进行迭代 -->
<select id="findUserByRoleIdForeachMap" resultType="u">
select * from smbms_user where gender=#{gender}
<!--使用if对集合元素进行非空判断-->
<if test="roleIds!=null and roleIds.size()>0">
and userRole in
<!-- collection:参数类型
item : 表示集合中每一个元素进行迭代时的别名
open : 表示该语句以什么开始,既然是in条件语句,所以必然是以"("开始
close : 表示该语句以什么结束, 既然是in条件语句,所以必然是以")"结束
separator:表示每次进行迭代之间以什么符号作为分隔符,既然是in条件语句,
所以以,分割 -->
<foreach collection="roleIds" item="roleId"
open="(" close=")" separator=",">
#{roleId}
</foreach>
</if>
</select>
在上述代码中,由于入参为Map,那么在SQL语句中需要根据key分别获得相应的value值,比如:SQL语句中的#{gender} 获取的是Map中key为 “gender” 的性别条件,而foreach中的collection=“roleIds” ,则获取的是Map中key为"roleIds" 的角色id 的集合;
最后: 修改UserMapperTest.java, 增加测试方法,示例代码如下:
@Test //测试Map入参(多参数) 根据用户角色列表 以及性别 获取该角色列表下 指定性别的所有用户信息
public void testFindUserByRoleIdForeachMap(){
SqlSession sqlSession = MyBatisUtil.createSqlSession();
try {
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
List<Integer> roleIds = Arrays.asList(1,2);
Map<String,Object> map = new HashMap<>();
map.put("gender", 1);
map.put("roleIds",roleIds);
List<User> list = userMapper.findUserByRoleIdForeachMap(map);
list.forEach(logger :: info);
} catch (Exception e) {
e.printStackTrace();
}finally {
MyBatisUtil.close(sqlSession);
}
}
运行测试代码,控制台正确输出结果.
通过对foreach标签的collection属性的学习,我们发现不管传入的是单参数 还是多参数,都可以得到有效解决. 若是单参数,是否可以封装成Map进行入参呢 ? 让我们测试一下:
现在就更改之前的演示示例:根据用户角色列表 获取该角色列表下所有的用户信息,此处传入的参数不再使用数组或者List,而是封装Map来实现.
修改UserMapper.java,增加接口方法,示例代码如下:
/**
* 根据用户角色列表(单参数) 获取该角色列表下的 所有用户信息 -foreach_map(单参数封装成map)
* 演示 foreach 元素 迭代Map中的key
* @param conditionMap key:rKey value:List<Integer>
* @return
*/
List<User> findUserByRoleIdForeachMapOne(Map<String,Object> conditionMap);
修改UserMapper.xml,增加相应的 示例代码如下:
<!-- 根据用户角色列表,获取该用户列表下 所有的用户信息
使用foreach元素对参数(map中的key key的类型为List)进行迭代 -->
<select id="findUserByRoleIdForeachMapOne" resultType="u">
select * from smbms_user where userRole in
<!-- collection:参数类型
item : 表示集合中每一个元素进行迭代时的别名
open : 表示该语句以什么开始,既然是in条件语句,所以必然是以"("开始
close : 表示该语句以什么结束, 既然是in条件语句,所以必然是以")"结束
separator:表示每次进行迭代之间以什么符号作为分隔符,既然是in条件语句,
所以以,分割 -->
<foreach collection="rKey" item="roleId"
open="(" close=")" separator=",">
#{roleId}
</foreach>
</select>
修改UserMapperTest.java, 增加相应的测试方法,示例代码如下:
@Test //测试map入参 根据用户角色列表 获取该角色列表下的所有用户信息(单参数封装成map)
public void testFindUserByRoleIdForeachMapOne(){
SqlSession sqlSession = MyBatisUtil.createSqlSession();
try {
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
List<Integer> roleIds = Arrays.asList(1,2);
Map<String,Object> map = new HashMap<>();
map.put("rKey",roleIds);
List<User> list = userMapper.findUserByRoleIdForeachMapOne(map);
list.forEach(logger :: info);
} catch (Exception e) {
e.printStackTrace();
}finally {
MyBatisUtil.close(sqlSession);
}
}
在上述代码中,注意collection的属性值不再是list,而是我们自己设置的roleMap的key,即:rKey,运行测试方法,结果正确.
由此可以总结,MyBatis在进行参数入参的时候,都会把它封装成一个Map,而Map的key就是参数名,对应的参数值就是Map的value.若参数为集合的时候,Map的key会根据传入的是List还是数组对象 相应的指定为 “list” 或者 “array”.
3.4 使用foreach元素批量插入数据
foreach
中可以有两种方式批量插入数据:
方式1:
INTERT INTO TableName (列名) Values(值1,值2,值3….),(值1,值2,值3….), ...
方式2:
INTERT INTO TableName (列名) Values(值1,值2,值3….);
INTERT INTO TableName (列名) Values(值1,值2,值3….);
...
第一种方式的XML映射文件foreach标签部分如下:
<!--演示批量插入操作 注意分隔符为, -->
<insert id="insertUserList" useGeneratedKeys="true"
keyProperty="id" keyColumn="id">
insert into smbms_user
(userCode,userName,userPassword,gender,birthday,phone,
address,userRole,createdBy,creationDate)
values
<foreach item="item" collection="list" separator=",">
(#{item.userCode},#{item.userName},#{item.userPassword},
#{item.gender},#{item.birthday},#{item.phone},
#{item.address},#{item.userRole},#{item.createdBy},
#{item.creationDate})
</foreach>
</insert>
第二种方式的XML映射文件foreach标签部分如下:
<!--演示批量插入操作 注意分隔符为; -->
<insert id="insertUserList" useGeneratedKeys="true"
keyProperty="id" keyColumn="id">
<foreach item="item" collection="list" separator=";">
insert into smbms_user
(userCode,userName,userPassword,gender,birthday,phone,
address,userRole,createdBy,creationDate)
values
(#{item.userCode},#{item.userName},#{item.userPassword},
#{item.gender},#{item.birthday},#{item.phone},
#{item.address},#{item.userRole},#{item.createdBy},
#{item.creationDate})
</foreach>
</insert>
使用第一种方式最佳,每次批量插入数据应该是一次性发一条SQL语句,效率高于每次发同样一条SQL语句。
小结:
(1) MyBatis接受的参数类型 : 基本类型 对象 List 数组 Map
(2) 无论MyBatis的入参是那种参数类型,MyBatis 都会将参数 封装成一个Map,对于单参数入参的情况:
- 若入参为基本类型 : 变量名 作为 key , 变量值作为value,此时生成的Map只有一个元素;
- 若入参为对象 : 对象的属性名作为key , 属性值为value;
- 若入参为List : 默认 “list” 作为key,该 List 即为value;
- 若入参为数组 : 默认 “array” 作为key, 该 数组 即为value;
- 若入参为Map: 键值不变
3.5 choose ( when otherwise)
对于某些查询需求 , 虽有多个查询条件,但是我们不想应用所有的条件,只想选择其中一种情况下查询结果.其实和Java中的switch语句相似,MyBatis提供choose元素来满足这种需求.
choose元素的作用相当于Java中的switch语句,基本上跟JSTL中的choose的作用和用法是一样的,通常就是搭配 when otherwise 使用,下面就通过一个示例来演示说明其用法
根据条件(用户名称 用户角色 用户编码 创建时间) 查询用户表,具体要求:查询条件提供前三个(用户名称 用户角色 用户编码)中的任意一个即可,若前三个条件都不提供,则使用默认提供的最后一个条件(创建时间),来完成查询操作.
修改UserMapper.java,增加接口方法,示例代码如下:
/**
* 查询用户列表
* @param userName : 指定用户名
* @param userRole : 指定用户角色
* @param userCode : 指定用户编码
* @param creationDate : 指定创建时间
* @return
*/
List<User> findUserByChoose(@Param("userName") String userName,
@Param("userRole") Integer userRole,
@Param("userCode") String userCode,
@Param("creationDate") Date creationDate);
在上述代码中,使用@Param实现多条件入参,修改UserMapper.xml,增加相应的findUserByChoose的select元素,示例代码如下:
<!--查询用户列表 -->
<select id="findUserByChoose" resultType="u">
select * from smbms_user
<where>
<choose>
<when test="userName != null and userName != ''">
and userName like concat('%',#{userName},'%')
</when>
<when test="userCode != null and userCode != ''">
and userCode = #{userCode}
</when>
<when test="userRole != null">
and userRole = #{userRole}
</when>
<otherwise>
and
DATE_FORMAT(creationDate,'%Y-%m-%d')=DATE_FORMAT(#{creationDate},'%Y-%m-%d')
</otherwise>
</choose>
</where>
</select>
在上述代码中,使用 choose(when otherwise) 来实现需求,choose一般与when otherwise配套使用.
when 元素 :
当前test属性中条件满足的时候,就会输出when元素中的内容。
跟Java中的switch效果差不多的是 同样按照条件的顺序来进行处理, 并当 when 中一旦有条件满足的时候,
就会跳出 choose ,即所有的when和 otherwise条件中,只有一个条件输出.
otherwise 元素 :
当when中所有条件都不满足的时候,就会自动输出otherwise元素中的内容.
UserMapperTest.java中,增加测试方法 进行相应的测试,示例代码如下:
@Test //查询用户列表
public void testFindUserByChoose(){
SqlSession sqlSession = MyBatisUtil.createSqlSession();
try {
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
String userName = "";
String userCode = "";
Integer userRole = null;
Date creationDate = new SimpleDateFormat("yyyy-MM-dd").parse("2016-04-21");
List<User> list = userMapper.findUserByChoose(userName, userRole, userCode, creationDate);
list.forEach(logger :: info);
} catch (Exception e) {
e.printStackTrace();
}finally {
MyBatisUtil.close(sqlSession);
}
}
运行测试方法,无论增加几个条件,最终关于条件输出的SQL语句都只有一个,并执行正确查询 输出结果.
4 . MyBatis 实现 分页 功能
4.1 MyBatis分页
MySQL 的 分页功能 是 基于内存的分页,即 查出来所有记录,再按起始位置和页面容量取出结果.
现在 我们 就给用户管理功能模块 的查询用户列表功能增加分页,根据用户名(模糊查询),用户角色 查询分页信息,要求结果列表按照 创建时间降序排列
具体DAO层的实现步骤如下所示:
(1) 根据用户名(模糊查询),用户角色,使用聚合函数 count() 获取总记录数 代码如下:
/**
* 根据用户名(模糊) 用户角色 获取总记录数
* @param userName : 用户名
* @param userRole : 角色id
* @return
*/
int findCountByPages(@Param("userName") String userName,
@Param("userRole") Integer userRole);
(2) 实现分页 , 通过 limit 起始位置,页面容量,修改UserMapper.java,增加分页方法,示例代码如下:
/**
* 根据用户名(模糊) 用户角色 获取用户列表信息(分页显示)
* @param userName : 用户名
* @param userRole : 角色id
* @param currentNo : 当前页
* @param pageSize : 页面容量
* @return
*/
List<User> findUserByPages(@Param("userName") String userName,
@Param("userRole") Integer userRole,
@Param("from") Integer currentNo,
@Param("pageSize") Integer pageSize);
上述代码增加了两个参数:起始位置(from) 和 页面容量 (pageSize) ,用于实现分页查询
(3) 修改UserMapper.,xml,分别增加相应的查询SQL语句,示例代码如下
<!--使用sql元素封装用户查询的条件 以实现SQL语句的复用 -->
<sql id="userColumns">
<where>
<if test="userName != null and userName != ''">
and userName like concat ('%',#{userName},'%')
</if>
<if test="userRole != null">
and userRole = #{userRole}
</if>
</where>
</sql>
<!-- 根据用户名(模糊) 用户角色 获取总记录数 -->
<select id="findCountByPages" resultType="int">
select count(1) from smbms_user
<!-- 引入外部的SQL -->
<include refid="userColumns"/>
</select>
<!--根据用户名(模糊) 用户角色 获取用户列表信息(分页显示) -->
<select id="findUserByPages" resultType="u">
select * from smbms_user
<!-- 引入外部的SQL -->
<include refid="userColumns"/>
order by creationDate DESC limit #{from},#{pageSize}
</select>
(4) 上述代码中,limit后为参数:起始位置(from) 和 页面容量(pageSize).
修改测试方法,进行分页列表测试,示例代码如下:
@Test //查询用户列表(分页显示)
public void testFindUserByPages(){
SqlSession sqlSession = MyBatisUtil.createSqlSession();
try {
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
String userName = "";
Integer userRole = null;
//获取总记录数
int count = userMapper.findCountByPages(userName, userRole);
//当前页 : 1
int currentPageNo = 1;
//页面容量 : 3
int pageSize = 3;
//总页数
int totalPage = count % pageSize == 0 ? count/pageSize : count/pageSize + 1;
logger.info("totalCount ===> " + count);
logger.info("totalPage ===> " + totalPage);
List<User> list = userMapper.findUserByPages(userName, userRole, (currentPageNo - 1) * pageSize, pageSize);
list.forEach(logger :: info);
} catch (Exception e) {
e.printStackTrace();
}finally {
MyBatisUtil.close(sqlSession);
}
}
在上述代码中,根据传入的起始位置((currentPageNo-1)*pageSize) 和页面容量(pageSize=3) 进行相应分页,查看第一页的数据列表.运行测试方法,输出了正确的分页列表.
4.2 MyBatis分页之RowBounds的使用
Mybatis如何分页查询?Mysql中可以使用limit语句,但limit并不是标准SQL中的,如果是其它的数据库,则需要使用其它语句。MyBatis提供了RowBounds类,用于实现分页查询。RowBounds中有两个数字,offset和limit。
offset属性是偏移量,即从第几行开始读取记录。limit是限制条数,从源码可知,默认值为0和Java的最大整数
UserMapper.java
代码如下:
/**
* 查询分页信息
* @return
*/
List<User> findUserPagesByRowBounds(@Param("userName")String userName,
@Param("userRole")Integer userRole,
RowBounds rowBounds);
UserMapper.xml
文件代码如下:
<select id="findUserPagesByRowBounds" resultType="user">
select * from smbms_user
<include refid="userColumns"/>
order by creationDate
</select>
<!--使用sql元素封装用户的条件 -->
<sql id="userColumns">
<where>
<if test="userName!=null and userName!=''">
and userName like concat('%',userName,'%')
</if>
<if test="userRole!=null">
and userRole=#{userRole}
</if>
</where>
</sql>
测试代码如下:
/**
* 使用RowBounds
*/
@Test
public void testFindUserPagesByRowBounds() {
try (SqlSession session = MyBatisUtil.openSession()) {
String userName = "";
int userRole = 3;
//当前页面
int pageIndex = 1;
//页面显示两条
int pageSize = 2;
//封装RowBounds参数
RowBounds rowBounds = new RowBounds((pageIndex-1)*pageSize,
pageSize);
UserMapper userMapper = session.getMapper(UserMapper.class);
List<User> userList = userMapper.findUserPagesByRowBounds(userName,userRole,rowBounds);
userList.forEach(u->log.debug("user"+u));
}
}
5 . 补充分页插件PageHelper使用
5.1 MyBatis分页插件
在MyBatis中我们需要实现分页功能,如果我们连接的是mysql我们就要写mysql的分页sql,连接oracle就要写oracle的sql语句,这是很不友好的,而我们针对各种不同的数据库的分页有一个插件PageHelper
5.2 PageHelper
这一定是最方便使用的分页插件。分页插件支持任何复杂的单表、多表分页。 支持常见的 12 种数据库。Oracle,MySql,MariaDB,SQLite,DB2, PostgreSQL,SqlServer 等
支持常见的RowBounds(PageRowBounds), PageHelper.startPage 方法调用, Mapper 接口参数调用
官网地址:https://pagehelper.github.io/
更多用法参照官方文档:https://pagehelper.github.io/docs/howtouse/
5.3 使用方法:
1. 引入分页插件
引入分页插件有下面2种方式,推荐使用 Maven 方式。
在 pom.xml 中添加如下依赖:
<!-- com.github.pagehelper/pagehelper 推荐使用最新版本 -->
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper</artifactId>
<version>5.1.10</version>
</dependency>
2. 配置拦截器插件
特别注意,新版拦截器是 com.github.pagehelper.PageInterceptor
。 com.github.pagehelper.PageHelper
现在是一个特殊的 dialect
实现类,是分页插件的默认实现类,提供了和以前相同的用法。
在 MyBatis 配置 xml 中配置拦截器插件
<!--
plugins在配置文件中的位置必须符合要求,否则会报错,顺序如下:
properties?, settings?,
typeAliases?, typeHandlers?,
objectFactory?,objectWrapperFactory?,
plugins?,
environments?, databaseIdProvider?, mappers?
-->
<plugins>
<!-- com.github.pagehelper为PageHelper类所在包名 -->
<plugin interceptor="com.github.pagehelper.PageInterceptor">
<!-- 使用下面的方式配置参数,后面会有所有的参数介绍 -->
<property name="param1" value="value1"/>
</plugin>
</plugins>
3. 分页插件参数介绍
分页插件提供了多个可选参数,这些参数使用时,按照上面两种配置方式中的示例配置即可。
分页插件可选参数如下:
-
dialect
:默认情况下会使用 PageHelper 方式进行分页,如果想要实现自己的分页逻辑,可以实现Dialect
(com.github.pagehelper.Dialect
) 接口,然后配置该属性为实现类的全限定名称。
下面几个参数都是针对默认 dialect 情况下的参数。使用自定义 dialect 实现时,下面的参数没有任何作用。
-
helperDialect
:分页插件会自动检测当前的数据库链接,自动选择合适的分页方式。 你可以配置helperDialect
属性来指定分页插件使用哪种方言。配置时,可以使用下面的缩写值:oracle
,mysql
,mariadb
,sqlite
,hsqldb
,postgresql
,db2
,sqlserver
,informix
,h2
,sqlserver2012
,derby
**特别注意:**使用 SqlServer2012 数据库时,需要手动指定为sqlserver2012
,否则会使用 SqlServer2005 的方式进行分页。
你也可以实现AbstractHelperDialect
,然后配置该属性为实现类的全限定名称即可使用自定义的实现方法。 -
offsetAsPageNum
:默认值为false
,该参数对使用RowBounds
作为分页参数时有效。 当该参数设置为true
时,会将RowBounds
中的offset
参数当成pageNum
使用,可以用页码和页面大小两个参数进行分页。 -
rowBoundsWithCount
:默认值为false
,该参数对使用RowBounds
作为分页参数时有效。 当该参数设置为true
时,使用RowBounds
分页会进行 count 查询。 -
pageSizeZero
:默认值为false
,当该参数设置为true
时,如果pageSize=0
或者RowBounds.limit = 0
就会查询出全部的结果(相当于没有执行分页查询,但是返回结果仍然是Page
类型)。 -
reasonable
:分页合理化参数,默认值为false
。当该参数设置为true
时,pageNum<=0
时会查询第一页,pageNum>pages
(超过总数时),会查询最后一页。默认false
时,直接根据参数进行查询。 -
params
:为了支持startPage(Object params)
方法,增加了该参数来配置参数映射,用于从对象中根据属性名取值, 可以配置pageNum,pageSize,count,pageSizeZero,reasonable
,不配置映射的用默认值, 默认值为pageNum=pageNum;pageSize=pageSize;count=countSql;reasonable=reasonable;pageSizeZero=pageSizeZero
。 -
supportMethodsArguments
:支持通过 Mapper 接口参数来传递分页参数,默认值false
,分页插件会从查询方法的参数值中,自动根据上面params
配置的字段中取值,查找到合适的值时就会自动分页。 使用方法可以参考测试代码中的com.github.pagehelper.test.basic
包下的ArgumentsMapTest
和ArgumentsObjTest
。 -
autoRuntimeDialect
:默认值为false
。设置为true
时,允许在运行时根据多数据源自动识别对应方言的分页 (不支持自动选择sqlserver2012
,只能使用sqlserver
),用法和注意事项参考下面的场景五。 -
closeConn
:默认值为true
。当使用运行时动态数据源或没有设置helperDialect
属性自动获取数据库类型时,会自动获取一个数据库连接, 通过该属性来设置是否关闭获取的这个连接,默认true
关闭,设置为false
后,不会关闭获取的连接,这个参数的设置要根据自己选择的数据源来决定。
重要提示:
当
offsetAsPageNum=false
的时候,由于PageNum
问题,RowBounds
查询的时候reasonable
会强制为false
。使用PageHelper.startPage
方法不受影响。
示例如下:
<!-- 插件配置 -->
<plugins>
<!-- com.github.pagehelper为PageHelper类所在包名 -->
<plugin interceptor="com.github.pagehelper.PageInterceptor">
<!-- 4.0.0以后版本可以不设置该参数 -->
<!-- <property name="dialect" value="mysql"/> -->
<!-- 该参数默认为false -->
<!-- 设置为true时,会将RowBounds第一个参数offset当成pageNum页码使用 -->
<!-- 和startPage中的pageNum效果一样-->
<property name="offsetAsPageNum" value="true"/>
<!-- 该参数默认为false -->
<!-- 设置为true时,使用RowBounds分页会进行count查询 -->
<property name="rowBoundsWithCount" value="true"/>
<!-- 设置为true时,如果pageSize=0或者RowBounds.limit = 0就会查询出全部的结果 -->
<!-- (相当于没有执行分页查询,但是返回结果仍然是Page类型)-->
<property name="pageSizeZero" value="true"/>
<!-- 3.3.0版本可用 - 分页参数合理化,默认false禁用 -->
<!-- 启用合理化时,如果pageNum<1会查询第一页,如果pageNum>pages会查询最后一页 -->
<!-- 禁用合理化时,如果pageNum<1或pageNum>pages会返回空数据 -->
<property name="reasonable" value="true"/>
<!-- 3.5.0版本可用 - 为了支持startPage(Object params)方法 -->
<!-- 增加了一个`params`参数来配置参数映射,用于从Map或ServletRequest中取值 -->
<!-- 可以配置pageNum,pageSize,count,pageSizeZero,reasonable,orderBy,不配置映射的用默认值 -->
<!-- 不理解该含义的前提下,不要随便复制该配置 -->
<property name="params" value="pageNum=start;pageSize=limit;"/>
<!-- 支持通过Mapper接口参数来传递分页参数 -->
<property name="supportMethodsArguments" value="true"/>
<!-- always总是返回PageInfo类型,check检查返回类型是否为PageInfo,none返回Page -->
<property name="returnPageInfo" value="check"/>
</plugin>
</plugins>
5.4 如何在代码中使用:
阅读前请注意看重要提示:
PageHelper.startPage
方法重要提示只有紧跟在
PageHelper.startPage
方法后的第一个Mybatis的查询(Select)方法会被分页。
分页插件支持以下几种调用方式:
//第一种,RowBounds方式的调用
List<Country> list = sqlSession.selectList("x.y.selectIf", null, new RowBounds(0, 10));
//第二种,Mapper接口方式的调用,推荐这种使用方式。
PageHelper.startPage(1, 10);
List<Country> list = countryMapper.selectIf(1);
//第三种,Mapper接口方式的调用,推荐这种使用方式。
PageHelper.offsetPage(1, 10);
List<Country> list = countryMapper.selectIf(1);
//第四种,参数方法调用
//存在以下 Mapper 接口方法,你不需要在 xml 处理后两个参数
public interface CountryMapper {
List<Country> selectByPageNumSize(
@Param("user") User user,
@Param("pageNum") int pageNum,
@Param("pageSize") int pageSize);
}
//配置supportMethodsArguments=true
//在代码中直接调用:
List<Country> list = countryMapper.selectByPageNumSize(user, 1, 10);
//第五种,参数对象
//如果 pageNum 和 pageSize 存在于 User 对象中,只要参数有值,也会被分页
//有如下 User 对象
public class User {
//其他fields
//下面两个参数名和 params 配置的名字一致
private Integer pageNum;
private Integer pageSize;
}
//存在以下 Mapper 接口方法,你不需要在 xml 处理后两个参数
public interface CountryMapper {
List<Country> selectByPageNumSize(User user);
}
//当 user 中的 pageNum!= null && pageSize!= null 时,会自动分页
List<Country> list = countryMapper.selectByPageNumSize(user);
//第六种,ISelect 接口方式
//jdk6,7用法,创建接口
Page<Country> page = PageHelper.startPage(1, 10).doSelectPage(new ISelect() {
@Override
public void doSelect() {
countryMapper.selectGroupBy();
}
});
//jdk8 lambda用法
Page<Country> page = PageHelper.startPage(1, 10).doSelectPage(()-> countryMapper.selectGroupBy());
//也可以直接返回PageInfo,注意doSelectPageInfo方法和doSelectPage
pageInfo = PageHelper.startPage(1, 10).doSelectPageInfo(new ISelect() {
@Override
public void doSelect() {
countryMapper.selectGroupBy();
}
});
//对应的lambda用法
pageInfo = PageHelper.startPage(1, 10).doSelectPageInfo(() -> countryMapper.selectGroupBy());
//count查询,返回一个查询语句的count数
long total = PageHelper.count(new ISelect() {
@Override
public void doSelect() {
countryMapper.selectLike(country);
}
});
//lambda
total = PageHelper.count(()->countryMapper.selectLike(country));
详细介绍请参考官网:https://pagehelper.github.io/docs/howtouse/
1). PageHelper.startPage
静态方法调用
除了 PageHelper.startPage
方法外,还提供了类似用法的 PageHelper.offsetPage
方法。
在你需要进行分页的 MyBatis 查询方法前调用
PageHelper.startPage
静态方法即可,紧跟在这个方法后的第一个MyBatis 查询方法会被进行分页。
例一:
//获取第1页,10条内容,默认查询总数count
PageHelper.startPage(1, 10);
//紧跟着的第一个select方法会被分页
List<Country> list = countryMapper.selectIf(1);
assertEquals(2, list.get(0).getId());
assertEquals(10, list.size());
//分页时,实际返回的结果list类型是Page<E>,如果想取出分页信息,需要强制转换为Page<E>
assertEquals(182, ((Page) list).getTotal());
例二:
//request: url?pageNum=1&pageSize=10
//支持 ServletRequest,Map,POJO 对象,需要配合 params 参数
PageHelper.startPage(request);
//紧跟着的第一个select方法会被分页
List<Country> list = countryMapper.selectIf(1);
//后面的不会被分页,除非再次调用PageHelper.startPage
List<Country> list2 = countryMapper.selectIf(null);
//list1
assertEquals(2, list.get(0).getId());
assertEquals(10, list.size());
//分页时,实际返回的结果list类型是Page<E>,如果想取出分页信息,需要强制转换为Page<E>,
//或者使用PageInfo类(下面的例子有介绍)
assertEquals(182, ((Page) list).getTotal());
//list2
assertEquals(1, list2.get(0).getId());
assertEquals(182, list2.size());
例三,使用PageInfo
的用法:
//获取第1页,10条内容,默认查询总数count
PageHelper.startPage(1, 10);
List<Country> list = countryMapper.selectAll();
//用PageInfo对结果进行包装
PageInfo page = new PageInfo(list);
//测试PageInfo全部属性
//PageInfo包含了非常全面的分页属性
assertEquals(1, page.getPageNum());
assertEquals(10, page.getPageSize());
assertEquals(1, page.getStartRow());
assertEquals(10, page.getEndRow());
assertEquals(183, page.getTotal());
assertEquals(19, page.getPages());
assertEquals(1, page.getFirstPage());
assertEquals(8, page.getLastPage());
assertEquals(true, page.isFirstPage());
assertEquals(false, page.isLastPage());
assertEquals(false, page.isHasPreviousPage());
assertEquals(true, page.isHasNextPage());
2). 使用参数方式
想要使用参数方式,需要配置 supportMethodsArguments
参数为 true
,同时要配置 params
参数。 例如下面的配置:
<plugins>
<!-- com.github.pagehelper为PageHelper类所在包名 -->
<plugin interceptor="com.github.pagehelper.PageInterceptor">
<!-- 使用下面的方式配置参数,后面会有所有的参数介绍 -->
<property name="supportMethodsArguments" value="true"/>
<property name="params" value="pageNum=pageNumKey;pageSize=pageSizeKey;"/>
</plugin>
</plugins>
在 MyBatis 方法中:
List<Country> selectByPageNumSize(
@Param("user") User user,
@Param("pageNumKey") int pageNum,
@Param("pageSizeKey") int pageSize);
当调用这个方法时,由于同时发现了 pageNumKey
和 pageSizeKey
参数,这个方法就会被分页。params 提供的几个参数都可以这样使用。
除了上面这种方式外,如果 User 对象中包含这两个参数值,也可以有下面的方法:
List<Country> selectByPageNumSize(User user);
当从 User 中同时发现了 pageNumKey
和 pageSizeKey
参数,这个方法就会被分页。
注意:
pageNum
和pageSize
两个属性同时存在才会触发分页操作,在这个前提下,其他的分页参数才会生效。
5.5 超市订单管理系统中使用分页
mybatis-config.xml配置分页插件:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<!--指定数据库连接条件所在的文件位置-->
<properties resource="database.properties"/>
<settings>
<setting name="logImpl" value="LOG4J"/>
<!--<setting name="autoMappingBehivor" value="FULL"/>-->
<!--将表中含有下划线的列自动映射成java属性中的驼峰命名法-->
<setting name="mapUnderscoreToCamelCase" value="true"/>
</settings>
<!--为实体类指定别名-->
<typeAliases>
<package name="cn.smbms.pojo"/>
</typeAliases>
<plugins>
<!-- com.github.pagehelper为PageHelper类所在包名 -->
<plugin interceptor="com.github.pagehelper.PageInterceptor">
<!-- 使用下面的方式配置参数,详细的参数介绍请参照官网 -->
<!-- <property name="param1" value="value1"/>-->
</plugin>
</plugins>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="${driver}"/>
<property name="url" value="${url}"/>
<property name="username" value="${username}"/>
<property name="password" value="${password}"/>
</dataSource>
</environment>
</environments>
<mappers>
<!--<mapper resource="org/mybatis/example/BlogMapper.xml"/>
<mapper class=""/>
<mapper url="file://"-->
<package name="cn.smbms.dao"/>
</mappers>
</configuration>
DAO接口增加方法如下:
/**
* 根据用户名(模糊) 用户角色 获取用户列表信息(分页显示)
* @param userName : 用户名
* @param userRole : 角色id
* @return
*/
List<User> findUserPages(@Param("userName") String userName,
@Param("userRole") Integer userRole);
映射文件增加如下代码:
<!--使用sql元素封装用户查询的条件 以实现SQL语句的复用 -->
<sql id="userColumns">
<where>
<if test="userName != null and userName != ''">
and userName like concat ('%',#{userName},'%')
</if>
<if test="userRole != null">
and userRole = #{userRole}
</if>
</where>
</sql>
<!--根据用户名(模糊) 用户角色 获取用户列表信息(分页显示) -->
<select id="findUserPages" resultType="u">
select * from smbms_user
<!-- 引入外部的SQL -->
<include refid="userColumns"/>
order by creationDate DESC
</select>
测试代码如下:
@Test
public void testFindUserPages() throws IOException {
try (SqlSession session = MyBatisUtil.openSession()) {
UserMapper userMapper = session.getMapper(UserMapper.class);
String userName = "";
Integer userRole = null;
PageHelper.startPage(1,2);
List<User> userList =
userMapper.findUserPages(userName,userRole);
userList.forEach(log::info);
Page<User> page = (Page<User>) userList;
log.debug("总记录数:"+page.getTotal());
log.debug("总页数:"+page.getPages());
}
}
更多用法,请参考官网:https://pagehelper.github.io/docs/howtouse/
官方手册:
https://apidoc.gitee.com/free/Mybatis_PageHelper