MyBatis的PageHelper还是挺好用

913 阅读10分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第28天,点击查看活动详情

现在的公司没有使用MyBatis-Plus的插件开发,使用的是原生的MyBatis,在做分页方面不太好,里面使用了PageHelper的拦截器处理分页

参考的文档:blog.csdn.net/xiaojin21ce…

在实际的项目开发中,常常需要使用到分页,分页方式分为两种:前端分页和后端分页

  • 前端分页(假分页)

    一次ajax请求数据的所有记录,然后在前端缓存并且计算count和分页逻辑,一般前端组件(例如dataTable)会提供分页动作。 特点是:简单很适合小规模的web平台,当数据量大的时候会产生性能问题,在查询和网络传输的时间会很长。

  • 后端分页(真分页)

    在ajax请求中指定页码pageNum和每页的大小pageSize,后端查询出当页的数据返回,前端只负责渲染。 特点是:复杂一些;性能瓶颈在MySQL的查询性能,这个当然可以调优解决。

    一般来说,开发使用的是这种方式。

1.不使用分页插件的分页操作

在没有使用分页插件的时候需要先写一个查询countselect语句,然后再写一个真正分页查询的语句,MySQL中有对分页的支持,是通过limit子句

  • limit关键字的用法是:LIMIT [offset,] rows
    • offset是相对于首行的偏移量(首行是0)
    • rows是返回条数(每页显示条数)

例如:

  • 每页5条记录,取第一页,返回的是前5条记录
select * from tableA limit 0,5;
  • 每页5条记录,取第二页,返回的是第6条记录,到第10条记录
select * from tableA limit 5,5;

不过当偏移量逐渐增大的时候,查询速度可能就会变慢,性能会有所下降(可以优化SQL语句提高效率)。

2.分页插件PageHelper 4.X配置

PageHelper是一款好用的开源免费的Mybatis第三方物理分页插件 Github地址:github.com/pagehelper/… 官方地址:pagehelper.github.io/

示例:在SpringBoot中使用PageHelper

<dependency>
    <groupId>com.github.pagehelper</groupId>
    <artifactId>pagehelper</artifactId>
    <version>4.2.1</version>
</dependency>

在Mybatis的配置文件中配置PageHelper插件,假如不配置在后面使用PageInfo类时就会出现问题,输出结果的PageInfo属性值基本上都是错的,配置如下

<plugins>
    <!-- com.github.pagehelper为PageHelper类所在包名 -->
    <plugin interceptor="com.github.pagehelper.PageHelper">
        <property name="dialect" value="mysql"/>
        <!-- 该参数默认为false -->
        <!-- 设置为true时,会将RowBounds第一个参数offset当成pageNum页码使用 -->
        <!-- 和startPage中的pageNum效果一样-->
        <property name="offsetAsPageNum" value="false"/>
        <!-- 该参数默认为false -->
        <!-- 设置为true时,使用RowBounds分页会进行count查询 -->
        <property name="rowBoundsWithCount" value="false"/>
        <!-- 设置为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,不配置映射的用默认值 -->
        <!--<property name="params" value="pageNum=start;pageSize=limit;pageSizeZero=zero;reasonable=heli;count=contsql"/>-->
    </plugin>
</plugins>

上面是PageHelper官方给的配置和注释,虽然写的很多,不过确实描述的很明白。

  • dialect:标识是哪一种数据库,设计上必须。
  • offsetAsPageNum:将RowBounds第一个参数offset当成pageNum页码使用
  • rowBoundsWithCount:设置为true时,使用RowBounds分页会进行count查询
  • reasonablevalue=true时,pageNum小于1会查询第一页,如果pageNum大于pageSize会查询最后一页

注:上面的配置只针对于pagehelper4.x版本的,如果你用的是pagehelper5.x版本就要这样配置

3.官方推荐PageHelper 5.X配置

(1)配置方式1:在 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>

示例代码

<!-- 配置分页插件 -->
<plugins>
    <plugin interceptor="com.github.pagehelper.PageInterceptor">
        <!-- 设置数据库类型 Oracle,Mysql,MariaDB,SQLite,Hsqldb,PostgreSQL六种数据库-->
        <property name="helperDialect" value="mysql"/>
    </plugin>
