浅谈 Mybatis 分页

一、 自行车

有时候我们可能会用到,自己业务代码查出来一个List,然后用sublist进行手动分页。

手动分页就了解清楚List的subList方法使用就了,但是这是很可取的,如果返回值太大,内存容易被无情撑爆。

import dao.TestMapper;
import entity.TestEntity;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import java.io.InputStream;
import java.util.*;

/**
 * @author 发现更多精彩  关注公众号:木子的昼夜编程
 * 一个生活在互联网底层,做着增删改查的码农,不谙世事的造作
 */
public class TestMain03 {
    public static void main(String[] args) throws Exception {
        String resource = "mybatis-config.xml";
        InputStream inputStream = Resources.getResourceAsStream(resource);
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
        try (SqlSession session = sqlSessionFactory.openSession()) {
            // 通过sesson获取Mapper 这个Mapper会编程Mybatis的代理Mapper
            TestMapper mapper = session.getMapper(TestMapper.class);
            // 业务传递进来的分页参数
            Integer pageNum = 1; // 当前页(一般与前端交互 就迁就一下他们 页数从1开始)
            Integer pageSize = 10; // 每页大小
            // 查询全部数据  有时候可能调用第三方或者其他接口 只有全部数据
            // 又想分页展示 但是人家不给你提供分页接口 这时候可能会用到这种
            List<TestEntity> list = mapper.select();
            List<TestEntity> res = new ArrayList<>();
            if (list != null) {
                // Params:
                //fromIndex – low endpoint (包含) of the subList
                //toIndex – high endpoint (不包含) of the subList
                // 如果符合: (fromIndex < 0 || toIndex > size || fromIndex > toIndex) 抛异常 IndexOutOfBoundsException –
                int size = list.size();
                // 防止 fromIndex < 0
                if (pageNum <= 0) {
                    throw  new Exception("pageNum必须是大于等于1的整数");
                }
                if (pageSize <= 0) {
                    throw  new Exception("pageSize必须是大于等于1的整数");
                }
                int totalPage = (int) Math.ceil(Double.valueOf(size)/Double.valueOf(pageSize));
                // 页数超了最大页数 (前端不可信)
                if (pageNum > pageSize) {
                    res = new ArrayList<>();
                } else {
                    Integer fromIndex = (pageNum -1 ) * pageSize;
                    Integer toIndex =pageNum * pageSize;
                    // 防止 toIndex > size
                    toIndex = toIndex > list.size() ? list.size() : toIndex;
                    res = list.subList(fromIndex, toIndex);
                }
            }
            for (int i = 0; i < res.size(); i++) {
                System.out.println(res.get(i));
            }
        }
    }
}

二、摩托车

Mybatis自带了RowBounds

看过源码就知道其实他是逻辑分页,也会把所有数据都查出来,然后内部进行了一个操作,先过滤掉offset大小的条数,然后继续读取limit大小的条数,最后返回。

// 如果有多个参数 在最后加上这个参数就可以 
@Select("select * from test")
List<TestEntity> selectRowBounds(RowBounds rd);
import dao.TestMapper;
import entity.TestEntity;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.RowBounds;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;

import java.io.InputStream;
import java.util.List;

/**
 * @author 发现更多精彩  关注公众号:木子的昼夜编程
 * 一个生活在互联网底层,做着增删改查的码农,不谙世事的造作
 */
public class TestMain04 {
    public static void main(String[] args) throws Exception {
        String resource = "mybatis-config.xml";
        InputStream inputStream = Resources.getResourceAsStream(resource);
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
        try (SqlSession session = sqlSessionFactory.openSession()) {
            // 通过sesson获取Mapper 这个Mapper会编程Mybatis的代理Mapper
            TestMapper mapper = session.getMapper(TestMapper.class);
            // offset偏移量从0开始  limit一共查几条
            Integer pageNum =1; // 当前页(一般与前端交互 就迁就一下他们 页数从1开始)
            Integer pageSize = 10; // 每页大小
            // 分页参数错误判断就先不判断了 根据业务自己判断
            //
            RowBounds rd = new RowBounds((pageNum-1)*pageSize,pageSize);
            List<TestEntity> res = mapper.selectRowBounds(rd);
            for (int i = 0; i < res.size(); i++) {
                System.out.println(res.get(i));
            }
        }
    }
}

