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() 方法,并加入查询条件 , 代码如下:

  1. /**
  2. * 根据用户名(模糊查询) 和 用户角色 查询用户列表
  3. * @param userName
  4. * @param userRole
  5. * @return
  6. */
  7. List<User> findUserList(@Param("userName") String userName,
  8. @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中最直接的区别就是:相当于对数据 加上 单引号,相当于直接显示数据(只讨论字符串类型的)。**#**``**$**

  1. #对传入的参数视为字符串,也就是它会预编译,select * from user where name = #{name},比如传一个aaa,那么传过来就是 select * from user where name = 'aaa'
  2. $将不会将传入的值进行预编译,select * from user where name=${name},比如传一个aaa,那么传过来就是 select * from user where name = aaa
  3. 的优势就在于它能很大程度的防止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注入。
  4. 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.PageInterceptorcom.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 实现时,下面的参数没有任何作用。

  1. helperDialect:分页插件会自动检测当前的数据库链接,自动选择合适的分页方式。 你可以配置helperDialect属性来指定分页插件使用哪种方言。配置时,可以使用下面的缩写值:
    oracle,mysql,mariadb,sqlite,hsqldb,postgresql,db2,sqlserver,informix,h2,sqlserver2012,derby
    特别注意:使用 SqlServer2012 数据库时,需要手动指定为 sqlserver2012,否则会使用 SqlServer2005 的方式进行分页。
    你也可以实现 AbstractHelperDialect,然后配置该属性为实现类的全限定名称即可使用自定义的实现方法。
  2. offsetAsPageNum:默认值为 false,该参数对使用 RowBounds 作为分页参数时有效。 当该参数设置为 true 时,会将 RowBounds 中的 offset 参数当成 pageNum 使用,可以用页码和页面大小两个参数进行分页。
  3. rowBoundsWithCount:默认值为false,该参数对使用 RowBounds 作为分页参数时有效。 当该参数设置为true时,使用 RowBounds 分页会进行 count 查询。
  4. pageSizeZero:默认值为 false,当该参数设置为 true 时,如果 pageSize=0 或者 RowBounds.limit = 0 就会查询出全部的结果(相当于没有执行分页查询,但是返回结果仍然是 Page 类型)。
  5. reasonable:分页合理化参数,默认值为false。当该参数设置为 true 时,pageNum<=0 时会查询第一页, pageNum>pages(超过总数时),会查询最后一页。默认false 时,直接根据参数进行查询。
  6. params:为了支持startPage(Object params)方法,增加了该参数来配置参数映射,用于从对象中根据属性名取值, 可以配置 pageNum,pageSize,count,pageSizeZero,reasonable,不配置映射的用默认值, 默认值为pageNum=pageNum;pageSize=pageSize;count=countSql;reasonable=reasonable;pageSizeZero=pageSizeZero
  7. supportMethodsArguments:支持通过 Mapper 接口参数来传递分页参数,默认值false,分页插件会从查询方法的参数值中,自动根据上面 params 配置的字段中取值,查找到合适的值时就会自动分页。 使用方法可以参考测试代码中的 com.github.pagehelper.test.basic 包下的 ArgumentsMapTestArgumentsObjTest
  8. autoRuntimeDialect:默认值为 false。设置为 true 时,允许在运行时根据多数据源自动识别对应方言的分页 (不支持自动选择sqlserver2012,只能使用sqlserver),用法和注意事项参考下面的场景五
  9. 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);

当调用这个方法时,由于同时发现了 pageNumKeypageSizeKey 参数,这个方法就会被分页。params 提供的几个参数都可以这样使用。

除了上面这种方式外,如果 User 对象中包含这两个参数值,也可以有下面的方法:

List<Country> selectByPageNumSize(User user);

当从 User 中同时发现了 pageNumKeypageSizeKey 参数,这个方法就会被分页。

注意:pageNumpageSize 两个属性同时存在才会触发分页操作,在这个前提下,其他的分页参数才会生效。

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

6. 实体类分层

实体类分层:po、dto、vo

命名规范(驼峰命名法)

含义描述 命名规范示例
post body请求参数封装 XxRequest
显示层对象命名(比如,数据库表中存了10个字段,显示给前端的只需要3个字段) XxVo
数据传输对象命名(两张表的某些数据或者从因业务需要用到的额外的字段) XxDto
es实体命名 XxDo
db实体命名 与表名相同
mongo实体命名 XxDoc
db组合关联实体命名 Xx
service接口命名 XxService
service接口的实现 XxServiceImpl
manager,service引入多个manager进行负责的组合业务处理 XxManager
dao层命名 XxMapper
封装持久化组合服务(一个实体需要从db、es、redis多种存储获取) XxRepository