1 前言

查询数据较多,或者有分页需求时,需要进行分页查询。

这里介绍 Mybatis 的这几种分页方式:

  1. 原生 SQL 的 Limit 分页
  2. Mybatis 自带的 RowBounds 分页
  3. 自定义拦截器插件进行分页
  4. 使用 PageHelper 插件分页(推荐)

2 原生 Limit 分页

原生 Limit 分页就是在编写 sql 语句时需要自己加上 limit 关键字,然后传入分页参数进行分页,例如 select * from t_user limit 0, 3

<select id="selectAllUserByLimit" resultType="User">
	select * from t_user limit #{start},#{size}
</select>
@Test
public void selectAllUserByLimit(){
	int currPage = 2;//当前页码
	int pageSize = 3;//当前显示页记录数量
	HashMap<String, Object> map = new HashMap<>();
	//计算起始位置,注意:currPage和start别搞错了,一个表示当前页码,一个是从第几行读取记录
	map.put("start",(currPage-1)*pageSize);
	//页面显示记录数
	map.put("size",pageSize);
	System.out.println("当前页码为:第"+currPage+"页,页面显示记录数量:"+pageSize+"个");
	List<User> userList = mapper.selectAllUserByLimit(map);
}

3 RowBounds 分页

Mybatis 内置了一个专门处理分页的类——RowBounds,我们使用它可以轻松完成分页。

RowBounds 分页有一点好处就是处理数据量少时还可以,但是数据量大时,就不行好用了,此时一般都会实现拦截器来完成分页。

RowBounds 源代码如下:

package org.apache.ibatis.session;
 
public class RowBounds {
    //默认值为0~~Java最大整数
    public static final int NO_ROW_OFFSET = 0;
    public static final int NO_ROW_LIMIT = Integer.MAX_VALUE;
    public static final RowBounds DEFAULT = new RowBounds();
    //偏移量,即从第几行开始读取
    private final int offset;
    //限制,即每页显示记录数量
    private final int limit;
 
    public RowBounds() {
        this.offset = NO_ROW_OFFSET;
        this.limit = NO_ROW_LIMIT;
    }
    public RowBounds(int offset, int limit) {
        this.offset = offset;
        this.limit = limit;
    }
    public int getOffset() {
        return offset;
    }
    public int getLimit() {
        return limit;
    }
}

Mapper 接口定义:

List<User> selectAllUserByRowBounds(RowBounds rowBounds);

SQL 映射:
使用 RowBounds 分页我们可以不写在映射 SQL 中写 limit 关键字,到时候自动回给我们拼接。

<select id="selectAllUserByRowBounds" resultMap="User">
    select * from t_user
</select>

测试代码:

@Test
public void selectAllUserByRowBounds(){
	int currPage=2;//当前页码
	int pageSize=3;//当前页显示记录数量
	//注意:currPage和start别搞错了,一个表示当前页码,一个是从第几行读取记录
	int start = (currPage-1)*pageSize;//计算从第几行读取记录
	RowBounds rowBounds = new RowBounds(start,pageSize);
	List<User> userList = mapper.selectAllUserByRowBounds(rowBounds);
}

4 自定义拦截器插件分页

自定义拦截器插件分页需要自己定义一个类实现 Interceptor 接口,这个接口是 Mybatis 提供的。任何分页插件想要对 Mybatis 进行分页就必须实现 Interceptor 接口,包括后面 PageHelper 分页插件。

创建 MyPageInterceptor 类

/**
 * @Intercepts 表示是一个拦截器
 * @Signature 拦截器的签名
 * type 拦截的类型 四大对象之一( Executor,ResultSetHandler,ParameterHandler,StatementHandler)
 * method 拦截的方法
 */
@Intercepts({@Signature(type=StatementHandler.class,method="prepare",args={Connection.class, Integer.class })})
public class MyPageInterceptor implements Interceptor {
 
    //当前页码
    private int currPage;
    //每页显示的条目数
    private int pageSize;
    //数据库类型
    private String dbType;
 
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        System.out.println("plugin is running...");
        //获取 StatementHandler,默认是 RoutingStatementHandler
        StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
        //获取 statementHandler 包装类
        MetaObject MetaObjectHandler = SystemMetaObject.forObject(statementHandler);
 
        //分离代理对象链
        while (MetaObjectHandler.hasGetter("h")) {
            Object obj = MetaObjectHandler.getValue("h");
            MetaObjectHandler = SystemMetaObject.forObject(obj);
        }
 
        while (MetaObjectHandler.hasGetter("target")) {
            Object obj = MetaObjectHandler.getValue("target");
            MetaObjectHandler = SystemMetaObject.forObject(obj);
        }
 