看他的源码实现是简单了解是这样的:

// 跨过offset条记录 
private void skipRows(ResultSet rs, RowBounds rowBounds) throws SQLException {
    if (rs.getType() != ResultSet.TYPE_FORWARD_ONLY) {
        if (rowBounds.getOffset() != RowBounds.NO_ROW_OFFSET) {
            rs.absolute(rowBounds.getOffset());
        }
    } else {
        // 也就是调动rs.next(); 写过原生sql的都知道这是
        for (int i = 0; i < rowBounds.getOffset(); i++) {
            rs.next();
        }
    }
}

// 
private void handleRowValuesForSimpleResultMap(ResultSetWrapper rsw, ResultMap resultMap, ResultHandler<?> resultHandler, RowBounds rowBounds, ResultMapping parentMapping)
    throws SQLException {
    DefaultResultContext<Object> resultContext = new DefaultResultContext<Object>();
    // 跳过offset条记录
    skipRows(rsw.getResultSet(), rowBounds);
    // 从剩余结果中读取limit条记录 
    // 每读取一条记录 就给resultContext++ 
    while (shouldProcessMoreRows(resultContext, rowBounds) 
           && rsw.getResultSet().next()) {
        ResultMap discriminatedResultMap = resolveDiscriminatedResultMap(rsw.getResultSet(), resultMap, null);
        Object rowValue = getRowValue(rsw, discriminatedResultMap);
        storeObject(resultHandler, resultContext, rowValue, parentMapping, rsw.getResultSet());
    }
}
// 这里就是比较读取的数 是否小于limit 如果小于就接着循环读取
private boolean shouldProcessMoreRows(ResultContext<?> context, RowBounds rowBounds) throws SQLException {
    return !context.isStopped() && context.getResultCount() < rowBounds.getLimit();
}

说白了也是通过代码逻辑处理

三、手动挡小轿车

物理分页,也就是不会查到客户端,在服务端已经分好了,

@Select("select * from test limit #{offset} , #{pageSize}")
List<TestEntity> selectLimit(@Param("offset") int offset, @Param("pageSize") Integer pageSize);
import dao.TestMapper;
import entity.TestEntity;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;

import java.io.InputStream;
import java.util.List;

/**
 * @author 发现更多精彩  关注公众号:木子的昼夜编程
 * 一个生活在互联网底层,做着增删改查的码农,不谙世事的造作
 */
public class TestMain05 {
    public static void main(String[] args) throws Exception {
        String resource = "mybatis-config.xml";
        InputStream inputStream = Resources.getResourceAsStream(resource);
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
        try (SqlSession session = sqlSessionFactory.openSession()) {
            // 通过sesson获取Mapper 这个Mapper会编程Mybatis的代理Mapper
            TestMapper mapper = session.getMapper(TestMapper.class);
            // offset偏移量从0开始  limit一共查几条
            Integer pageNum =1; // 当前页(一般与前端交互 就迁就一下他们 页数从1开始)
            Integer pageSize = 10; // 每页大小
            // 分页参数错误判断就先不判断了 根据业务自己判断
            List<TestEntity> res = mapper.selectLimit((pageNum-1)*pageSize, pageSize);
            for (int i = 0; i < res.size(); i++) {
                System.out.println(res.get(i));
            }
        }
    }
}

为什么说他是小轿车呢,因为这个对系统内存消耗最小,而且对网络消耗也是最小的。他会在服务端就把不需要的行过滤掉。

四、自动挡小轿车

有人说了,自己拼limit 多没劲,能不能稍微智能点儿,当然可以了,只要你想要的,我就能给。

自定义参数基类PageQo

package entity;

/**
 * @author 发现更多精彩  关注公众号:木子的昼夜编程
 * 一个生活在互联网底层,做着增删改查的码农,不谙世事的造作
 * @create 2021-09-21 17:37
 */