</plugins>

(2)配置方式2:在 Spring 配置文件中配置拦截器插件

<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
  <!-- 注意其他配置 -->
  <property name="plugins">
    <array>
      <bean class="com.github.pagehelper.PageInterceptor">
        <property name="properties">
          <!--使用下面的方式配置参数,一行配置一个 -->
          <value>
            params=value1
          </value>
        </property>
      </bean>
    </array>
  </property>
</bean>

4.MyBatis项目中的应用

在配置完mybatis后,我简单的说下PageHelper的业务用法,就以分页查询用户列表为例,查询所有用户的mapper接口(没有进行分页SQL)

List<UserVo> listUser();// SELECT * FROM sys_user

重点来了,然后在service中,先开启分页,然后把查询结果集放入PageInfo

public PageInfo listUserByPage(int pageNum, int pageSize) {
    PageHelper.startPage(pageNum, pageSize);// 1. 开启分页
    List<UserVo> userVoList=userMapper.listUser();
    PageInfo pageInfo=new PageInfo(userVoList);// 2.查询结果集放入`PageInfo`
    return pageInfo;
}

代码说明:

  • PageHelper.startPage(pageNum, pageSize);这句非常重要,这段代码表示分页的开始,意思是从第pageNum页开始,每页显示pageSize条记录。
  • PageInfo这个类是插件里的类,这个类里面的属性会在输出结果中显示,使用PageInfo这个类,你需要将查询出来的list放进去。

PageHelper输出的数据结构,然后在controller层调用该方法设置对应的pageNumpageSize就可以了,我设置pageNum为1, pageSize为5,看个输出JSON结果

{
    "msg": "获取第1页用户信息成功",
    "code": 200,
    "data": {
        "pageNum": 1, 					// 当前页
        "pageSize": 5,					// 每页的数量
        "size": 5,						// 当前页的数量
        "orderBy": null,				// 排序
        "startRow": 1,					// 当前页面第一个元素在数据库中的行号
        "endRow": 5,					// 当前页面最后一个元素在数据库中的行号
        "total": 11,					// 总记录数(在这里也就是查询到的用户总数)
        "pages": 3,						// 总页数 (这个页数也很好算,每页5条,总共有11条,需要3页才可以显示完)
        "list": [						// 结果集
            {
                "userId": "a24d0c3b-2786-11e8-9835-e4f89cdc0d1f",
                "username": "2015081040"
            },
            {
                "userId": "b0bc9e45-2786-11e8-9835-e4f89cdc0d1f",
                "username": "2015081041"
            },
            {
                "userId": "b44fd6ac-2786-11e8-9835-e4f89cdc0d1f",
                "username": "2015081042"
            },
            {
                "userId": "b7ac58f7-2786-11e8-9835-e4f89cdc0d1f",
                "username": "2015081043"
            },
            {
                "userId": "bbdeb5d8-2786-11e8-9835-e4f89cdc0d1f",
                "username": "2015081044"
            }
        ],
        "prePage": 0,						// 前一页
        "nextPage": 2,						// 下一页
        "isFirstPage": true,				// 是否为第一页
        "isLastPage": false,				// 是否为最后一页
        "hasPreviousPage": false,			// 是否有前一页
        "hasNextPage": true,				// 是否有下一页
        "navigatePages": 8,					// 导航页码数
        "navigatepageNums": [				// 所有导航页号
            1,
            2,
            3
        ],
        "navigateFirstPage": 1,				// 导航第一页
        "navigateLastPage": 3,				// 导航最后一页
        "firstPage": 1,						// 第一页
        "lastPage": 3						// 最后一页
    },
    "success": true,
    "error": null
}

5. SpringBoot中的配置

  • 引入依赖
<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>2.0.0</version>
</dependency>
<dependency>
    <groupId>tk.mybatis</groupId>
    <artifactId>mapper-spring-boot-starter</artifactId>
    <version>2.1.5</version>