        //获取连接对象
        //Connection connection = (Connection) invocation.getArgs()[0];
        //object.getValue("delegate");  获取 StatementHandler 的实现类
 
        //获取查询接口映射的相关信息
        MappedStatement mappedStatement = (MappedStatement) MetaObjectHandler.getValue("delegate.mappedStatement");
        String mapId = mappedStatement.getId();
 
        //statementHandler.getBoundSql().getParameterObject();
 
        //拦截以.ByPage 结尾的请求,分页功能的统一实现
        if (mapId.matches(".+ByPage$")) {
            //获取进行数据库操作时管理参数的 handler
            ParameterHandler parameterHandler = (ParameterHandler) MetaObjectHandler.getValue("delegate.parameterHandler");
            //获取请求时的参数
            Map<String, Object> paraObject = (Map<String, Object>) parameterHandler.getParameterObject();
            //也可以这样获取
            //paraObject = (Map<String, Object>) statementHandler.getBoundSql().getParameterObject();
 
            //参数名称和在 service 中设置到 map 中的名称一致
            currPage = (int) paraObject.get("currPage");
            pageSize = (int) paraObject.get("pageSize");
 
            String sql = (String) MetaObjectHandler.getValue("delegate.boundSql.sql");
            //也可以通过 statementHandler 直接获取
            //sql = statementHandler.getBoundSql().getSql();
 
            //构建分页功能的 sql 语句
            String limitSql;
            sql = sql.trim();
            limitSql = sql + " limit " + (currPage - 1) * pageSize + "," + pageSize;
 
            //将构建完成的分页 sql 语句赋值个体'delegate.boundSql.sql',偷天换日
            MetaObjectHandler.setValue("delegate.boundSql.sql", limitSql);
        }
        //调用原对象的方法,进入责任链的下一级
        return invocation.proceed();
    }
 
    //获取代理对象
    @Override
    public Object plugin(Object o) {
        //生成 object 对象的动态代理对象
        return Plugin.wrap(o, this);
    }
 
    //设置代理对象的参数
    @Override
    public void setProperties(Properties properties) {
        //如果项目中分页的 pageSize 是统一的,也可以在这里统一配置和获取,这样就不用每次请求都传递 pageSize 参数了。参数是在配置拦截器时配置的。
        String limit1 = properties.getProperty("limit", "10");
        this.pageSize = Integer.valueOf(limit1);
        this.dbType = properties.getProperty("dbType", "mysql");
    }
}

全局配置文件增加 plugin 设置(注意位置)

<plugins>
    <plugin interceptor="com.thr.interceptor.MyPageInterceptor">
    </plugin>
</plugins>

Mapper 接口方法:
由于拦截器中设置了拦截以. ByPage 结尾的方法,所以方法一定要命名正确,

List<User> selectAllUserByPage(Map map);

SQL 映射:

<select id="selectAllUserByPage" resultMap="userMap">
    select * from t_user
</select>

测试方法

@Test
public void selectAllUserByLimit(){
	int currPage = 2;//当前页码
	int pageSize = 3;//当前显示页记录数量
	HashMap<String, Object> map = new HashMap<>();
	//计算起始位置,注意:currPage 和 start 别搞错了,一个表示当前页码,一个是从第几行读取记录
	map.put("start",(currPage-1)*pageSize);
	//页面显示记录数
	map.put("size",pageSize);
	System.out.println("当前页码为:第"+currPage+"页,页面显示记录数量:"+pageSize+"个");
	List<User> userList = mapper.selectAllUserByPage(map);
}

5 PageHelper 分页插件(推荐)

PageHelper 是一款非常优秀的分页插件,用的人非常多,详细的可以参考 PageHelper 的官方文档,讲的比较通俗易懂。链接:如何使用分页插件。 PageHelper 分页其实也是自定义拦截器方式的一种第三方实现,它内部帮助我们实现了 Interceptor 的功能。所以实际上我们在执行查询方法之前,PageHelper 分页插件同样是对我们的 sql 进行拦截,然后对分页参数进行拼接。

PageHelper 的简单使用:

导入依赖

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

全局配置文件增加 plugin 设置(注意位置)

<plugins>
    <!-- PageHelper5版本配置 -->
    <plugin interceptor="com.github.pagehelper.PageInterceptor"/>
</plugins>

接口方法

List<User> selectAllUserByPageHelper();

SQL 映射

<select id="selectAllUserByPageHelper" resultMap="userMap">
    select * from t_user
</select>

测试方法