public class PageQo {
    // 当前页数 1开始
    Integer pageNum;
    // 每页大小
    Integer pageSize;

    public PageQo() {
    }
    public PageQo(Integer pageNum, Integer pageSize) {
        this.pageNum = pageNum;
        this.pageSize = pageSize;
    }

    public Integer getPageNum() {
        return pageNum;
    }

    public void setPageNum(Integer pageNum) {
        this.pageNum = pageNum;
    }

    public Integer getPageSize() {
        return pageSize;
    }

    public void setPageSize(Integer pageSize) {
        this.pageSize = pageSize;
    }
}

自定义拦截器MyPageInterceptor

package interceptor;

import entity.PageInfo;
import entity.PageQo;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.SystemMetaObject;
import org.apache.ibatis.session.Configuration;

import java.sql.Connection;
import java.util.ArrayList;
import java.util.Properties;

/**
 * @author 发现更多精彩  关注公众号:木子的昼夜编程
 * 一个生活在互联网底层,做着增删改查的码农,不谙世事的造作
 */
@Intercepts({
        @Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})
})
public class MyPageInterceptor implements Interceptor {

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        System.out.println("执行拦截器代码");
        // 获取参数 这里指定如果参数是PageQo的子类,才会进行分页 其他不进行分页
        StatementHandler  statementHandler = (StatementHandler) invocation.getTarget();
        //获取StatementHandler的包装类
        MetaObject metaObject = SystemMetaObject.forObject(statementHandler);
        BoundSql boundSql = statementHandler.getBoundSql();
        Object param = boundSql.getParameterObject();
        // 参数是PageQo的子类 才会进行分页操作
        if(param instanceof PageQo) {
            // 强转 主要是为了分页参数
            PageQo pageQo = (PageQo) param;
            //获取原始SQL语句
            String originalSql = (String) metaObject.getValue("delegate.boundSql.sql");
            System.out.println("原sql:"+originalSql);
            String sql = originalSql.trim() + " limit " + (pageQo.getPageNum()-1)* pageQo.getPageSize() + ", " + pageQo.getPageSize();
            System.out.println("分页sql:"+sql);
            metaObject.setValue("delegate.boundSql.sql", sql);
        }
        // 默认不进行分页逻辑
        return invocation.proceed();
    }

    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target,this);
    }

    @Override
    public void setProperties(Properties properties) {

    }
}

配置文件配置拦截器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>
    <settings>
        <setting name="logImpl" value="LOG4J"/>
    </settings>
    <!--扫描包路径-->
    <typeAliases>
        <package name="entity"/>
    </typeAliases>
    <!--自定义拦截器-->
    <plugins>
        <plugin interceptor="interceptor.MyPageInterceptor"></plugin>
    </plugins>

    <environments default="development">
        <environment id="development">
            <transactionManager type="JDBC"/>
            <dataSource type="POOLED">
                <property name="driver" value="com.mysql.jdbc.Driver"/>
                <property name="url" value="jdbc:mysql://127.0.0.1:3306/test?useUnicode=true&amp;characterEncoding=utf-8&amp;useSSL=false&amp;serverTimezone=GMT%2B8"/>
                <property name="username" value="root"/>
                <property name="password" value="123456"/>
            </dataSource>
        </environment>
    </environments>
    <!--扫描-->
    <mappers>
        <mapper class="dao.TestMapper"/>
    </mappers>

</configuration>

测试:

import dao.TestMapper;
import entity.PageQo;
import entity.TestEntity;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;

import java.io.InputStream;
import java.util.List;

/**
 * @author 发现更多精彩  关注公众号:木子的昼夜编程
 * 一个生活在互联网底层,做着增删改查的码农,不谙世事的造作
 */