</dependency>
<dependency>
    <groupId>com.github.pagehelper</groupId>
    <artifactId>pagehelper-spring-boot-starter</artifactId>
    <version>1.2.10</version>
</dependency>
  • 简单使用
//[pageNum, pageSize]  页码  每页显示数量
PageHelper.startPage(pageNum,pageSize);
PageInfo<UserInfo> pageInfo = new PageInfo<>(UserInfoService.selectUserList());
  • 进阶说明

官方文档中,提供了很多的参数供我们配置:helperDialectoffsetAsPageNumrowBoundsWithCountpageSizeZeroreasonableparamssupportMethodsArgumentsautoRuntimeDialectcloseConn等等。

spring boot项目里面,到application.yml中进行配置

pagehelper:
  # dialect: (1)
  # 分页插件会自动检测当前的数据库链接,自动选择合适的分页方式(可以不设置)
  helper-dialect: mysql 
  # 上面数据库设置后,下面的设置为true不会改变上面的结果(默认为true)
  auto-dialect: true 
  page-size-zero: false # (2)
  reasonable: true # (3)
  # 默认值为 false,该参数对使用 RowBounds 作为分页参数时有效。(一般用不着)
  offset-as-page-num: false 
  # 默认值为 false,RowBounds是否进行count查询(一般用不着)
  row-bounds-with-count: false 
  #params: (4)
  #support-methods-arguments: 和params配合使用,具体可以看下面的讲解
  # 默认值为 false。设置为 true 时,允许在运行时根据多数据源自动识别对应方言的分页
  auto-runtime-dialect: false # (5)
  # 与auto-runtime-dialect配合使用
  close-conn: true 
  # 用于控制默认不带 count 查询的方法中,是否执行 count 查询,这里设置为true后,total会为-1
  default-count: false 
  #dialect-alias: (6)

(1) 默认情况下会使用 PageHelper 方式进行分页,如果想要实现自己的分页逻辑,可以实现 Dialect(com.github.pagehelper.Dialect) 接口,然后配置该属性为实现类的全限定名称。(这里不推荐这样玩,毕竟你用了别人的插件,干嘛还要多此一举呢?)

(2) 默认值为 false,当该参数设置为 true 时,如果 pageSize=0 或者 RowBounds.limit = 0 就会查询出全部的结果(相当于没有执行分页查询,但是返回结果仍然是 Page 类型)。

(3) 合法性,即纠错机制,配置reasonabletrue,这时如果 pageNum <= 0 会查询第一页,如果 pageNum > pages 会查询最后一页。

(4) 为了支持startPage(Object params)方法,增加了该参数来配置参数映射,用于从对象中根据属性名取值, 可以配置pageNum,pageSize,count,pageSizeZero,reasonable,不配置映射的用默认值, 默认值为 pageNum=pageNum; pageSize=pageSize; count=countSql; reasonable=reasonable; pageSizeZero=pageSizeZerosupport-methods-arguments支持通过 Mapper 接口参数来传递分页参数,默认值false,分页插件会从查询方法的参数值中,自动根据上面 params 配置的字段中取值,查找到合适的值时就会自动分页。

(5) 默认值为false。设置为 true 时,允许在运行时根据多数据源自动识别对应方言的分页,closeConn:默认值为 true。当使用运行时动态数据源或没有设置 helperDialect 属性自动获取数据库类型时,会自动获取一个数据库连接, 通过该属性来设置是否关闭获取的这个连接,默认true关闭,设置为 false 后,不会关闭获取的连接,这个参数的设置要根据自己选择的数据源来决定。

(6) dialect-alias 参数,允许配置自定义实现的别名,可以用于根据JDBCURL自动获取对应实现,允许通过此种方式覆盖已有的实现,配置示例如(多个配置用分号;隔开):

pagehelper.dialect-alias=oracle=com.github.pagehelper.dialect.helper.OracleDialect

可以使用Lambda表达式