@Test
public void selectAllUserByPageHelper(){
    int currPage = 2;//当前页码
    int pageSize = 3;//当前页记录数量
    //表示获取第 2 页,3 条内容,默认会查询总数 count
    PageHelper.startPage(currPage,pageSize);
    List<User> userList = mapper.selectAllUserByPageHelper();
    for (User user : userList) {
        System.out.println(user);
    }
}

5.1 开启分页功能

  • 在查询功能之前使用 PageHelper.startPage(int pageNum, int pageSize) 开启分页功能
    • pageNum:当前页的页码
    • pageSize:每页显示的条数
@Test
public void testPageHelper() throws IOException {
	InputStream is = Resources.getResourceAsStream("mybatis-config.xml");
	SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
	SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder.build(is);
	SqlSession sqlSession = sqlSessionFactory.openSession(true);
	EmpMapper mapper = sqlSession.getMapper(EmpMapper.class);
	//访问第一页,每页四条数据
	PageHelper.startPage(1,4);
	List<Emp> emps = mapper.selectByExample(null);
	emps.forEach(System.out::println);
}

5.2.1 方法一:直接输出

@Test
public void testPageHelper() throws IOException {
	InputStream is = Resources.getResourceAsStream("mybatis-config.xml");
	SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
	SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder.build(is);
	SqlSession sqlSession = sqlSessionFactory.openSession(true);
	EmpMapper mapper = sqlSession.getMapper(EmpMapper.class);
	//访问第一页,每页四条数据
	Page<Object> page = PageHelper.startPage(1, 4);
	List<Emp> emps = mapper.selectByExample(null);
	//在查询到 List 集合后,打印分页数据
	System.out.println(page);
}
  • 分页相关数据:
    Page{count=true, pageNum=1, pageSize=4, startRow=0, endRow=4, total=8, pages=2, reasonable=false, pageSizeZero=false}[Emp{eid=1, empName='admin', age=22, sex='男', email=' 456@qq.com ', did=3}, Emp{eid=2, empName='admin2', age=22, sex='男', email=' 456@qq.com ', did=3}, Emp{eid=3, empName='王五', age=12, sex='女', email=' 123@qq.com ', did=3}, Emp{eid=4, empName='赵六', age=32, sex='男', email=' 123@qq.com ', did=1}]
    

5.2 方法二:使用 PageInfo

在查询获取 list 集合之后,使用 PageInfo<T> pageInfo = new PageInfo<>(List<T> list, intnavigatePages) 获取分页相关数据

  • list:分页之后的数据
  • navigatePages:导航分页的页码数
@Test
public void testPageHelper() throws IOException {
	InputStream is = Resources.getResourceAsStream("mybatis-config.xml");
	SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
	SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder.build(is);
	SqlSession sqlSession = sqlSessionFactory.openSession(true);
	EmpMapper mapper = sqlSession.getMapper(EmpMapper.class);
	PageHelper.startPage(1, 4);
	List<Emp> emps = mapper.selectByExample(null);
	PageInfo<Emp> page = new PageInfo<>(emps,5);
	System.out.println(page);
}
  • 分页相关数据:
PageInfo{
pageNum=1, pageSize=4, size=4, startRow=1, endRow=4, total=8, pages=2,
list=Page{count=true, pageNum=1, pageSize=4, startRow=0, endRow=4, total=8, pages=2, reasonable=false, pageSizeZero=false}[Emp{eid=1, empName='admin', age=22, sex='男', email=' 456@qq.com ', did=3}, Emp{eid=2, empName='admin2', age=22, sex='男', email=' 456@qq.com ', did=3}, Emp{eid=3, empName='王五', age=12, sex='女', email=' 123@qq.com ', did=3}, Emp{eid=4, empName='赵六', age=32, sex='男', email=' 123@qq.com ', did=1}],
prePage=0, nextPage=2, isFirstPage=true, isLastPage=false, hasPreviousPage=false, hasNextPage=true, navigatePages=5, navigateFirstPage=1, navigateLastPage=2, navigatepageNums=[1, 2]}
  • 其中 list 中的数据等同于方法一中直接输出的 page 数据

5.2.1 常用数据

  • pageNum:当前页的页码
  • pageSize:每页显示的条数
  • size:当前页显示的真实条数
  • total:总记录数
  • pages:总页数
  • prePage:上一页的页码
  • nextPage:下一页的页码
  • isFirstPage/isLastPage:是否为第一页/最后一页
  • hasPreviousPage/hasNextPage:是否存在上一页/下一页
  • navigatePages:导航分页的页码数
  • navigatepageNums:导航分页的页码,[1,2,3,4,5]