public class TestMain06 {
    public static void main(String[] args) throws Exception {
        String resource = "mybatis-config.xml";
        InputStream inputStream = Resources.getResourceAsStream(resource);
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
        try (SqlSession session = sqlSessionFactory.openSession()) {
            // 通过sesson获取Mapper 这个Mapper会编程Mybatis的代理Mapper
            TestMapper mapper = session.getMapper(TestMapper.class);
            // offset偏移量从0开始  limit一共查几条
            Integer pageNum =1; // 当前页(一般与前端交互 就迁就一下他们 页数从1开始)
            Integer pageSize = 10; // 每页大小
            // 分页参数错误判断就先不判断了 根据业务自己判断
            List<TestEntity> res = mapper.selectPage(new PageQo(pageNum, pageSize));
            for (int i = 0; i < res.size(); i++) {
                System.out.println(res.get(i));
            }
        }
    }
}

输出:

浅谈 Mybatis 分页

Mybatis拦截器与我们平时web项目拦截器一个道理,就是一种机制,可以进行特殊扩展的,学过SpringBoot源码的人知道其实他也有类似的机制叫PostProcessor 可以自定义自己的业务实现

可以看到之后是修改了原始SQL 所以这个也是服务器端进行的分页,物理分页。

五、自动驾驶小轿车

PageHelper

他是一些来自互联网的高手自定义了Interceptor ,可以直接引入使用

等我们有能力了,一定要多给人提供开源的东西(写博客也算一种贡献方式)

大家使用的时候一定注意:

PageHelper.startPage() ;后面紧接着一定是Mapper的调用方法,

引入pom.xml

<!--pagehelper-->
<dependency>
    <groupId>com.github.pagehelper</groupId>
    <artifactId>pagehelper</artifactId>
    <!--520 真好 代码对我说:我爱你-->
    <version>5.2.0</version>
</dependency>

使用方式一:

与自定义Interceptor一样,mybatis-config.xml引入

<!--自定义拦截器-->
<plugins>
    <plugin interceptor="com.github.pagehelper.PageInterceptor">
    </plugin>
</plugins>
import com.github.pagehelper.PageHelper;
import dao.TestMapper;
import entity.TestEntity;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;

import java.io.InputStream;
import java.util.List;

/**
 * @author 发现更多精彩  关注公众号:木子的昼夜编程
 * 一个生活在互联网底层,做着增删改查的码农,不谙世事的造作
 */
public class TestMain07 {
    public static void main(String[] args) throws Exception {
        String resource = "mybatis-config.xml";
        InputStream inputStream = Resources.getResourceAsStream(resource);
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
        try (SqlSession session = sqlSessionFactory.openSession()) {
            // 通过sesson获取Mapper 这个Mapper会编程Mybatis的代理Mapper
            TestMapper mapper = session.getMapper(TestMapper.class);
            // offset偏移量从0开始  limit一共查几条
            Integer pageNum =1; // 当前页(一般与前端交互 就迁就一下他们 页数从1开始)
            Integer pageSize = 10; // 每页大小
            // 1. 最常用使用方式
            // https://github.com/pagehelper/Mybatis-PageHelper/blob/master/wikis/en/HowToUse.md
            PageHelper.startPage(pageNum, pageSize);
            List<TestEntity> res = mapper.select();
            for (int i = 0; i < res.size(); i++) {
                System.out.println(res.get(i));
            }
        }
    }
}

使用方式二:

自定义pageNum和pageSize 参数名称

mybatis-config.xml 需要修改

<!--自定义拦截器-->
<plugins>
    <!-- com.github.pagehelper为PageHelper类所在包名 -->
    <plugin interceptor="com.github.pagehelper.PageInterceptor">
        <!-- 使用下面的方式配置参数,后面会有所有的参数介绍 -->
        <property name="supportMethodsArguments" value="true"/>
        <!--自定义key值 闲的没事儿搞一个特殊的值 也是没谁了 一般我们就用pageSize pageNum-->
        <property name="params" value="pageNum=myPageNum;pageSize=myPageSize;"/>
    </plugin>
</plugins>

Mapper

@Select("select * from test")
List<TestEntity> select01(@Param("myPageNum") Integer pageNum, @Param("myPageSize") Integer pageSize);

@Select("select * from test")
List<TestEntity> select02(Map<String, Integer> params);