//1.offsetPage
PageHelper.offsetPage(1, 10);
return PageInfo.of(userService.findAll());
//2.Lambda
return PageHelper.startPage(1, 10).doSelectPageInfo(() -> userService.findAll());
//部分属性
System.out.println("总数量"+pageInfo.getTotal());
System.out.println("当前页查询记录"+pageInfo.getList().size());
System.out.println("当前页码"+pageInfo.getPageNum());
System.out.println("每页显示数量"+pageInfo.getPageSize());
System.out.println("总页"+pageInfo.getPages());

controler中方式

public PageInfo<T> getPageById(@RequestParam("sectionId") String sectionId,@RequestParam("pageNum" Integer pageNum,@RequestParam("pageSize" Integer pageSize)) {
	PageInfo<T> info= PageHelper
        	.orderBy(xxx ASC/DESC)
        	.startPage(pageNum, pageSize).doSelectPage(() -> {
        		this.xxxService.getPagedBySectionId(sectionId);
    		});
   return  info;
}
//第一种、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));

6. PageHelper 安全调用

  • 使用 RowBoundsPageRowBounds参数方式是极其安全的

  • 使用参数方式是极其安全的

  • 使用 ISelect 接口调用是极其安全的

    ISelect接口方式除了可以保证安全外,还特别实现了将查询转换为单纯的 count 查询方式,这个方法可以将任意的查询方法,变成一个 select count(*) 的查询方法。

  • 什么时候会导致不安全的分页? PageHelper 方法使用了静态的 ThreadLocal 参数,分页参数和线程是绑定的。 **只要你可以保证在 PageHelper 方法调用后紧跟 MyBatis 查询方法,这就是安全的。**因为 PageHelper 在 finally 代码段中自动清除了 ThreadLocal存储的对象。如果代码在进入 Executor 前发生异常,就会导致线程不可用,这属于人为的 Bug(例如接口方法和 XML 中的不匹配,导致找不到 MappedStatement 时), 这种情况由于线程不可用,也不会导致 ThreadLocal 参数被错误的使用。\

  • 但是如果你写出下面这样的代码,就是不安全的用法:

PageHelper.startPage(1, 10);
List<Country> list;
if(param1 != null){
    list = countryMapper.selectIf(param1);
} else {
    list = new ArrayList<Country>();
}

这种情况下由于 param1 存在null的情况,就会导致 PageHelper 生产了一个分页参数,但是没有被消费,这个参数就会一直保留在这个线程上。当这个线程再次被使用时,就可能导致不该分页的方法去消费这个分页参数,这就产生了莫名其妙的分页。

上面这个代码,应该写成下面这个样子:这种写法就能保证安全。

List<Country> list;
if(param1 != null){
    PageHelper.startPage(1, 10);
    list = countryMapper.selectIf(param1);
} else {
    list = new ArrayList<Country>();
}

附录

(1)如果4.x的版本用了5.x的版本报错信息如下,springboot在启动项目的时候就会报错,报错信息有很多,主要是因为

Caused by: org.apache.ibatis.builder.BuilderException: 
Error resolving class. Cause: org.apache.ibatis.type.TypeException: 
Could not resolve type alias 'com.github.pagehelper.PageInterceptor'.
Caused by: org.apache.ibatis.type.TypeException: 
Could not resolve type alias 'com.github.pagehelper.PageInterceptor'.
Caused by: java.lang.ClassNotFoundException: 
Cannot find class: com.github.pagehelper.PageInterceptor

总的来说就是缺少了com.github.pagehelper.PageInterceptor,这个是新版拦截器,5.x版本才开始使用,所以在4.x版本这样配置是不行的。

(2)5.x版本的配置在pagehelper4.x上能生效吗?答案是不行

Caused by: org.apache.ibatis.builder.BuilderException: 
Error parsing SQL Mapper Configuration. Cause: 
java.lang.ClassCastException: com.github.pagehelper.PageHelper 
cannot be cast to org.apache.ibatis.plugin.Interceptor
Caused by: java.lang.ClassCastException: 
com.github.pagehelper.PageHelper 
cannot be cast to org.apache.ibatis.plugin.Interceptor

新版的拦截器PageInterceptor不能和旧版拦截器相互转换,所以还是不行的。

总结:pagehelper4.x就该用4.x的配置,pagehelper5.x就用5.x的配置(官方推荐)