Test

import com.github.pagehelper.Page;
import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;
import dao.TestMapper;
import entity.TestEntity;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;

import javax.servlet.ServletRequest;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.InputStream;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * @author 发现更多精彩  关注公众号:木子的昼夜编程
 * 一个生活在互联网底层,做着增删改查的码农,不谙世事的造作
 */
public class TestMain08 {
    public static void main(String[] args) throws Exception {
        String resource = "mybatis-config.xml";
        InputStream inputStream = Resources.getResourceAsStream(resource);
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
        try (SqlSession session = sqlSessionFactory.openSession()) {
            // 通过sesson获取Mapper 这个Mapper会编程Mybatis的代理Mapper
            TestMapper mapper = session.getMapper(TestMapper.class);
            // offset偏移量从0开始  limit一共查几条
            Integer pageNum =1; // 当前页(一般与前端交互 就迁就一下他们 页数从1开始)
            Integer pageSize = 10; // 每页大小
            List<TestEntity> all = mapper.select();
            System.out.println("总条数:"+all.size());
            // 1. 参数方式
            // https://github.com/pagehelper/Mybatis-PageHelper/blob/master/wikis/en/HowToUse.md
            List<TestEntity> res = mapper.select01(pageNum, pageSize);
            System.out.println("分页条数01:"+res.size());
            // 2. 支持  ServletRequest,Map,POJO 对象,
            // request难获取,这里举一个Map 的例子吧
            Map<String, Integer> params = new HashMap<>();
            params.put("myPageNum", pageNum);
            params.put("myPageSize", pageSize);
            List<TestEntity> res02 = mapper.select02(params);
            System.out.println("分页条数02:"+res02.size());
            // 可以看到这里返回类型用List接收的,但是此list非彼list
            // 其实他返回的是一个com.github.pagehelper.Page 他继承了ArrayList
            // 这个page除了我们看到的数据以为 还有很多高级的东西
            // 我们可以直接转换成他提供的PageInfo
            PageInfo pageInfo = new PageInfo(res02);
            System.out.println("当前页数pageNum:"+pageInfo.getPageNum());
            System.out.println("分页大小pageSize:"+pageInfo.getPageSize());
            System.out.println("上一页页数prePage:"+pageInfo.getPrePage());
            System.out.println("一共多少页:"+pageInfo.getPages());
            System.out.println("当前页条数:"+pageInfo.getSize());
            System.out.println("总条数:"+pageInfo.getTotal());
            System.out.println("记录开始行数:"+pageInfo.getStartRow());
            System.out.println("记录结束行数:"+pageInfo.getEndRow());

            System.out.println("------------华丽丽的分割线-----------------");
            // 或者强转成Page使用也是ok的
            Page page = (Page)res02;
            System.out.println("当前页数pageNum:"+page.getPageNum());
            System.out.println("分页大小pageSize:"+page.getPageSize());
            System.out.println("一共多少页:"+page.getPages());
            System.out.println("当前页条数:"+page.size());
            System.out.println("总条数:"+page.getTotal());
            System.out.println("记录开始行数:"+page.getStartRow());
            System.out.println("记录结束行数:"+page.getEndRow());
        }
    }
}

浅谈 Mybatis 分页

其他用法:
它还可以指定是否使用count语句统计总条数,我之前同事遇到过,由于count一次耗费性能很大,所以只在第一次查询的时候count一次,如果再一次查询前端把count带过来,这边就不再进行count查询

六、唠唠

有人觉得PageHelper很神奇,为什么startPage调用,下边的方法就知道该分页,而且还能获取分页参数。

这里其实用到了一个知识点叫:ThreadLocal

startPage其实是把参数放到了ThreadLocal当中,当走到Interceptor的时候,会去ThreadLocal中找对应参数。

这个知识点以后会写相关文章来简单讲一下ThreadLocal的使用,这个知识点在项目需要动态切换数据源的时候也会用到。

有事儿没事儿关注公众号:木子的昼夜编程

上一篇:虚拟字段进行排序后分页


下一篇:PHP array_slice分装分页