分页

2021/11/21 mybatis 共 83719 字,约 240 分钟
闷骚的程序员

1. 什么是分页?

做过前端页面列表展示的朋友都清楚,当我们在页面上展示很多数据时,都是展示很多页,通过下面的下标(一般都表示当前页),第1页就是当前页,用1表示;第2页就是下一页,用2表示,以此类推。
一般最新的数据都在最前面,越老的数据就在后面。
当我们点击下一页时,就展示下一页的数据。
这个现象就叫做分页,即把很多数据进行分页展示。

2. 数据分页的方式

一般数据分页方式有两种:
(1)客户端(应用程序或浏览器)分页
(2)应用服务器分页

2.1 客户端分页

将数据从应用服务器全部下载到本地应用程序或浏览器,在应用程序或浏览器内部通过本地代码进行分页处理。
优点:编码简单,减少客户端与应用服务器网络交互次数
缺点:首次交互时间长,占用客户端内存
适应场景:客户端与应用服务器网络延时较大,但要求后续操作流畅,如手机GPRS,超远程访问(跨国)等等。

2.2 应用服务器分页

将数据从数据库服务器全部下载到应用服务器,在应用服务器内部再进行数据筛选。以下是一个应用服务器端Java程序分页的示例:

List list=executeQuery(select * from employee order by id);
Int count= list.size();
List subList= list.subList(10, 20);

优点:编码简单,只需要一次SQL交互,总数据与分页数据差不多时性能较好。
缺点:总数据量较多时性能较差。
适应场景:数据库系统不支持分页处理,数据量较小并且可控。

注意,在服务器分页的话,具体又划分为两种:
(1)逻辑分页;
(2)物理分页,也叫做数据库SQL分页,直接通过SQL来分页。

3. 逻辑分页

SQL层面不分页,即和数据库交互时一次性查出所有的数据,然后由后端程序去控制分页,将这些数据按照一定的分页规则,分批返回给客户端。
逻辑分页不涉及SQL层面,只是应用程序自己做了软分页,其对性能的提升几乎没有价值,适用于小批量的数据,因此实际使用场景不多。
可以这么说,逻辑分页除了跨数据库外,几乎一无是处,性能比较低,每次都是全量查询到ResultSet,然后再使用代码进行分页展示到前端(每次分页都全量查询,只是为了进行分页展示,假分页)。

3.1 使用mybatis提供的逻辑分页

3.1.1 resource下创建mybatis的全局配置文件

mybatis/sqlmap/sqlmap-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="mybatis/db.properties">
    </properties>

    <typeAliases>
        <package name="zeh.mybatis.code00common"/>
        <package name="zeh.mybatis.code09mybatisspring.pojo"/>
    </typeAliases>

    <environments default="development">
        <environment id="development">
            <transactionManager type="JDBC"/>
            <dataSource type="POOLED">
                <property name="driver" value="${jdbc.driver}"/>
                <property name="url" value="${jdbc.url}"/>
                <property name="username" value="${jdbc.username}"/>
                <property name="password" value="${jdbc.password}"/>
            </dataSource>
        </environment>
    </environments>

    <mappers>
        <mapper resource="mybatis/mapper/mybatis-yuanshi-cache-mapper.xml"/>
    </mappers>
</configuration>

3.1.2 resource创建数据源文件

mybatis/db.properties

# oracle 数据库连接配置
jdbc.driver=oracle.jdbc.driver.OracleDriver
jdbc.url=jdbc:oracle:thin:@localhost:1521:orcl
jdbc.username=oracle
jdbc.password=oracle

3.1.3 创建mapper.xml

    <!-- 全量查询users表,测试RowBounds逻辑分页功能 -->
    <select id="findUserAllYuanShi" resultType="zeh.mybatis.code00common.po.singleton.CommonPo">
        <!-- 测试_parameter参数,该参数表示传入进来的查询参数对象 -->
        <if test="_parameter == null">
            select * from zeh_user
        </if>
    </select>

上面的mapper.xml的含义是:当传递进来的查询参数为null的时,执行“select * from zeh_user”这个SQL。
注意,_parameter表示mybatis中默认传递进来的查询对象,即用_parameter就能表示这个statementId的查询对象。

3.1.4 编写DAO

写一个DAO接口:
zeh.mybatis.code03yuanshi.dao.IMybatisCacheDao

package zeh.mybatis.code03yuanshi.dao;
import java.util.List;
public interface IMybatisCacheDao<T> {

    // 全量查询users表信息
    public List<T> findUserAll() throws Exception;
}

编写实现类:
zeh.mybatis.code03yuanshi.dao.impl.MybatisCacheDaoImpl

package zeh.mybatis.code03yuanshi.dao.impl;

import org.apache.ibatis.session.RowBounds;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import zeh.mybatis.code00common.po.singleton.CommonPo;
import zeh.mybatis.code03yuanshi.dao.IMybatisCacheDao;

import java.util.Iterator;
import java.util.List;

public class MybatisCacheDaoImpl implements IMybatisCacheDao<CommonPo> {

    private SqlSessionFactory sqlSessionFactory;

    public MybatisCacheDaoImpl(SqlSessionFactory sqlSessionFactory) {
        this.sqlSessionFactory = sqlSessionFactory;
    }

    // 使用RowBounds对全量查询的结果进行逻辑分页
    @Override
    public List<CommonPo> findUserAll() throws Exception {
        SqlSession sqlSession = this.sqlSessionFactory.openSession();

        RowBounds rb = new RowBounds(4, 3);// 设置RowBounds分页查询参数
        List<CommonPo> list = sqlSession.selectList("cache.findUserAllYuanShi", null, rb);

        sqlSession.close();

        return list;
    }
}

RowBounds是mybatis提供的一个用于设置逻辑分页参数的对象,我们在执行statementId时只需要传入这个对象,设置好分页参数,mybatis就能自动对这次查询出来的结果按照我们的分页参数进行逻辑分页。

RowBounds源码如下:
org.apache.ibatis.session.RowBounds

package org.apache.ibatis.session;

public class RowBounds {
    public static final int NO_ROW_OFFSET = 0;
    public static final int NO_ROW_LIMIT = 2147483647;
    public static final RowBounds DEFAULT = new RowBounds();
    private int offset;
    private int limit;

    public RowBounds() {
        this.offset = 0;
        this.limit = 2147483647;
    }

    public RowBounds(int offset, int limit) {
        this.offset = offset;
        this.limit = limit;
    }

    public int getOffset() {
        return this.offset;
    }

    public int getLimit() {
        return this.limit;
    }
}

其构造器接收两个参数:

offset: 当前起始位
limit: 当前要查询的条数

因此,取出来的数据就是:从第offset+1行开始,取limit行.

3.1.5 测试

package zeh.mybatis.code03yuanshi.run;


import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import org.junit.Before;
import org.junit.Test;
import zeh.mybatis.code00common.po.singleton.CommonPo;
import zeh.mybatis.code03yuanshi.dao.IMybatisCacheDao;
import zeh.mybatis.code03yuanshi.dao.impl.MybatisCacheDaoImpl;

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

public class MybatisCacheDaoRun {
    private SqlSessionFactory sqlSessionFactory;

    @Before
    public void setUp() throws Exception {
        String resource = "mybatis/sqlmap" + File.separator + "sqlmap-config.xml";
        InputStream inputStream = Resources.getResourceAsStream(resource);
        this.sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
    }


    // 检测mybatis自带的逻辑分页功能RowBounds。
    @Test
    public void testFindUserAll() throws Exception {
        IMybatisCacheDao<CommonPo> mybatisCacheDao = new MybatisCacheDaoImpl(sqlSessionFactory);
        List<CommonPo> list = mybatisCacheDao.findUserAll();
        Iterator<CommonPo> iterator = list.iterator();
        System.out.println("全量查询的users表的record如下:");
        while (iterator.hasNext()) {
            System.out.println(iterator.next());
        }
    }
}

日志如下:

==>  Preparing: select * from zeh_user 
==> Parameters: 
<==    Columns: ID, NAME, AGE, SEX, HOBBY
<==        Row: 4, 更新, 29, 不男不女, 吃饭喝酒
<==        Row: 8, getMyName, 27, 不男不女, 唱歌、写作、编程
<==        Row: 8, getMyName, 27, 不男不女, 唱歌、写作、编程
<==        Row: 8, getMyName, 27, 不男不女, 唱歌、写作、编程
<==        Row: 4, 更新, 29, 不男不女, 吃饭喝酒
<==        Row: 110, 批量1, 9, 不男不女, kanshu
<==        Row: 111, 批量2, 12, 不男不女, kanshu

全量查询的users表的record如下:
[id = 4;name = 更新;age = 29;sex = 不男不女;hobby = 吃饭喝酒]
[id = 110;name = 批量1;age = 9;sex = 不男不女;hobby = kanshu]
[id = 111;name = 批量2;age = 12;sex = 不男不女;hobby = kanshu]

从日志可看出,使用 RowBounds 分页,发送给DB的SQL是全量查询的,将结果全部查询到内存中后才对数据在内存中进行分页。
这就是逻辑分页,也叫做内存分页。

逻辑分页应对于数据量比较小的情况,它对内存的要求比较高,生产环境慎用,经常会导致OOM,即内存溢出。

3.2 mybatis的二级缓存结合逻辑分页

3.2.1 全局配置中开启mybatis的二级缓存

<?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="mybatis/db.properties">
    </properties>

    <settings>
        <setting name="logImpl" value="STDOUT_LOGGING"/>

        <!-- 开启mybatis的二级缓存配置,默认为true开启。如果此处设置为false,则所有mapper中的二级缓存即便配置开启也不生效。 -->
        <setting name="cacheEnabled" value="true"/>

        <!-- mybatis返回空字段,设置此属性为true,则mybatis每次执行完查询后,返回的 list<Map<String,Object>>中当字段查询值为null的时候, -->
        <!-- 也会将字段名和字段值存储在map的entry中 -->
        <setting name="callSettersOnNulls" value="true"/>

        <!-- 打开延迟加载 的开关 -->
        <setting name="lazyLoadingEnabled" value="true"/>
        <!-- 将积极加载改为消极加载即按需要加载 -->
        <setting name="aggressiveLazyLoading" value="false"/>
    </settings>

    <typeAliases>
        <package name="zeh.mybatis.code00common"/>
        <package name="zeh.mybatis.code09mybatisspring.pojo"/>
    </typeAliases>

    <environments default="development">
        <environment id="development">
            <transactionManager type="JDBC"/>
            <dataSource type="POOLED">
                <property name="driver" value="${jdbc.driver}"/>
                <property name="url" value="${jdbc.url}"/>
                <property name="username" value="${jdbc.username}"/>
                <property name="password" value="${jdbc.password}"/>
            </dataSource>
        </environment>
    </environments>

    <mappers>
        <mapper resource="mybatis/mapper/mybatis-yuanshi-cache-mapper.xml"/>
    </mappers>

</configuration>

上述全局配置文件中,开启了mybatis的二级缓存,而且,全局配置中的二级缓存开关默认就是true,即默认打开。

3.2.2 mapper.xml中增加当前mapper的二级缓存配置

在mybatis/mapper/mybatis-yuanshi-cache-mapper.xml中,增加二级缓存配置。
注意,二级缓存以namespace进行区分 ;即同一个sqlSession下不同的namespace持有不同的二级缓存,二级缓存是namespace级别的。
注意当前mapper.xml中通过标签进行缓存配置,才表示开启二级缓存。
但是此处的配置取决于全局的配置开关是否打开(注意全局的二级缓存配置开关默认是打开的)。
如果sqlmapconfig.xml中全局的二级缓存配置手动关闭,则即便此处开启了二级缓存也不生效。
设置二级缓存推荐使用的是 ehcache 框架,下面标签中的type属性是ehcache和mybatis整合包中的Cache接口的实现类。 mybatis开放了Cache接口供第三方缓存框架实现,如果需要自定义缓存框架则需要自己实现Cache接口。 Cache接口:

org.apache.ibatis.cache.Cache

mbatis和ehcache整合包中的实现类:

org.mybatis.caches.ehcache.EhcacheCache

配置ehcache时,根据需求调整参数。
如果只在下面文件中使用配置,则mybatis默认的二级缓存的Cache接口的实现类是:

org.apache.ibatis.cache.impl.PerpetualCache

即mybatis自己实现的二级缓存,一般开发中都使用第三方缓存框架。

但在此案例中,我们指定标签的内容,让其使用第三方缓存框架ehcache。

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="cache">
    <!-- 开启本mapper的namespace下的二级缓存, -->
    <!-- 整合EhCache -->
    <cache type="org.mybatis.caches.ehcache.EhcacheCache">
        <property name="timeToIdleSeconds" value="3600"/>
        <property name="timeToLiveSeconds" value="3600"/>
        <!-- 同ehcache参数maxElementsInMemory -->
        <property name="maxEntriesLocalHeap" value="1000"/>
        <!-- 同ehcache参数maxElementsOnDisk -->
        <property name="maxEntriesLocalDisk" value="10000000"/>
        <property name="memoryStoreEvictionPolicy" value="LRU"/>
    </cache>

    <!-- 全量查询users表,测试RowBounds逻辑分页功能 -->
    <select id="findUserAllYuanShi" resultType="zeh.mybatis.code00common.po.singleton.CommonPo">
        <!-- 测试_parameter参数,该参数表示传入进来的查询参数对象 -->
        <if test="_parameter == null">
            select * from zeh_user
        </if>
    </select>

</mapper>

3.2.3 DAO接口中新增方法

    // 分页结合缓存
    public List<T> findUserAllByCache1() throws Exception;

3.2.4 DAO实现类中新增方法

    @Override
    public List<CommonPo> findUserAllByCache1() throws Exception {
        SqlSession sqlSession = this.sqlSessionFactory.openSession();

        // 尽管是在同一个sqlSession中,但是每次传递的分页参数不同
        List<CommonPo> list = sqlSession.selectList("cache.findUserAllYuanShi", null, new RowBounds(4, 3));
        List<CommonPo> list2 = sqlSession.selectList("cache.findUserAllYuanShi", null, new RowBounds(4, 8));

        // 采用标准iterator输出集合输出List
        Iterator<CommonPo> iterator = list.iterator();
        System.out.println("全量查询的users表的record如下:");
        while (iterator.hasNext()) {
            System.out.println(iterator.next());
        }
        Iterator<CommonPo> iterator2 = list2.iterator();
        System.out.println("全量查询的users表的record如下:");
        while (iterator2.hasNext()) {
            System.out.println(iterator2.next());
        }
        sqlSession.close();
        return list;
    }

上面代码中,我们在mybatis的同一个sqlSession中,连续进行了两次SQL查询动作,如果按照二级缓存的作用范围来说,应该在第二次查询时会直接从二级缓存中获取数据,而不会再次查询SQL。
但是,我们两次查询传递进去的RowBounds分页对象不同,因为设置了不同的分页参数,那么二级缓存到底会不会生效呢?
我们来测试一下。

3.2.5 测试

    @Test
    public void testFindUserAllByCache1() throws Exception {
        IMybatisCacheDao<CommonPo> mybatisCacheDao = new MybatisCacheDaoImpl(sqlSessionFactory);
        mybatisCacheDao.findUserAllByCache1();
    }

日志如下:

Logging initialized using 'class org.apache.ibatis.logging.stdout.StdOutImpl' adapter.
PooledDataSource forcefully closed/removed all connections.
PooledDataSource forcefully closed/removed all connections.
PooledDataSource forcefully closed/removed all connections.
PooledDataSource forcefully closed/removed all connections.
Cache Hit Ratio [cache]: 0.0
Opening JDBC Connection
Created connection 1169794610.
Setting autocommit to false on JDBC Connection [oracle.jdbc.driver.T4CConnection@45b9a632]
==>  Preparing: select * from zeh_user 
==> Parameters: 
<==    Columns: ID, NAME, AGE, SEX, HOBBY
<==        Row: 4, 更新, 29, 不男不女, 吃饭喝酒
<==        Row: 8, getMyName, 27, 不男不女, 唱歌、写作、编程
<==        Row: 8, getMyName, 27, 不男不女, 唱歌、写作、编程
<==        Row: 8, getMyName, 27, 不男不女, 唱歌、写作、编程
<==        Row: 4, 更新, 29, 不男不女, 吃饭喝酒
<==        Row: 110, 批量1, 9, 不男不女, kanshu
<==        Row: 111, 批量2, 12, 不男不女, kanshu
Cache Hit Ratio [cache]: 0.0
==>  Preparing: select * from zeh_user 
==> Parameters: 
<==    Columns: ID, NAME, AGE, SEX, HOBBY
<==        Row: 4, 更新, 29, 不男不女, 吃饭喝酒
<==        Row: 8, getMyName, 27, 不男不女, 唱歌、写作、编程
<==        Row: 8, getMyName, 27, 不男不女, 唱歌、写作、编程
<==        Row: 8, getMyName, 27, 不男不女, 唱歌、写作、编程
<==        Row: 4, 更新, 29, 不男不女, 吃饭喝酒
<==        Row: 110, 批量1, 9, 不男不女, kanshu
<==        Row: 111, 批量2, 12, 不男不女, kanshu
<==        Row: 4, 于瑞, 18, 不男不女, 发呆
<==      Total: 8
全量查询的users表的record如下:
[id = 4;name = 更新;age = 29;sex = 不男不女;hobby = 吃饭喝酒]
[id = 110;name = 批量1;age = 9;sex = 不男不女;hobby = kanshu]
[id = 111;name = 批量2;age = 12;sex = 不男不女;hobby = kanshu]
全量查询的users表的record如下:
[id = 4;name = 更新;age = 29;sex = 不男不女;hobby = 吃饭喝酒]
[id = 110;name = 批量1;age = 9;sex = 不男不女;hobby = kanshu]
[id = 111;name = 批量2;age = 12;sex = 不男不女;hobby = kanshu]
[id = 4;name = 于瑞;age = 18;sex = 不男不女;hobby = 发呆]
Resetting autocommit to true on JDBC Connection [oracle.jdbc.driver.T4CConnection@45b9a632]
Closing JDBC Connection [oracle.jdbc.driver.T4CConnection@45b9a632]
Returned connection 1169794610 to pool.

从日志结果看出,尽管开启了mybatis的二级缓存,在同一个sqlSession中还是进行了两次查询。
二级缓存并没有生效。
这是因为:逻辑分页每次传入的offset和limit不同,mybatis将认为是两个不同的 statementId 而不会查询缓存。
因此,mybatis的二级缓存对RowBounds逻辑分页并没有任何作用。

4. 物理分页

使用mybatis来进行物理分页,比较常见的做法是使用开源的插件,pageHelper。
pageHelper是开源的组件,其依靠mybatis提供的拦截器功能实现,在mybatis每次进行SQL查询前对statement的组装进行拦截,根据不同的数据源,将SQL转换为对应的物理分页SQL。

4.1 使用物理分页组件pageHelper

4.1.1 全局配置中配置pageHelper

<?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="mybatis/db.properties">
    </properties>

    <settings>
        <setting name="logImpl" value="STDOUT_LOGGING"/>

        <!-- 开启mybatis的二级缓存配置,默认为true开启。如果此处设置为false,则所有mapper中的二级缓存即便配置开启也不生效。 -->
        <setting name="cacheEnabled" value="true"/>

        <!-- mybatis返回空字段,设置此属性为true,则mybatis每次执行完查询后,返回的 list<Map<String,Object>>中当字段查询值为null的时候, -->
        <!-- 也会将字段名和字段值存储在map的entry中 -->
        <setting name="callSettersOnNulls" value="true"/>

        <!-- 打开延迟加载 的开关 -->
        <setting name="lazyLoadingEnabled" value="true"/>
        <!-- 将积极加载改为消极加载即按需要加载 -->
        <setting name="aggressiveLazyLoading" value="false"/>
    </settings>

    <typeAliases>
        <package name="zeh.mybatis.code00common"/>
        <package name="zeh.mybatis.code09mybatisspring.pojo"/>
    </typeAliases>

    <!-- 配置分页插件拦截器,拦截mybatis发送给数据库的sql,并设置分页参数 -->
    <plugins>
        <!-- com.github.pagehelper为PageHelper类所在包名 -->
        <plugin interceptor="com.github.pagehelper.PageInterceptor">
            <!-- 配置对应数据库的方言:4.0.0以后版本可以不设置该参数 ,mybatis会自动确认使用的数据库。 -->
            <!-- <property name="dialect" value="oracle"></property> -->
        </plugin>
    </plugins>

    <environments default="development">
        <environment id="development">
            <transactionManager type="JDBC"/>
            <dataSource type="POOLED">
                <property name="driver" value="${jdbc.driver}"/>
                <property name="url" value="${jdbc.url}"/>
                <property name="username" value="${jdbc.username}"/>
                <property name="password" value="${jdbc.password}"/>
            </dataSource>
        </environment>
    </environments>

    <mappers>
        <mapper resource="mybatis/mapper/mybatis-yuanshi-cache-mapper.xml"/>
        <mapper resource="mybatis/mapper/mybatis-yuanshi-pagehelper-mapper.xml"/>
    </mappers>

</configuration>

4.1.2 mapper.xml

mybatis/mapper/mybatis-yuanshi-pagehelper-mapper.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="pagehelper">
    <cache type="org.mybatis.caches.ehcache.EhcacheCache">
        <property name="timeToIdleSeconds" value="3600"/>
        <property name="timeToLiveSeconds" value="3600"/>
        <!-- 同ehcache参数maxElementsInMemory -->
        <property name="maxEntriesLocalHeap" value="1000"/>
        <!-- 同ehcache参数maxElementsOnDisk -->
        <property name="maxEntriesLocalDisk" value="10000000"/>
        <property name="memoryStoreEvictionPolicy" value="LRU"/>
    </cache>

    <select id="findUserByIdForPageHelper" parameterType="String"
            resultType="zeh.mybatis.code00common.po.singleton.CommonPo">
        select * from zeh_user where 1=1
        <if test="value != null">
            and id=#{value}
        </if>
    </select>
</mapper>

4.1.3 定义DAO接口

zeh.mybatis.code03yuanshi.dao.IPageHelperDao

package zeh.mybatis.code03yuanshi.dao;
import java.util.List;
public interface IPageHelperDao<T> {

    // 根据id查询用户信息
    public List<T> findUserByIdForPageHelper(String param) throws Exception;
}

4.1.3 定义DAO实现类

zeh.mybatis.code03yuanshi.dao.impl.PageHelperDaoImpl

package zeh.mybatis.code03yuanshi.dao.impl;

import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import zeh.mybatis.code00common.po.singleton.CommonPo;
import zeh.mybatis.code03yuanshi.dao.IPageHelperDao;

import java.util.List;

public class PageHelperDaoImpl implements IPageHelperDao<CommonPo> {

    private SqlSessionFactory sqlSessionFactory;

    public PageHelperDaoImpl(SqlSessionFactory sqlSessionFactory) {
        this.sqlSessionFactory = sqlSessionFactory;
    }

    @Override
    public List<CommonPo> findUserByIdForPageHelper(String id) throws Exception {
        SqlSession sqlSession = this.sqlSessionFactory.openSession();
        List<CommonPo> list = sqlSession.selectList("pagehelper.findUserByIdForPageHelper", id);
        sqlSession.close();
        return list;
    }
}

4.1.4 测试1

测试 pageHelper 物理分页插件的使用。
其原理是:在执行sql前,加入PageHelper物理分页插件,每次都执行指定分页参数的查询sql,而不是全量查询,因此提升查询性能。
本案例,将PageHelper.startPage(2, 5);写在Service层不合理,应该写在DAO层,后面紧跟mybatis的查询语句。因为PageHelper.startPage()后面如果不紧跟mybatis的查询语句的话,就有可能在SQL执行前对SQL做了一些其他处理,这样容易导致分页失效。

不合理使用pageHelper会导致不安全的分页:
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>();
}

这种写法就能保证安全.
如果你对此不放心,你可以手动清理 ThreadLocal 存储的分页参数,可以像下面这样使用:

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

这么写很不好看,而且没有必要。

我们需要记住,pageHelper基于ThreadLocal记录参数,生效于代理对象,且拦截生效于第一个SQL语句。

下面是pageHelper的几种使用方式:
(1)直接使用mapper查询,pageHelper生效

PageHelper.startPage(1,5);
mapper.selectByKey(22);

(2)后面跟子线程,pageHelper失效

PageHelper.startPage(1,5);
new Thread(()->{mapper.selectById(22);}).start();

(3)存在条件,导致分页错乱
建议:分页设置后紧跟SQL查询,避免跨逻辑。

PageHelper.startPage(1,5);
if(flag){
    mapper.list(query);
}
mapper2.list(query);

下面看测试:

package zeh.mybatis.code03yuanshi.run;

import com.github.pagehelper.Page;
import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import org.junit.Before;
import org.junit.Test;
import zeh.mybatis.code00common.po.singleton.CommonPo;
import zeh.mybatis.code00common.utils.PageInfoLimitUtil;
import zeh.mybatis.code03yuanshi.dao.IPageHelperDao;
import zeh.mybatis.code03yuanshi.dao.impl.PageHelperDaoImpl;

import java.io.File;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.stream.Collectors;

public class PageHelperDaoRun {
    private SqlSessionFactory sqlSessionFactory;

    @Before
    public void setUp() throws Exception {
        String resource = "mybatis/sqlmap" + File.separator + "sqlmap-config.xml";
        InputStream inputStream = Resources.getResourceAsStream(resource);
        SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
        this.sqlSessionFactory = sqlSessionFactoryBuilder.build(inputStream);
    }

    @Test
    public void testPageHelperShiYong() throws Exception {
        // 实例化 IPageHelperDao 对象
        IPageHelperDao<CommonPo> pageHelperDao = new PageHelperDaoImpl(sqlSessionFactory);

        // 通过 PageHelper 物理分页插件进行 sql 拦截分页。
        // 第一个参数是当前页面索引,即当前查询第几页。
        // 第二个参数是当前页面应该显示的record条数,即当前页面展示的记录数。
        // 在要执行的 statementId 前面加上该分页语句,则该条 statementId 绑定的sql将会被分页拦截器拦截并拼装成带有分页参数的sql语句。
        PageHelper.startPage(2, 5);

        // 调用 IPageHelperDao 的方法。
        // 上面加上PageHelper分页参数后,会拦截sql拼装成分页sql后再执行。
        // 如果想要获取分页后的sql结果,则按照下面第一种方式。
        // 如果不仅仅想要获取单纯的结果list,还想获取分页的其他信息,则要强转成Page对象。
        // PageHelper返回的Page对象是list集合的子类,将原来的list强转成Page后会将ThreadLocal保存的page的其他属性一同塞进集合里。
//         List<CommonPo> list = pageHelperDao.findUserByIdForPageHelper("8");
        Page<CommonPo> list = (Page<CommonPo>) pageHelperDao.findUserByIdForPageHelper("3");

        //上面返回的list已经不仅仅只包含查询结果,也包含众多分页信息 ,下面将从执行后的page对象中获取page的其他属性信息
        System.out.println("-----" + list.getCountColumn());
        System.out.println("--开始行(从哪一行开始)---" + list.getStartRow());
        System.out.println("--结束行(从哪一行开始结束)---" + list.getEndRow());
        System.out.println("--排序列---" + list.getOrderBy());
        System.out.println("--当前页面索引---" + list.getPageNum());
        System.out.println("--总共显示了几页---" + list.getPages());
        System.out.println("--当前页的页面大小(显式多少行)---" + list.getPageSize());
        System.out.println("--开始行(不包含当前行)---" + list.getStartRow());
        System.out.println("--执行count的总条数---" + list.getTotal());// 获取分页sql执行count(1)的查询总量结果,如果count(1)查询标志设置为false,则此值永远为1。
        System.out.println("-----" + list.getPageSizeZero());
        System.out.println("-----" + list.getReasonable());
        System.out.println("--取得page的所有属性和查询结果集合---" + list.getResult());

        //采用标准iterator输出集合输出List
        Iterator<CommonPo> iterator = list.iterator();
        System.out.println("根据用户id查询的users表的record如下:");
        while (iterator.hasNext()) {
            System.out.println(iterator.next().getName());
        }
    }
}

日志:

Logging initialized using 'class org.apache.ibatis.logging.stdout.StdOutImpl' adapter.
PooledDataSource forcefully closed/removed all connections.
PooledDataSource forcefully closed/removed all connections.
PooledDataSource forcefully closed/removed all connections.
PooledDataSource forcefully closed/removed all connections.
Created connection 517052730.
Returned connection 517052730 to pool.
Cache Hit Ratio [pagehelper]: 0.0
Opening JDBC Connection
Checked out connection 517052730 from pool.
Setting autocommit to false on JDBC Connection [oracle.jdbc.driver.T4CConnection@1ed1993a]
==>  Preparing: SELECT count(0) FROM zeh_user WHERE 1 = 1 AND id = ? 
==> Parameters: 8(String)
<==    Columns: COUNT(0)
<==        Row: 48
<==      Total: 1
Cache Hit Ratio [pagehelper]: 0.0
==>  Preparing: SELECT * FROM ( SELECT TMP_PAGE.*, ROWNUM ROW_ID FROM ( select * from zeh_user where 1=1 and id=? ) TMP_PAGE WHERE ROWNUM <= ? ) WHERE ROW_ID > ? 
==> Parameters: 8(String), 10(Integer), 5(Integer)
<==    Columns: ID, NAME, AGE, SEX, HOBBY, ROW_ID
<==        Row: 8, getMyName, 27, 不男不女, 唱歌、写作、编程, 6
<==        Row: 8, getMyName, 27, 不男不女, 唱歌、写作、编程, 7
<==        Row: 8, getMyName, 27, 不男不女, 唱歌、写作、编程, 8
<==        Row: 8, getMyName, 27, 不男不女, 唱歌、写作、编程, 9
<==        Row: 8, getMyName, 27, 不男不女, 唱歌、写作、编程, 10
<==      Total: 5
Resetting autocommit to true on JDBC Connection [oracle.jdbc.driver.T4CConnection@1ed1993a]
Closing JDBC Connection [oracle.jdbc.driver.T4CConnection@1ed1993a]
Returned connection 517052730 to pool.
-----0
--开始行(从哪一行开始)---5
--结束行(从哪一行开始结束)---10
--排序列---null
--当前页面索引---2
--总共显示了几页---10
--当前页的页面大小(显式多少行)---5
--开始行(不包含当前行)---5
--执行count的总条数---48
-----false
-----false
--取得page的所有属性和查询结果集合---Page{count=true, pageNum=2, pageSize=5, startRow=5, endRow=10, total=48, pages=10, reasonable=false, pageSizeZero=false}[[id = 8;name = getMyName;age = 27;sex = 不男不女;hobby = 唱歌、写作、编程], [id = 8;name = getMyName;age = 27;sex = 不男不女;hobby = 唱歌、写作、编程], [id = 8;name = getMyName;age = 27;sex = 不男不女;hobby = 唱歌、写作、编程], [id = 8;name = getMyName;age = 27;sex = 不男不女;hobby = 唱歌、写作、编程], [id = 8;name = getMyName;age = 27;sex = 不男不女;hobby = 唱歌、写作、编程]]
根据用户id查询的users表的record如下:
getMyName
getMyName
getMyName
getMyName
getMyName

从日志可看出:
(1)pageHelper物理分页生效了,mybatis自动根据连接的数据源将SQL转换为对应的SQL物理分页。
(2)pageHelper在执行查询SQL前,会首先执行不带分页参数的SELECT COUNT(0) 操作。
(3)将分页结果转换为Page对象是常规的做法,Page对象对数据集做了包装,里面还封装了分页的很多其他参数供外部使用。

4.1.5 测试2

上一个案例是将DAO的返回值直接强转成了pageHelper提供的Page对象。
实际上,PageHelper在设置分页参数方法时,本身也有返回值,就是Page对象:

Page<CommonPo> page = PageHelper.startPage(1, 3, Boolean.TRUE);

我们可以看下startPage()方法的源码:

   public static <E> Page<E> startPage(int pageNum, int pageSize, boolean count) {
        return startPage(pageNum, pageSize, count, (Boolean)null, (Boolean)null);
    }

startPage()方法本身就返回一个Page对象。
所以,我们也可以直接使用startPage()方法的返回值。
但是这种方式并不常用,实际中往往是直接使用dao接口返回的结果集,然后对其转型成page对象,因为这样代码的结果集不依赖pageHelper。

该版本的pageHelper的startPage还有第三个参数,即设置是否将拦截sql生成count(1)的总量查询:
如果设置为true,则会生成总量查询语句,当拦截后的count(1)为0时,不会生成select * from 详情查询sql;只有当count(1)大于0,才会生成select * from 详情查询sql。
如果设置为false,则不会生成count(1)总量查询sql,查询总数也不会设置到结果集page或者pageInfo对象中(因为根本就没有执行查询总数的sql,count总默认为1),而且每次都会执行详情sql。
默认为true。
这也是为什么上一个案例,默认会打印出SELECT COUNT(0)语句的原因。

在直接使用PageHelper.startPage(2, 5)方法返回的Page对象作为结果集时,应注意:
PageHelper.startPage(2, 5)返回的page对象没有存储真正的分页查询结果,只是保存了对真正sql处理后的结果集的一个引用而已。
在真正的 sql 执行前,此处返回的 page 对象是没有真正的属性信息的;只有当真正的查询sql执行完毕才会回调该方法将结果的引用设置进去。
同时注意,startPage()方法被重载了很多个,可以设置排序字段,可以指定是否生成count(1)的查询sql等。

在真正执行SQL前,Page对象没有任何属性;只有在执行SQL后,才会拦截它,向其中回填真正查询出来的各种属性信息。

   // 直接使用 PageHelper.startPage(1, 3, Boolean.TRUE); 方法返回的page对象接收执行后的sql结果集。
    @Test
    public void testPageHelperByOrder() throws Exception {
        IPageHelperDao<CommonPo> pageHelperDao = new PageHelperDaoImpl(sqlSessionFactory);

        // 直接使用PageHelper.startPage()返回值作为相应结果,此时Page对象只是持有结果集的一个引用,其他属性信息全没有。
        Page<CommonPo> page = PageHelper.startPage(1, 3, Boolean.TRUE);
        // pageHelper设置排序列,注意pageHelper也可以对指定列设置orderBy,此时是拦截sql,先全量查询进行全部排序,然后再进行分页截取。
        PageHelper.orderBy("name");
        // 拦截sql前,page 对象没有任何属性信息。
        System.out.println("sql执行前的page对象:" + page.getTotal());
        pageHelperDao.findUserByIdForPageHelper("8");
        //在sql执行后
        System.out.println("sql执行后的page对象:" + page.getTotal());

        // 采用标准iterator输出集合输出List
        Iterator<CommonPo> iterator = page.iterator();
        System.out.println("根据用户id查询的users表的record如下:");
        while (iterator.hasNext()) {
            System.out.println(iterator.next().getName());
        }
    }

日志:

Logging initialized using 'class org.apache.ibatis.logging.stdout.StdOutImpl' adapter.
PooledDataSource forcefully closed/removed all connections.
PooledDataSource forcefully closed/removed all connections.
PooledDataSource forcefully closed/removed all connections.
PooledDataSource forcefully closed/removed all connections.
sql执行前的page对象:0
Created connection 517052730.
Returned connection 517052730 to pool.
Cache Hit Ratio [pagehelper]: 0.0
Opening JDBC Connection
Checked out connection 517052730 from pool.
Setting autocommit to false on JDBC Connection [oracle.jdbc.driver.T4CConnection@1ed1993a]
==>  Preparing: SELECT count(0) FROM zeh_user WHERE 1 = 1 AND id = ? 
==> Parameters: 8(String)
<==    Columns: COUNT(0)
<==        Row: 96
<==      Total: 1
Cache Hit Ratio [pagehelper]: 0.0
==>  Preparing: SELECT * FROM ( SELECT TMP_PAGE.*, ROWNUM ROW_ID FROM ( SELECT * FROM zeh_user WHERE 1 = 1 AND id = ? order by name ) TMP_PAGE WHERE ROWNUM <= ? ) WHERE ROW_ID > ? 
==> Parameters: 8(String), 3(Integer), 0(Integer)
<==    Columns: ID, NAME, AGE, SEX, HOBBY, ROW_ID
<==        Row: 8, getMyName, 27, 不男不女, 唱歌、写作、编程, 1
<==        Row: 8, getMyName, 27, 不男不女, 唱歌、写作、编程, 2
<==        Row: 8, getMyName, 27, 不男不女, 唱歌、写作、编程, 3
<==      Total: 3
Resetting autocommit to true on JDBC Connection [oracle.jdbc.driver.T4CConnection@1ed1993a]
Closing JDBC Connection [oracle.jdbc.driver.T4CConnection@1ed1993a]
Returned connection 517052730 to pool.
sql执行后的page对象:96
根据用户id查询的users表的record如下:
getMyName
getMyName
getMyName

从日志可以看出,查询时对结果按照name字段进行了排序,然后物理分页,符合结果。

5. 使用PageInfo对pageHelper返回的Page(List)进行再次包装

5.1 PageInfo是什么?

PageInfo比Page作用更强大,都是pageHelper包提供的结果集封装对象。 源码如下:
com.github.pagehelper.PageInfo

package com.github.pagehelper;

import java.util.Collection;
import java.util.List;

public class PageInfo<T> extends PageSerializable<T> {
    private int pageNum;
    private int pageSize;
    private int size;
    private int startRow;
    private int endRow;
    private int pages;
    private int prePage;
    private int nextPage;
    private boolean isFirstPage;
    private boolean isLastPage;
    private boolean hasPreviousPage;
    private boolean hasNextPage;
    private int navigatePages;
    private int[] navigatepageNums;
    private int navigateFirstPage;
    private int navigateLastPage;

    public PageInfo() {
        this.isFirstPage = false;
        this.isLastPage = false;
        this.hasPreviousPage = false;
        this.hasNextPage = false;
    }

    public PageInfo(List<T> list) {
        this(list, 8);
    }

    public PageInfo(List<T> list, int navigatePages) {
        super(list);
        this.isFirstPage = false;
        this.isLastPage = false;
        this.hasPreviousPage = false;
        this.hasNextPage = false;
        if (list instanceof Page) {
            Page page = (Page)list;
            this.pageNum = page.getPageNum();
            this.pageSize = page.getPageSize();
            this.pages = page.getPages();
            this.size = page.size();
            if (this.size == 0) {
                this.startRow = 0;
                this.endRow = 0;
            } else {
                this.startRow = page.getStartRow() + 1;
                this.endRow = this.startRow - 1 + this.size;
            }
        } else if (list instanceof Collection) {
            this.pageNum = 1;
            this.pageSize = list.size();
            this.pages = this.pageSize > 0 ? 1 : 0;
            this.size = list.size();
            this.startRow = 0;
            this.endRow = list.size() > 0 ? list.size() - 1 : 0;
        }

        if (list instanceof Collection) {
            this.navigatePages = navigatePages;
            this.calcNavigatepageNums();
            this.calcPage();
            this.judgePageBoudary();
        }

    }

    public static <T> PageInfo<T> of(List<T> list) {
        return new PageInfo(list);
    }

    public static <T> PageInfo<T> of(List<T> list, int navigatePages) {
        return new PageInfo(list, navigatePages);
    }

    private void calcNavigatepageNums() {
        int i;
        if (this.pages <= this.navigatePages) {
            this.navigatepageNums = new int[this.pages];

            for(i = 0; i < this.pages; ++i) {
                this.navigatepageNums[i] = i + 1;
            }
        } else {
            this.navigatepageNums = new int[this.navigatePages];
            i = this.pageNum - this.navigatePages / 2;
            int endNum = this.pageNum + this.navigatePages / 2;
            int i;
            if (i < 1) {
                i = 1;

                for(i = 0; i < this.navigatePages; ++i) {
                    this.navigatepageNums[i] = i++;
                }
            } else if (endNum > this.pages) {
                endNum = this.pages;

                for(i = this.navigatePages - 1; i >= 0; --i) {
                    this.navigatepageNums[i] = endNum--;
                }
            } else {
                for(i = 0; i < this.navigatePages; ++i) {
                    this.navigatepageNums[i] = i++;
                }
            }
        }

    }

    private void calcPage() {
        if (this.navigatepageNums != null && this.navigatepageNums.length > 0) {
            this.navigateFirstPage = this.navigatepageNums[0];
            this.navigateLastPage = this.navigatepageNums[this.navigatepageNums.length - 1];
            if (this.pageNum > 1) {
                this.prePage = this.pageNum - 1;
            }

            if (this.pageNum < this.pages) {
                this.nextPage = this.pageNum + 1;
            }
        }

    }

    private void judgePageBoudary() {
        this.isFirstPage = this.pageNum == 1;
        this.isLastPage = this.pageNum == this.pages || this.pages == 0;
        this.hasPreviousPage = this.pageNum > 1;
        this.hasNextPage = this.pageNum < this.pages;
    }

    public int getPageNum() {
        return this.pageNum;
    }

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

    public int getPageSize() {
        return this.pageSize;
    }

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

    public int getSize() {
        return this.size;
    }

    public void setSize(int size) {
        this.size = size;
    }

    public int getStartRow() {
        return this.startRow;
    }

    public void setStartRow(int startRow) {
        this.startRow = startRow;
    }

    public int getEndRow() {
        return this.endRow;
    }

    public void setEndRow(int endRow) {
        this.endRow = endRow;
    }

    public int getPages() {
        return this.pages;
    }

    public void setPages(int pages) {
        this.pages = pages;
    }

    /** @deprecated */
    @Deprecated
    public int getFirstPage() {
        return this.navigateFirstPage;
    }

    /** @deprecated */
    @Deprecated
    public void setFirstPage(int firstPage) {
        this.navigateFirstPage = firstPage;
    }

    public int getPrePage() {
        return this.prePage;
    }

    public void setPrePage(int prePage) {
        this.prePage = prePage;
    }

    public int getNextPage() {
        return this.nextPage;
    }

    public void setNextPage(int nextPage) {
        this.nextPage = nextPage;
    }

    /** @deprecated */
    @Deprecated
    public int getLastPage() {
        return this.navigateLastPage;
    }

    /** @deprecated */
    @Deprecated
    public void setLastPage(int lastPage) {
        this.navigateLastPage = lastPage;
    }

    public boolean isIsFirstPage() {
        return this.isFirstPage;
    }

    public void setIsFirstPage(boolean isFirstPage) {
        this.isFirstPage = isFirstPage;
    }

    public boolean isIsLastPage() {
        return this.isLastPage;
    }

    public void setIsLastPage(boolean isLastPage) {
        this.isLastPage = isLastPage;
    }

    public boolean isHasPreviousPage() {
        return this.hasPreviousPage;
    }

    public void setHasPreviousPage(boolean hasPreviousPage) {
        this.hasPreviousPage = hasPreviousPage;
    }

    public boolean isHasNextPage() {
        return this.hasNextPage;
    }

    public void setHasNextPage(boolean hasNextPage) {
        this.hasNextPage = hasNextPage;
    }

    public int getNavigatePages() {
        return this.navigatePages;
    }

    public void setNavigatePages(int navigatePages) {
        this.navigatePages = navigatePages;
    }

    public int[] getNavigatepageNums() {
        return this.navigatepageNums;
    }

    public void setNavigatepageNums(int[] navigatepageNums) {
        this.navigatepageNums = navigatepageNums;
    }

    public int getNavigateFirstPage() {
        return this.navigateFirstPage;
    }

    public int getNavigateLastPage() {
        return this.navigateLastPage;
    }

    public void setNavigateFirstPage(int navigateFirstPage) {
        this.navigateFirstPage = navigateFirstPage;
    }

    public void setNavigateLastPage(int navigateLastPage) {
        this.navigateLastPage = navigateLastPage;
    }

    public String toString() {
        StringBuilder sb = new StringBuilder("PageInfo{");
        sb.append("pageNum=").append(this.pageNum);
        sb.append(", pageSize=").append(this.pageSize);
        sb.append(", size=").append(this.size);
        sb.append(", startRow=").append(this.startRow);
        sb.append(", endRow=").append(this.endRow);
        sb.append(", total=").append(this.total);
        sb.append(", pages=").append(this.pages);
        sb.append(", list=").append(this.list);
        sb.append(", prePage=").append(this.prePage);
        sb.append(", nextPage=").append(this.nextPage);
        sb.append(", isFirstPage=").append(this.isFirstPage);
        sb.append(", isLastPage=").append(this.isLastPage);
        sb.append(", hasPreviousPage=").append(this.hasPreviousPage);
        sb.append(", hasNextPage=").append(this.hasNextPage);
        sb.append(", navigatePages=").append(this.navigatePages);
        sb.append(", navigateFirstPage=").append(this.navigateFirstPage);
        sb.append(", navigateLastPage=").append(this.navigateLastPage);
        sb.append(", navigatepageNums=");
        if (this.navigatepageNums == null) {
            sb.append("null");
        } else {
            sb.append('[');

            for(int i = 0; i < this.navigatepageNums.length; ++i) {
                sb.append(i == 0 ? "" : ", ").append(this.navigatepageNums[i]);
            }

            sb.append(']');
        }

        sb.append('}');
        return sb.toString();
    }
}

继承了PageSerializable:
com.github.pagehelper.PageSerializable

package com.github.pagehelper;

import java.io.Serializable;
import java.util.List;

public class PageSerializable<T> implements Serializable {
    private static final long serialVersionUID = 1L;
    protected long total;
    protected List<T> list;

    public PageSerializable() {
    }

    public PageSerializable(List<T> list) {
        this.list = list;
        if (list instanceof Page) {
            this.total = ((Page)list).getTotal();
        } else {
            this.total = (long)list.size();
        }

    }

    public static <T> PageSerializable<T> of(List<T> list) {
        return new PageSerializable(list);
    }

    public long getTotal() {
        return this.total;
    }

    public void setTotal(long total) {
        this.total = total;
    }

    public List<T> getList() {
        return this.list;
    }

    public void setList(List<T> list) {
        this.list = list;
    }

    public String toString() {
        return "PageSerializable{total=" + this.total + ", list=" + this.list + '}';
    }
}

它有如下特点:
(1)对pageHelper拦截后的原生返回List进行再次包装,前面说过,pageHelper返回后的原生list实际上类型是Page类型。
(2)存在total属性,表示总数。
(3)存在pageNum,表示当前页面索引。
(4)存在pageSize,表示当前页面实际返回的页面条数。
(5)存在size,表示当前传入的List的实际元素数量。
(6)存在startRow,表示开始行。
(7)存在endRow,表示结束行。
……..

我们一般使用PageInfo对pageHelper的操作结果再次进行封装,因为它根据实际的业务需求提供了更多的分页属性。
但在实际操作中,使用它往往会出现一些奇怪的结果。
下面我们看几个怪异的案例。

5.2 对PageInfo做加法和减法

5.2.1 对PageInfo做加法

使用PageInfo对SQL的查询结果集进行包装。
再强调一次,PageHelper只能拦截一个sql语句,如果存在多个sql执行,需要在每个sql执行前加上PageHelper进行拦截分页。

该案例尝试对mybatis查询的原始结果集做加法,即对pageHelper查询的结果集做加法。

    @Test
    public void testPageHelperByByJiaFa() throws Exception {
        IPageHelperDao<CommonPo> pageHelperDao = new PageHelperDaoImpl(sqlSessionFactory);
        PageHelper.startPage(1, 5, Boolean.TRUE);
        List<CommonPo> list = pageHelperDao.findUserByIdForPageHelper(null);
        System.out.println("封装前的 list:" + list);
        System.out.println("封装前的 list 的 size:" + list.size());
        System.out.println("封装前的 list 的 pageSize:" + ((Page) list).getPageSize());
        System.out.println("封装前的 list 的 total:" + ((Page) list).getTotal());
        
        list.add(new CommonPo());//手动改装 list,进行加法,观察pageInfo的总数信息。
        list.add(new CommonPo());
        list.add(new CommonPo());

        //注意,PageInfo比Page作用更强大,都是pageHelper包提供的结果集封装对象。
        PageInfo<CommonPo> pageInfo = new PageInfo(list);
        System.out.println("封装list对象后的pageInfo总数:" + pageInfo.getTotal());
        System.out.println("封装list对象后的pageInfo size:" + pageInfo.getSize());
        System.out.println("封装list对象后的pageInfo pageSize:" + pageInfo.getPageSize());
    }

日志:

==>  Preparing: SELECT count(0) FROM zeh_user WHERE 1 = 1 
==> Parameters: 
<==    Columns: COUNT(0)
<==        Row: 101
<==      Total: 1
Cache Hit Ratio [pagehelper]: 0.0
==>  Preparing: SELECT * FROM ( SELECT TMP_PAGE.*, ROWNUM ROW_ID FROM ( select * from zeh_user where 1=1 ) TMP_PAGE WHERE ROWNUM <= ? ) WHERE ROW_ID > ? 
==> Parameters: 5(Integer), 0(Integer)
<==    Columns: ID, NAME, AGE, SEX, HOBBY, ROW_ID
<==        Row: 4, 更新, 29, 不男不女, 吃饭喝酒, 1
<==        Row: 8, getMyName, 27, 不男不女, 唱歌、写作、编程, 2
<==        Row: 8, getMyName, 27, 不男不女, 唱歌、写作、编程, 3
<==        Row: 8, getMyName, 27, 不男不女, 唱歌、写作、编程, 4
<==        Row: 8, getMyName, 27, 不男不女, 唱歌、写作、编程, 5
<==      Total: 5
Resetting autocommit to true on JDBC Connection [oracle.jdbc.driver.T4CConnection@1ed1993a]
Closing JDBC Connection [oracle.jdbc.driver.T4CConnection@1ed1993a]
Returned connection 517052730 to pool.
封装前的 list:Page{count=true, pageNum=1, pageSize=5, startRow=0, endRow=5, total=101, pages=21, reasonable=false, pageSizeZero=false}[[id = 4;name = 更新;age = 29;sex = 不男不女;hobby = 吃饭喝酒], [id = 8;name = getMyName;age = 27;sex = 不男不女;hobby = 唱歌、写作、编程], [id = 8;name = getMyName;age = 27;sex = 不男不女;hobby = 唱歌、写作、编程], [id = 8;name = getMyName;age = 27;sex = 不男不女;hobby = 唱歌、写作、编程], [id = 8;name = getMyName;age = 27;sex = 不男不女;hobby = 唱歌、写作、编程]]
封装前的 list 的 size:5
封装前的 list 的 pageSize:5
封装前的 list 的 total:101
封装list对象后的pageInfo总数:101
封装list对象后的pageInfo size:8
封装list对象后的pageInfo pageSize:5

从日志来看:
(1)在查询SQL默认查询了SELECT COUNT(0)总数,发现符合条件的总数有101条。
(2)pageHelper拦截器生效了,当前查询只查出了5条数据,即分页后的total是5。
(3)使用PageHelper后,实际上返回的List本身是个Page对象,通过拦截器将原本返回的List对象给强转成了Page对象了。并且原生的Page对象里面的total是不参与分页的总数,pageSize是当前实际分页后返回的总数。
(4)对原始结果集做加法,通过转换成PageInfo后,其total永远是count(0)的总数,即不参与分页的全数量;而size就是当前分页后实际返回的数量。
(5)改造完原生List后,向原生List手动添加了几个元素,然后通过PageInfo进行再次封装,发现,PageInfo的size变成了改造后的list的实际数量了。
(6)改造前后,pageSize的值没有发生变化。

猜想:难道是只对结果集做加法,则pageInfo中的total永远是count(1)查询出来的总数,而不是做完加法后的真正总数 ?

5.2.2 对PageInfo做减法

对pageHelper查询的结果集做减法。

    @Test
    public void testPageHelperByJianFa() throws Exception {
        IPageHelperDao<CommonPo> pageHelperDao = new PageHelperDaoImpl(sqlSessionFactory);
        PageHelper.startPage(1, 5, Boolean.TRUE);
        List<CommonPo> list = pageHelperDao.findUserByIdForPageHelper(null);
        System.out.println("封装前的 list:" + list);
        System.out.println("封装前的 list 的 size:" + list.size());
        System.out.println("封装前的 list 的 pageSize:" + ((Page) list).getPageSize());
        System.out.println("封装前的 list 的 total:" + ((Page) list).getTotal());

        list.add(new CommonPo());//手动改装 list,进行加法,观察pageInfo的总数信息。
        list.add(new CommonPo());
        list.add(new CommonPo());

        list = list.stream().filter(e -> e.getId() == 8).collect(Collectors.toList());
        //做完减法再做加法
        list.add(new CommonPo());//手动改装list1,进行加法或者减法,观察pageInfo的总数信息。
        list.add(new CommonPo());
        list.add(new CommonPo());

        //注意,PageInfo比Page作用更强大,都是pageHelper包提供的结果集封装对象。
        PageInfo<CommonPo> pageInfo = new PageInfo(list);
        System.out.println("封装list对象后的pageInfo总数:" + pageInfo.getTotal());
        System.out.println("封装list对象后的pageInfo size:" + pageInfo.getSize());
        System.out.println("封装list对象后的pageInfo pageSize:" + pageInfo.getPageSize());
    }

日志:

==>  Preparing: SELECT count(0) FROM zeh_user WHERE 1 = 1 
==> Parameters: 
<==    Columns: COUNT(0)
<==        Row: 101
<==      Total: 1
Cache Hit Ratio [pagehelper]: 0.0
==>  Preparing: SELECT * FROM ( SELECT TMP_PAGE.*, ROWNUM ROW_ID FROM ( select * from zeh_user where 1=1 ) TMP_PAGE WHERE ROWNUM <= ? ) WHERE ROW_ID > ? 
==> Parameters: 5(Integer), 0(Integer)
<==    Columns: ID, NAME, AGE, SEX, HOBBY, ROW_ID
<==        Row: 4, 更新, 29, 不男不女, 吃饭喝酒, 1
<==        Row: 8, getMyName, 27, 不男不女, 唱歌、写作、编程, 2
<==        Row: 8, getMyName, 27, 不男不女, 唱歌、写作、编程, 3
<==        Row: 8, getMyName, 27, 不男不女, 唱歌、写作、编程, 4
<==        Row: 8, getMyName, 27, 不男不女, 唱歌、写作、编程, 5
<==      Total: 5
Resetting autocommit to true on JDBC Connection [oracle.jdbc.driver.T4CConnection@1ed1993a]
Closing JDBC Connection [oracle.jdbc.driver.T4CConnection@1ed1993a]
Returned connection 517052730 to pool.
封装前的 list:Page{count=true, pageNum=1, pageSize=5, startRow=0, endRow=5, total=101, pages=21, reasonable=false, pageSizeZero=false}[[id = 4;name = 更新;age = 29;sex = 不男不女;hobby = 吃饭喝酒], [id = 8;name = getMyName;age = 27;sex = 不男不女;hobby = 唱歌、写作、编程], [id = 8;name = getMyName;age = 27;sex = 不男不女;hobby = 唱歌、写作、编程], [id = 8;name = getMyName;age = 27;sex = 不男不女;hobby = 唱歌、写作、编程], [id = 8;name = getMyName;age = 27;sex = 不男不女;hobby = 唱歌、写作、编程]]
封装前的 list 的 size:5
封装前的 list 的 pageSize:5
封装前的 list 的 total:101
封装list对象后的pageInfo总数:7
封装list对象后的pageInfo size:7
封装list对象后的pageInfo pageSize:7

从日志上分析如下:
如果对list做过滤操作,然后通过pageInfo对象包装,则pageInfo中的属性是完全按照做减法后的list数量来的,即做加法总数始终是count(0),而做减法总数就是实际上转换的list数量。
只要查询的list之后做了过滤(不论在哪一步做减法),则最后封装的pageInfo对象中的total就和list是一致绑定的。
而且size和pageSize也都变为list的真实数量了。

这样导致的问题是:
本来我们向前端进行分页返回时,每次都需要把数据库全量的总数total,和当前分页后的数据给返回。
但是,对分页后的数据手动做了减法后,则数据库原始的count(1)将不会再绑定到pageInfo的total上,而变成了我们改造后的实际List的数量。
这样一来,客户端实际上每次查询时,页面选择了多少条记录,则total总是查询出来的总条数,导致没有下一页。
比如当前请求页面是要查50条数据,那么后端返回的数据经过改造后可能是48条,然后total也是48条,这样前端认为总数就是48条,就不展示下一页了。
但实际上总数可能还很多,只是因为我们手动做了减法,导致返回的total不再是数据库中真实的总数。

5.2.3 原因分析

为什么上面同样是修改原始返回的lit对象,然后使用PageInfo再次包装,结果包装后的total竟然会出现完全不同的结果?

从测试代码中可看到,我们有如下代码:

PageInfo<CommonPo> pageInfo = new PageInfo(list);

所以,我们有必要看看PageInfo的构造器:

    public PageInfo(List<T> list) {
        this(list, 8);
    }

    public PageInfo(List<T> list, int navigatePages) {
        super(list);
        this.isFirstPage = false;
        this.isLastPage = false;
        this.hasPreviousPage = false;
        this.hasNextPage = false;
        if (list instanceof Page) {
            Page page = (Page)list;
            this.pageNum = page.getPageNum();
            this.pageSize = page.getPageSize();
            this.pages = page.getPages();
            this.size = page.size();
            if (this.size == 0) {
                this.startRow = 0;
                this.endRow = 0;
            } else {
                this.startRow = page.getStartRow() + 1;
                this.endRow = this.startRow - 1 + this.size;
            }
        } else if (list instanceof Collection) {
            this.pageNum = 1;
            this.pageSize = list.size();
            this.pages = this.pageSize > 0 ? 1 : 0;
            this.size = list.size();
            this.startRow = 0;
            this.endRow = list.size() > 0 ? list.size() - 1 : 0;
        }

        if (list instanceof Collection) {
            this.navigatePages = navigatePages;
            this.calcNavigatepageNums();
            this.calcPage();
            this.judgePageBoudary();
        }
    }

点进super(list):

package com.github.pagehelper;

import java.io.Serializable;
import java.util.List;

public class PageSerializable<T> implements Serializable {
    private static final long serialVersionUID = 1L;
    protected long total;
    protected List<T> list;

    public PageSerializable() {
    }

    // 重点
    public PageSerializable(List<T> list) {
        this.list = list;
        if (list instanceof Page) {
            this.total = ((Page)list).getTotal();
        } else {
            this.total = (long)list.size();
        }

    }
}

我们重点关注上面的total、size、pageSize这三个变量的逻辑。
(1)当list还是Page类型时,total、size、pageSize都是从Page中获取的,此时的值实际上就是SQL真实返回后封装在page对象中的属性值,这种情况是符合预期的。
(2)当我们改造后,传入给PageInfo的list的类型不再是Page类型时,total、size、pageSize的值,实际上都是取自传入的list的真正size值。

很清楚了,其实total的值,到底是取原始返回的page对象中的原本就从SQL中真正查询出来的total,还是取我们改造后的list的真正数量,实际上有逻辑判断的。
即当我们传入的List对象,如果还是Page类型的话,那么就取这个对象里面的total,即从SQL里面真实返回的count(0)的值;
否则,取传入的List对象真正的size。

    public PageSerializable(List<T> list) {
        this.list = list;
        if (list instanceof Page) {
            this.total = ((Page)list).getTotal();
        } else {
            this.total = (long)list.size();
        }

    }

上面的两个案例的真正原因:
并不是说我们对原始list做加法还是做减法导致total、size、pageSize变化的问题,而是要看我们在操作list对象时,有没有改变其类型,即有没有将其类型从原本的Page类型又转换回了List类型了。

因为在 5.2.2 案例中,我们在做减法时,有如下代码:

list = list.stream().filter(e -> e.getId() == 8).collect(Collectors.toList());

这个代码实际上已经将原生返回的List类型从Page子类型又自动转换为ArrayList父类型了。
因此才有了上面比较奇怪的操作。

5.3.4 pageHelper返回的原始类型不可以强转为PageInfo

其实从上面的源码已经清楚了,PageInfo是独立的一个类,它持有了page对象。
而使用pageHelper拦截SQL后之所以能直接将返回的List对象强转为Page对象,是因为它通过代理操作完SQL,返回的那个结果集本身其实就是Page类型的,因此可以将List强转为它本身就存在的类型Page。
而要将结果直接转换为PageInfo,即将Page强转为PageInfo,类型是不匹配的,会报错。

    @Test
    public void testPageHelperByPageDiffPageInfo() throws Exception {
        IPageHelperDao<CommonPo> pageHelperDao = new PageHelperDaoImpl(sqlSessionFactory);
        PageHelper.startPage(1, 5, Boolean.TRUE);
        PageInfo<CommonPo> pageInfo = (PageInfo<CommonPo>) pageHelperDao.findUserByIdForPageHelper(null);
        System.out.println("pageInfo 总数:" + pageInfo.getTotal());
    }

日志:

==>  Preparing: SELECT count(0) FROM zeh_user WHERE 1 = 1 
==> Parameters: 
<==    Columns: COUNT(0)
<==        Row: 101
<==      Total: 1
Cache Hit Ratio [pagehelper]: 0.0
==>  Preparing: SELECT * FROM ( SELECT TMP_PAGE.*, ROWNUM ROW_ID FROM ( select * from zeh_user where 1=1 ) TMP_PAGE WHERE ROWNUM <= ? ) WHERE ROW_ID > ? 
==> Parameters: 5(Integer), 0(Integer)
<==    Columns: ID, NAME, AGE, SEX, HOBBY, ROW_ID
<==        Row: 4, 更新, 29, 不男不女, 吃饭喝酒, 1
<==        Row: 8, getMyName, 27, 不男不女, 唱歌、写作、编程, 2
<==        Row: 8, getMyName, 27, 不男不女, 唱歌、写作、编程, 3
<==        Row: 8, getMyName, 27, 不男不女, 唱歌、写作、编程, 4
<==        Row: 8, getMyName, 27, 不男不女, 唱歌、写作、编程, 5
<==      Total: 5
Resetting autocommit to true on JDBC Connection [oracle.jdbc.driver.T4CConnection@1ed1993a]
Closing JDBC Connection [oracle.jdbc.driver.T4CConnection@1ed1993a]
Returned connection 517052730 to pool.

java.lang.ClassCastException: com.github.pagehelper.Page cannot be cast to com.github.pagehelper.PageInfo

	at zeh.mybatis.code03yuanshi.run.PageHelperDaoRun.testPageHelperByPageDiffPageInfo(PageHelperDaoRun.java:232)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)
	at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
	at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
	at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
	at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
	at org.junit.internal.runners.statements.RunBefores.evaluate(RunBefores.java:26)
	at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
	at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:78)
	at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:57)
	at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
	at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
	at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
	at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
	at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
	at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
	at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
	at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:68)
	at com.intellij.rt.execution.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:47)
	at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:242)
	at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:70)

5.3.5 直接将结果集手动set到PageInfo中

    @Test
    public void testPageHelperBySetList() throws Exception {
        IPageHelperDao<CommonPo> pageHelperDao = new PageHelperDaoImpl(sqlSessionFactory);
        PageHelper.startPage(1, 5, Boolean.TRUE);
        List list = pageHelperDao.findUserByIdForPageHelper(null);
        System.out.println("封装前的 list:" + list);
        System.out.println("封装前的 list 的 size:" + list.size());
        System.out.println("封装前的 list 的 pageSize:" + ((Page) list).getPageSize());
        System.out.println("封装前的 list 的 total:" + ((Page) list).getTotal());

        PageInfo pageInfo = new PageInfo();
        pageInfo.setList(list);
        System.out.println("封装list对象后的pageInfo总数:" + pageInfo.getTotal());
        System.out.println("封装list对象后的pageInfo size:" + pageInfo.getSize());
        System.out.println("封装list对象后的pageInfo pageSize:" + pageInfo.getPageSize());
    }

日志如下:

==>  Preparing: SELECT count(0) FROM zeh_user WHERE 1 = 1 
==> Parameters: 
<==    Columns: COUNT(0)
<==        Row: 101
<==      Total: 1
Cache Hit Ratio [pagehelper]: 0.0
==>  Preparing: SELECT * FROM ( SELECT TMP_PAGE.*, ROWNUM ROW_ID FROM ( select * from zeh_user where 1=1 ) TMP_PAGE WHERE ROWNUM <= ? ) WHERE ROW_ID > ? 
==> Parameters: 5(Integer), 0(Integer)
<==    Columns: ID, NAME, AGE, SEX, HOBBY, ROW_ID
<==        Row: 4, 更新, 29, 不男不女, 吃饭喝酒, 1
<==        Row: 8, getMyName, 27, 不男不女, 唱歌、写作、编程, 2
<==        Row: 8, getMyName, 27, 不男不女, 唱歌、写作、编程, 3
<==        Row: 8, getMyName, 27, 不男不女, 唱歌、写作、编程, 4
<==        Row: 8, getMyName, 27, 不男不女, 唱歌、写作、编程, 5
<==      Total: 5
Resetting autocommit to true on JDBC Connection [oracle.jdbc.driver.T4CConnection@1ed1993a]
Closing JDBC Connection [oracle.jdbc.driver.T4CConnection@1ed1993a]
Returned connection 517052730 to pool.
封装前的 list:Page{count=true, pageNum=1, pageSize=5, startRow=0, endRow=5, total=101, pages=21, reasonable=false, pageSizeZero=false}[[id = 4;name = 更新;age = 29;sex = 不男不女;hobby = 吃饭喝酒], [id = 8;name = getMyName;age = 27;sex = 不男不女;hobby = 唱歌、写作、编程], [id = 8;name = getMyName;age = 27;sex = 不男不女;hobby = 唱歌、写作、编程], [id = 8;name = getMyName;age = 27;sex = 不男不女;hobby = 唱歌、写作、编程], [id = 8;name = getMyName;age = 27;sex = 不男不女;hobby = 唱歌、写作、编程]]
封装前的 list 的 size:5
封装前的 list 的 pageSize:5
封装前的 list 的 total:101
封装list对象后的pageInfo总数:0
封装list对象后的pageInfo size:0
封装list对象后的pageInfo pageSize:0

手动setList后,发现结果全部是0.
其原因是,手动set list时,PageInfo对象使用了无参构造器:

    public PageInfo() {
        this.isFirstPage = false;
        this.isLastPage = false;
        this.hasPreviousPage = false;
        this.hasNextPage = false;
    }

而这样产生出来的PageInfo对象里面是没有那些属性值的,尽管手动将list设置进去了,但是并没有执行那些属性的操作逻辑。
因此,要使用PageInfo,只能通过构造器传参的方式,直接将list传递进去。
因此从上面的源码也看到了,构造器传参后,实际上构造器内部做了很多初始化逻辑。

5.3.6 先通过PageInfo构造器封装真正的list,再手动set一个list

    @Test
    public void testPageHelperByGouZaoAndSetList() throws Exception {
        IPageHelperDao<CommonPo> pageHelperDao = new PageHelperDaoImpl(sqlSessionFactory);
        PageHelper.startPage(1, 5, Boolean.TRUE);
        List<CommonPo> list = pageHelperDao.findUserByIdForPageHelper(null);
        System.out.println("封装前的 list:" + list);
        System.out.println("封装前的 list 的 size:" + list.size());
        System.out.println("封装前的 list 的 pageSize:" + ((Page) list).getPageSize());
        System.out.println("封装前的 list 的 total:" + ((Page) list).getTotal());

        //先使用带参数的PageInfo进行构造,再手动set List。
        PageInfo pageInfo = new PageInfo(list);
        List<CommonPo> addList = new ArrayList<>();
        addList.add(new CommonPo());
        addList.add(new CommonPo());
        addList.add(new CommonPo());
        pageInfo.setList(addList);
        System.out.println("封装list对象后的pageInfo总数:" + pageInfo.getTotal());
        System.out.println("封装list对象后的pageInfo size:" + pageInfo.getSize());
        System.out.println("封装list对象后的pageInfo pageSize:" + pageInfo.getPageSize());
    }

日志:

==>  Preparing: SELECT count(0) FROM zeh_user WHERE 1 = 1 
==> Parameters: 
<==    Columns: COUNT(0)
<==        Row: 101
<==      Total: 1
Cache Hit Ratio [pagehelper]: 0.0
==>  Preparing: SELECT * FROM ( SELECT TMP_PAGE.*, ROWNUM ROW_ID FROM ( select * from zeh_user where 1=1 ) TMP_PAGE WHERE ROWNUM <= ? ) WHERE ROW_ID > ? 
==> Parameters: 5(Integer), 0(Integer)
<==    Columns: ID, NAME, AGE, SEX, HOBBY, ROW_ID
<==        Row: 4, 更新, 29, 不男不女, 吃饭喝酒, 1
<==        Row: 8, getMyName, 27, 不男不女, 唱歌、写作、编程, 2
<==        Row: 8, getMyName, 27, 不男不女, 唱歌、写作、编程, 3
<==        Row: 8, getMyName, 27, 不男不女, 唱歌、写作、编程, 4
<==        Row: 8, getMyName, 27, 不男不女, 唱歌、写作、编程, 5
<==      Total: 5
Resetting autocommit to true on JDBC Connection [oracle.jdbc.driver.T4CConnection@1ed1993a]
Closing JDBC Connection [oracle.jdbc.driver.T4CConnection@1ed1993a]
Returned connection 517052730 to pool.
封装前的 list:Page{count=true, pageNum=1, pageSize=5, startRow=0, endRow=5, total=101, pages=21, reasonable=false, pageSizeZero=false}[[id = 4;name = 更新;age = 29;sex = 不男不女;hobby = 吃饭喝酒], [id = 8;name = getMyName;age = 27;sex = 不男不女;hobby = 唱歌、写作、编程], [id = 8;name = getMyName;age = 27;sex = 不男不女;hobby = 唱歌、写作、编程], [id = 8;name = getMyName;age = 27;sex = 不男不女;hobby = 唱歌、写作、编程], [id = 8;name = getMyName;age = 27;sex = 不男不女;hobby = 唱歌、写作、编程]]
封装前的 list 的 size:5
封装前的 list 的 pageSize:5
封装前的 list 的 total:101
封装list对象后的pageInfo总数:101
封装list对象后的pageInfo size:5
封装list对象后的pageInfo pageSize:5

使用pageInfo包装结果集,先使用 PageInfo 构造对结果集进行包装,然后再手动 setList (set一个新的list)。
只要使用了PageInfo的有参构造器将list传递进去了,就不影响其各个属性值的正确性。
手动set一个list进去,除了会覆盖原有的list属性后,对其他的属性毫无影响,因为其他的属性已经在执行有参构造时进行初始化了。

5.3.7 先通过PageInfo构造器封装真正的list,取出来修改后,再手动set进去

    @Test
    public void testPageHelperByGetListAndSetList() throws Exception {
        IPageHelperDao<CommonPo> pageHelperDao = new PageHelperDaoImpl(sqlSessionFactory);
        PageHelper.startPage(1, 5, Boolean.TRUE);
        List list = pageHelperDao.findUserByIdForPageHelper(null);
        System.out.println("封装前的 list:" + list);
        System.out.println("封装前的 list 的 size:" + list.size());
        System.out.println("封装前的 list 的 pageSize:" + ((Page) list).getPageSize());
        System.out.println("封装前的 list 的 total:" + ((Page) list).getTotal());

        //构造方法包装list
        PageInfo pageInfo = new PageInfo(list);
        //包装后获取list
        List<CommonPo> getList = pageInfo.getList();
        //修改获取出的list
        getList.add(new CommonPo());
        getList.add(new CommonPo());
        getList.add(new CommonPo());
        getList.add(new CommonPo());
        getList.add(new CommonPo());
        getList.add(new CommonPo());
        getList.add(new CommonPo());
        getList.add(new CommonPo());
        getList.add(new CommonPo());
        //重新手动setList
        pageInfo.setList(list);
        System.out.println("封装list对象后的pageInfo总数:" + pageInfo.getTotal());
        System.out.println("封装list对象后的pageInfo size:" + pageInfo.getSize());
        System.out.println("封装list对象后的pageInfo pageSize:" + pageInfo.getPageSize());
    }

日志:

==>  Preparing: SELECT count(0) FROM zeh_user WHERE 1 = 1 
==> Parameters: 
<==    Columns: COUNT(0)
<==        Row: 101
<==      Total: 1
Cache Hit Ratio [pagehelper]: 0.0
==>  Preparing: SELECT * FROM ( SELECT TMP_PAGE.*, ROWNUM ROW_ID FROM ( select * from zeh_user where 1=1 ) TMP_PAGE WHERE ROWNUM <= ? ) WHERE ROW_ID > ? 
==> Parameters: 5(Integer), 0(Integer)
<==    Columns: ID, NAME, AGE, SEX, HOBBY, ROW_ID
<==        Row: 4, 更新, 29, 不男不女, 吃饭喝酒, 1
<==        Row: 8, getMyName, 27, 不男不女, 唱歌、写作、编程, 2
<==        Row: 8, getMyName, 27, 不男不女, 唱歌、写作、编程, 3
<==        Row: 8, getMyName, 27, 不男不女, 唱歌、写作、编程, 4
<==        Row: 8, getMyName, 27, 不男不女, 唱歌、写作、编程, 5
<==      Total: 5
Resetting autocommit to true on JDBC Connection [oracle.jdbc.driver.T4CConnection@1ed1993a]
Closing JDBC Connection [oracle.jdbc.driver.T4CConnection@1ed1993a]
Returned connection 517052730 to pool.
封装前的 list:Page{count=true, pageNum=1, pageSize=5, startRow=0, endRow=5, total=101, pages=21, reasonable=false, pageSizeZero=false}[[id = 4;name = 更新;age = 29;sex = 不男不女;hobby = 吃饭喝酒], [id = 8;name = getMyName;age = 27;sex = 不男不女;hobby = 唱歌、写作、编程], [id = 8;name = getMyName;age = 27;sex = 不男不女;hobby = 唱歌、写作、编程], [id = 8;name = getMyName;age = 27;sex = 不男不女;hobby = 唱歌、写作、编程], [id = 8;name = getMyName;age = 27;sex = 不男不女;hobby = 唱歌、写作、编程]]
封装前的 list 的 size:5
封装前的 list 的 pageSize:5
封装前的 list 的 total:101
封装list对象后的pageInfo总数:101
封装list对象后的pageInfo size:5
封装list对象后的pageInfo pageSize:5

同上一个案例一样,手动setList不影响原本的各个属性值。

5.4 总结

通过对上面pageHelper物理分页的各个案例分析,基本上可以得出一个结论:
使用pageHelper物理分页完毕,不要对结果集做一些增加操作或者过滤操作,也就是不雅尝试对本次分页结果在业务层做加减法操作。
因为,我们在业务层对本次分页结果集做了加减法操作的目的,无非就是想把我们重新操作后的数据返回给客户端,但这样一来,我们操作后的total要么是原始的结果,要么是我们操作后的list的个数。
而且操作后的当前数据量和Page中原本查出来的当前数据量也不匹配,导致分页错乱。

如果非要对结果集做处理的化,其实最简单的方案就是:
对有逻辑处理的结果集应该尽量编写在同一个sql中,统一交给sql进行逻辑处理,而不要通过应用代码进行处理。
如果实在不行,则需要借助PageInfo对象进行手动分页参数设置了。

5.5 手动组装PageInfo的分页参数

详细解释下加减法操作:
比如本次分页,查询出来的数据是20条,总数有100条,当前页面是3,当前的pageSize是20.
如果在这次分页请求中,我们要对这20条数据手动加上5条,那么按照之前的逻辑,返回给客户端的总数还是100条,当前页面是3,当前的pageSize还是20,但实际上我们返回给客户端的数据是25条。
这样计算下来,当前页面就展示了25条,前端根据25条和total再判断有没有下一页,当达到总数100后,就没有下一页了,但实际上此时数据库真正符合条件的还有数据,因为我们每一页都手动加上了5条数据,相当于凭空多给了5条数据给客户端。
因此,这样一来,分页就出现错乱了。

那么如果我们非要在业务层对结果集进行加减法的话,就不能再使用pageHelper物理分页了。
总结不下去了,想明白了再继续总结吧!!!!!!!!!!!!!!!!!!!

6. pageHelper源码解析

6.1 PageHelper的maven依赖及插件配置

pageHelper是中国团队开发的,本身只是mybatis的一个插件。
maven依赖如下:

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

PageHelper除了本身的jar包外,它还依赖了一个叫jsqlparser的jar包,使用时,我们不需要单独指定jsqlparser的maven依赖,maven的间接依赖会帮我们引入。

pageHelper作为Mybatis的插件来使用时,官方提供了配置的说明,这些配置需要定义在mybatis的全局配置的节点中,才能对插件生效。

<!-- 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="true" />

    <!-- 默认为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;"/> -->
</plugin>

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

dialect:标识是哪一种数据库,设计上必须。   
offsetAsPageNum:将RowBounds第一个参数offset当成pageNum页码使用,这就是上面说的一参两用,个人觉得完全没必要,offset = pageSize * pageNum就搞定了,何必混用参数呢?   
rowBoundsWithCount:设置为true时,使用RowBounds分页会进行count查询,个人觉得完全没必要,实际开发中,每一个列表分页查询,都配备一个count数量查询即可。
reasonable:value=true时,pageNum小于1会查询第一页,如果pageNum大于总页数会查询最后一页 ,个人认为,参数校验在进入Mybatis业务体系之前,就应该完成了,不可能到达Mybatis业务体系内参数还带有非法的值。

这么一来,我们只需要记住 dialect = mysql 一个参数即可,其实,还有下面几个相关参数可以配置。

autoDialect:true or false,是否自动检测dialect。默认为true。
autoRuntimeDialect:true or false,多数据源时,是否自动检测dialect。默认为false。
closeConn:true or false,检测完dialect后,是否关闭Connection连接。默认为true。

上面这3个智能参数,不到万不得已,我们不应该在系统中使用,我们只需要一个dialect = mysql 或者 dialect = oracle就够了,如果系统中需要使用,还是得问问自己,是否真的非用不可。

从上面的配置可以看出,在使用pageHelper时,PageHelper.startPage()的参数应该注意如下:

pageNum:第一个参数,表示当前页;标准应该设置为1,默认情况下如果传入一个小于1的数字,将返回空数据,什么都查不出来。
pageSize:第二个参数,表示需要查询多少条。默认应该大于0,如果传入0或者负数,默认情况下什么都查不出来;并且如果这个值比pageNum小,默认情况下也是什么都查不出来。

6.2 源码解析

6.2.1 拦截器

mybatis提供了拦截器接口:

org.apache.ibatis.plugin.Interceptor

我们只需要在实现类中对拦截对象和方法进行处理,即可以完成mybatis的拦截器。
org.apache.ibatis.plugin.Interceptor源码如下:

package org.apache.ibatis.plugin;

import java.util.Properties;

public interface Interceptor {
    Object intercept(Invocation var1) throws Throwable;

    Object plugin(Object var1);

    void setProperties(Properties var1);
}

6.2.1.1 Object intercept(Invocation invocation)

intercept 是mybatis 运行时要执行的方法。通过该方法的参数invocation 可以得到很多有用的信息,该参数的方法常用如下:

public class ZszInterceptor implements Interceptor {
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        Object target = invocation.getTarget();
        Method method = invocation.getMethod();
        Object[] args = invocation.getArgs();
        Object result = invocation.proceed();
        return result;
    }
}

getTarget():获取当前拦截的对象
getMethod():获取当前被拦截的方法
getArgs():可以返回被拦截方法中的参数
invocation.proceed():proceed() 方法实际上执行了method.invoke(target,args)方法,上面的代码中没有做任何处理,直接返回了结果

6.2.1.2 plugin(Object target)

这个方法的参数target 就是拦截器要拦截的对象,该方法会在创建被拦截的接口实现类时被调用。
听起来有点绕,其实现其实我们只需要调用mybatis 提供的 Plugin.wrap(target, this) 静态方法就可以通过java的动态代理拦截目标对象

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

6.2.1.3 setProperties(Properties properties)

传递插件的参数,可以通过参数来改变插件的行为.
如何配置参数?
我们在配置拦截器的时候,就需要配置:

    <plugins>
        <plugin interceptor="com.github.pagehelper.PageInterceptor">
            <!-- 支持通过Mapper接口参数来传递分页参数 -->
            <property name="helperDialect" value="mysql"/>
            <property name="supportMethodsArguments" value="true"/>
            <property name="rowBoundsWithCount" value="true"/>
        </plugin>
    </plugins>

6.2.2 拦截器签名

@Intercepts 和 注解签名 @Signature 用来配置拦截器要拦截的接口的方法.
@Intercepts 注解中的属性是一个 @Signature 签名数组,可以在同一个拦截器中同时拦截不同的接口和方法.

@Intercepts({@Signature(
    type = Executor.class,
    method = "query",
    args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}
), @Signature(
    type = Executor.class,
    method = "query",
    args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}
)})

@Signature 注解包含以下三个属性. type: 配置拦截器的接口,可选值是:

Executor.class
ParameterHandler.class
ResultSetHandler.class
StatementHandler.class

Method: 设置拦截器中的方法名,可选值是上面4个接口中对应的方法,需要和接口匹配.
例如Executor 中能选query,update等.

public interface Executor {
    ResultHandler NO_RESULT_HANDLER = null;

    int update(MappedStatement var1, Object var2) throws SQLException;

    <E> List<E> query(MappedStatement var1, Object var2, RowBounds var3, ResultHandler var4, CacheKey var5, BoundSql var6) throws SQLException;

    <E> List<E> query(MappedStatement var1, Object var2, RowBounds var3, ResultHandler var4) throws SQLException;

    List<BatchResult> flushStatements() throws SQLException;

    void commit(boolean var1) throws SQLException;

    void rollback(boolean var1) throws SQLException;

    CacheKey createCacheKey(MappedStatement var1, Object var2, RowBounds var3, BoundSql var4);

    boolean isCached(MappedStatement var1, CacheKey var2);

    void clearLocalCache();

    void deferLoad(MappedStatement var1, MetaObject var2, String var3, CacheKey var4, Class<?> var5);

    Transaction getTransaction();

    void close(boolean var1);

    boolean isClosed();

    void setExecutorWrapper(Executor var1);
}

args:设置拦截方法的参数类型数组,通过方法名和参数类型可以确定唯一一个方法.

6.2.2 构造Page对象

startPage()方法核心就是在构建Page对象,然后根据条件初始化其中的各个属性值,最后将其保存在ThreadLocal中,即保存在当前线程中。
Page对象的各个属性值表示了pageHelper的各个配置项,和传入的参数。

    PageHelper.startPage(1, 5, Boolean.TRUE);
    public static <E> Page<E> startPage(int pageNum, int pageSize, boolean count) {
        return startPage(pageNum, pageSize, count, (Boolean)null, (Boolean)null);
    }
    public static <E> Page<E> startPage(int pageNum, int pageSize, boolean count, Boolean reasonable, Boolean pageSizeZero) {
        // 在这里构造Page对象
        Page<E> page = new Page(pageNum, pageSize, count);
        
        // 设置reasonable
        page.setReasonable(reasonable);
        
        // 设置pageSizeZero
        page.setPageSizeZero(pageSizeZero);
        Page<E> oldPage = getLocalPage();
        if (oldPage != null && oldPage.isOrderByOnly()) {
            page.setOrderBy(oldPage.getOrderBy());
        }

        // 将page对象缓存到ThreadLocal中
        setLocalPage(page);
        return page;
    }

构建Page对象的核心代码:

    private Page(int pageNum, int pageSize, boolean count, Boolean reasonable) {
        super(0);
        // count默认设置为true,表示默认会先执行SELECT COUNT(0)语句。
        this.count = true;
        
        // pageNum 为 1 且 pageSize为最大值,将pageSizeZero设置为true。表示查出所有数据。
        if (pageNum == 1 && pageSize == 2147483647) {
            this.pageSizeZero = true;
            // 设置为true后将pageSize设为0
            pageSize = 0;
        }

        // 下面再按个初始化
        this.pageNum = pageNum;
        this.pageSize = pageSize;
        this.count = count;
        
        // 在这个方法里面通过将pageNum和pageSize转换为SQL中通用的startRow和endRow
        // 后面在拦截SQL时,分页参数真正使用的实际上是这个初始化的startRow和endRow
        this.calculateStartAndEndRow();
        
        // 设置reasonable,表示是否需要合理化
        this.setReasonable(reasonable);
    }

calculateStartAndEndRow方法源码:

    private void calculateStartAndEndRow() {
        this.startRow = this.pageNum > 0 ? (this.pageNum - 1) * this.pageSize : 0;
        this.endRow = this.startRow + this.pageSize * (this.pageNum > 0 ? 1 : 0);
    }

SQL中进行物理分页真正使用的分页参数,就是通过上面这个方法转换的。
可以看到:
pageNum小于1,则startRow取值为0;endRow计算出来也是0。
因此,不论使用什么数据库,通过pageHelper设置分页参数时,pageNum的值至少都得是1,才表示查询第一页,不能是0或者负数。

setReasonable方法:

    public Page<E> setReasonable(Boolean reasonable) {
        if (reasonable == null) {
            return this;
        } else {
            this.reasonable = reasonable;
            // reasonable为true并且pageNum小于等于0
            // 手动将pageNum设置为1,并且重新执行calculateStartAndEndRow方法进行startRow和endRow的计算
            if (this.reasonable && this.pageNum <= 0) {
                this.pageNum = 1;
                this.calculateStartAndEndRow();
            }
            return this;
        }
    }

6.2.3 pageHelper实现的拦截器

mybatis对外暴露了插件的机制,如果需要实现自己的插件,都必须要实现mybatis暴露的这个插件接口:

org.apache.ibatis.plugin.Interceptor

它实际上是Mybatis设计的一个拦截器,本质上就是个钩子接口,mybatis在执行的不同时期都会回调这个接口中对应的方法,从而达到拦截的效果。
很显然,pageHelper要实现拦截分页,它也必须实现上面的接口:

@Intercepts({@Signature(
    type = Executor.class,
    method = "query",
    args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}
), @Signature(
    type = Executor.class,
    method = "query",
    args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}
)})
public class PageInterceptor implements Interceptor {
}

注意上面拦截器的注解,可以看到,这个拦截器拦截的对象是Executor,即SQL执行器;拦截的方法是query,即SQL查询;
拦截的方法的参数有两种,一种是:

MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class

另一种是:

MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class

源码如下:

package com.github.pagehelper;

import com.github.pagehelper.cache.Cache;
import com.github.pagehelper.cache.CacheFactory;
import com.github.pagehelper.util.MSUtils;
import com.github.pagehelper.util.StringUtil;
import java.lang.reflect.Field;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import org.apache.ibatis.cache.CacheKey;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Plugin;
import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;

@Intercepts({@Signature(
    type = Executor.class,
    method = "query",
    args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}
), @Signature(
    type = Executor.class,
    method = "query",
    args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}
)})
public class PageInterceptor implements Interceptor {
    protected Cache<String, MappedStatement> msCountMap = null;
    private Dialect dialect;
    private String default_dialect_class = "com.github.pagehelper.PageHelper";
    private Field additionalParametersField;
    private String countSuffix = "_COUNT";

    public PageInterceptor() {
    }

    // mybatis拦截器方法
    public Object intercept(Invocation invocation) throws Throwable {
        Object var22;
        try {
            // 获取拦截参数
            Object[] args = invocation.getArgs();
            MappedStatement ms = (MappedStatement)args[0];
            Object parameter = args[1];
            RowBounds rowBounds = (RowBounds)args[2];
            ResultHandler resultHandler = (ResultHandler)args[3];
            Executor executor = (Executor)invocation.getTarget();
            CacheKey cacheKey;
            BoundSql boundSql;
            if (args.length == 4) {
                // query方法有4个参数时
                boundSql = ms.getBoundSql(parameter);
                // 创建缓存Key
                cacheKey = executor.createCacheKey(ms, parameter, rowBounds, boundSql);
            } else {
                // 6个参数时
                cacheKey = (CacheKey)args[4];
                boundSql = (BoundSql)args[5];
            }

            List resultList;
            // 调用方法判断是否跳过分页,如果跳过(不需要分页),则直接执行普通查询结果,并返回去
            // skip的判断逻辑就是是否有设置pageHelper分页参数,有就不跳过,没有就跳过,请注意,它对逻辑分页rowBounds做了支持
            if (this.dialect.skip(ms, parameter, rowBounds)) {
                // 如果跳过分页即不适用pageHelper时,仍然支持默认的rowBounds内存分页
                resultList = executor.query(ms, parameter, rowBounds, resultHandler, cacheKey, boundSql);
            } else {
                // 否则,执行分页逻辑
                String msId = ms.getId();
                Configuration configuration = ms.getConfiguration();
                Map<String, Object> additionalParameters = (Map)this.additionalParametersField.get(boundSql);
                
                // 判断是否需要在正式执行SQL前进行count查询
                if (this.dialect.beforeCount(ms, parameter, rowBounds)) {
                    String countMsId = msId + this.countSuffix;
                    MappedStatement countMs = this.getExistedMappedStatement(configuration, countMsId);
                    Long count;
                    // 下面这一段,执行count总数查询
                    if (countMs != null) {
                        count = this.executeManualCount(executor, countMs, parameter, boundSql, resultHandler);
                    } else {
                        countMs = (MappedStatement)this.msCountMap.get(countMsId);
                        if (countMs == null) {
                            countMs = MSUtils.newCountMappedStatement(ms, countMsId);
                            this.msCountMap.put(countMsId, countMs);
                        }

                        count = this.executeAutoCount(executor, countMs, parameter, boundSql, rowBounds, resultHandler);
                    }
                    
                    // 处理查询总数,afterCount返回true时表示需要继续分页查询,返回false时,则直接返回
                    if (!this.dialect.afterCount(count, parameter, rowBounds)) {
                        // 当查询总数为0时,直接返回空的查询结果
                        Object var24 = this.dialect.afterPage(new ArrayList(), parameter, rowBounds);
                        return var24;
                    }
                }

                // 判断是否需要进行分页查询
                if (!this.dialect.beforePage(ms, parameter, rowBounds)) {
                    // 没有order by 且 pageSize <= 0时,则不需要进行分页查询
                    resultList = executor.query(ms, parameter, RowBounds.DEFAULT, resultHandler, cacheKey, boundSql);
                } else {
                    // 否则,只要有order by或者pageSize>0就需要进行分页查询
                    // 处理参数对象
                    parameter = this.dialect.processParameterObject(ms, parameter, boundSql, cacheKey);
                    // 调用方言获取分页SQL
                    String pageSql = this.dialect.getPageSql(ms, boundSql, parameter, rowBounds, cacheKey);
                    BoundSql pageBoundSql = new BoundSql(configuration, pageSql, boundSql.getParameterMappings(), parameter);
                    Iterator var17 = additionalParameters.keySet().iterator();

                    while(true) {
                        if (!var17.hasNext()) {
                            resultList = executor.query(ms, parameter, RowBounds.DEFAULT, resultHandler, cacheKey, pageBoundSql);
                            break;
                        }

                        String key = (String)var17.next();
                        pageBoundSql.setAdditionalParameter(key, additionalParameters.get(key));
                    }
                }
            }

            var22 = this.dialect.afterPage(resultList, parameter, rowBounds);
        } finally {
            this.dialect.afterAll();
        }

        return var22;
    }

    private Long executeManualCount(Executor executor, MappedStatement countMs, Object parameter, BoundSql boundSql, ResultHandler resultHandler) throws IllegalAccessException, SQLException {
        CacheKey countKey = executor.createCacheKey(countMs, parameter, RowBounds.DEFAULT, boundSql);
        BoundSql countBoundSql = countMs.getBoundSql(parameter);
        Object countResultList = executor.query(countMs, parameter, RowBounds.DEFAULT, resultHandler, countKey, countBoundSql);
        Long count = ((Number)((List)countResultList).get(0)).longValue();
        return count;
    }

    private Long executeAutoCount(Executor executor, MappedStatement countMs, Object parameter, BoundSql boundSql, RowBounds rowBounds, ResultHandler resultHandler) throws IllegalAccessException, SQLException {
        Map<String, Object> additionalParameters = (Map)this.additionalParametersField.get(boundSql);
        CacheKey countKey = executor.createCacheKey(countMs, parameter, RowBounds.DEFAULT, boundSql);
        String countSql = this.dialect.getCountSql(countMs, boundSql, parameter, rowBounds, countKey);
        BoundSql countBoundSql = new BoundSql(countMs.getConfiguration(), countSql, boundSql.getParameterMappings(), parameter);
        Iterator var11 = additionalParameters.keySet().iterator();

        while(var11.hasNext()) {
            String key = (String)var11.next();
            countBoundSql.setAdditionalParameter(key, additionalParameters.get(key));
        }

        Object countResultList = executor.query(countMs, parameter, RowBounds.DEFAULT, resultHandler, countKey, countBoundSql);
        Long count = (Long)((List)countResultList).get(0);
        return count;
    }

    private MappedStatement getExistedMappedStatement(Configuration configuration, String msId) {
        MappedStatement mappedStatement = null;

        try {
            mappedStatement = configuration.getMappedStatement(msId, false);
        } catch (Throwable var5) {
            ;
        }

        return mappedStatement;
    }

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

    public void setProperties(Properties properties) {
        this.msCountMap = CacheFactory.createCache(properties.getProperty("msCountCache"), "ms", properties);
        String dialectClass = properties.getProperty("dialect");
        if (StringUtil.isEmpty(dialectClass)) {
            dialectClass = this.default_dialect_class;
        }

        try {
            Class<?> aClass = Class.forName(dialectClass);
            this.dialect = (Dialect)aClass.newInstance();
        } catch (Exception var6) {
            throw new PageException(var6);
        }

        this.dialect.setProperties(properties);
        String countSuffix = properties.getProperty("countSuffix");
        if (StringUtil.isNotEmpty(countSuffix)) {
            this.countSuffix = countSuffix;
        }

        try {
            this.additionalParametersField = BoundSql.class.getDeclaredField("additionalParameters");
            this.additionalParametersField.setAccessible(true);
        } catch (NoSuchFieldException var5) {
            throw new PageException(var5);
        }
    }
}

处理参数对象:

public Object processParameterObject(MappedStatement ms, Object parameterObject, BoundSql boundSql, CacheKey pageKey) {
        // 从ThreadLocal中获取之前准备好的Page对象
        Page page = this.getLocalPage();
        if (page.isOrderByOnly()) {
            // 如果仅仅是排序,则返回原始参数
            return parameterObject;
        } else {
            // 封装参数
            Map<String, Object> paramMap = null;
            if (parameterObject == null) {
                // 如果原始参数为null,则new一个
                paramMap = new HashMap();
            } else if (parameterObject instanceof Map) {
                // 如果原始参数为Map类型,则new一个然后将原始参数组装进去
                paramMap = new HashMap();
                paramMap.putAll((Map)parameterObject);
            } else {
                // 其他情况,直接new一个map
                paramMap = new HashMap();
                // 判断是否有类型处理器
                boolean hasTypeHandler = ms.getConfiguration().getTypeHandlerRegistry().hasTypeHandler(parameterObject.getClass());
                MetaObject metaObject = MetaObjectUtil.forObject(parameterObject);
                if (!hasTypeHandler) {
                    String[] var9 = metaObject.getGetterNames();
                    int var10 = var9.length;

                    for(int var11 = 0; var11 < var10; ++var11) {
                        String name = var9[var11];
                        paramMap.put(name, metaObject.getValue(name));
                    }
                }

                if (boundSql.getParameterMappings() != null && boundSql.getParameterMappings().size() > 0) {
                    Iterator var13 = boundSql.getParameterMappings().iterator();

                    ParameterMapping parameterMapping;
                    String name;
                    do {
                        do {
                            do {
                                do {
                                    if (!var13.hasNext()) {
                                        return this.processPageParameter(ms, paramMap, page, boundSql, pageKey);
                                    }

                                    parameterMapping = (ParameterMapping)var13.next();
                                    name = parameterMapping.getProperty();
                                } while(name.equals("First_PageHelper"));
                            } while(name.equals("Second_PageHelper"));
                        } while(paramMap.get(name) != null);
                    } while(!hasTypeHandler && !parameterMapping.getJavaType().equals(parameterObject.getClass()));

                    paramMap.put(name, parameterObject);
                }
            }

            return this.processPageParameter(ms, paramMap, page, boundSql, pageKey);
        }
    }

processPageParameter处理分页参数:

    public Object processPageParameter(MappedStatement ms, Map<String, Object> paramMap, Page page, BoundSql boundSql, CacheKey pageKey) {
        // 组装第一个参数:page对象中的EndRow,具体是多少,需要看前面是如何封装page对象的
        paramMap.put("First_PageHelper", page.getEndRow());
        // 第二个参数:startRow
        paramMap.put("Second_PageHelper", page.getStartRow());
        pageKey.update(page.getEndRow());
        pageKey.update(page.getStartRow());
        if (boundSql.getParameterMappings() != null) {
            List<ParameterMapping> newParameterMappings = new ArrayList();
            if (boundSql != null && boundSql.getParameterMappings() != null) {
                newParameterMappings.addAll(boundSql.getParameterMappings());
            }

            newParameterMappings.add((new Builder(ms.getConfiguration(), "First_PageHelper", Integer.class)).build());
            newParameterMappings.add((new Builder(ms.getConfiguration(), "Second_PageHelper", Integer.class)).build());
            MetaObject metaObject = MetaObjectUtil.forObject(boundSql);
            metaObject.setValue("parameterMappings", newParameterMappings);
        }

        return paramMap;
    }

调用方言获取分页SQL:

    public String getPageSql(MappedStatement ms, BoundSql boundSql, Object parameterObject, RowBounds rowBounds, CacheKey pageKey) {
        String sql = boundSql.getSql();
        Page page = this.getLocalPage();
        String orderBy = page.getOrderBy();
        if (StringUtil.isNotEmpty(orderBy)) {
            pageKey.update(orderBy);
            sql = OrderByParser.converToOrderBySql(sql, orderBy);
        }
        // 如果设置为仅仅排序,则直接返回sql,否则进一步获取分页SQL
        return page.isOrderByOnly() ? sql : this.getPageSql(sql, page, pageKey);
    }

1.String sql = boundSql.getSql() 方言sql
2.其中Page page = this.getLocalPage(); 就是获得分页的参数
3.判断是否需要排序,添加orderby 语句

如果设置了orderBy字段,则转换为orderBy格式的SQL:

    public static String converToOrderBySql(String sql, String orderBy) {
        Statement stmt = null;

        try {
            stmt = CCJSqlParserUtil.parse(sql);
            Select select = (Select)stmt;
            SelectBody selectBody = select.getSelectBody();
            List<OrderByElement> orderByElements = extraOrderBy(selectBody);
            String defaultOrderBy = PlainSelect.orderByToString(orderByElements);
            if (defaultOrderBy.indexOf(63) != -1) {
                throw new RuntimeException("原SQL[" + sql + "]中的order by包含参数,因此不能使用OrderBy插件进行修改!");
            }

            sql = select.toString();
        } catch (Throwable var7) {
            var7.printStackTrace();
        }

        return sql + " order by " + orderBy;
    }

获取分页的SQL,取ORACLE的实现如下:

    public String getPageSql(String sql, Page page, CacheKey pageKey) {
        StringBuilder sqlBuilder = new StringBuilder(sql.length() + 120);
        sqlBuilder.append("SELECT * FROM ( ");
        sqlBuilder.append(" SELECT TMP_PAGE.*, ROWNUM ROW_ID FROM ( ");
        sqlBuilder.append(sql);
        sqlBuilder.append(" ) TMP_PAGE WHERE ROWNUM <= ? ");
        sqlBuilder.append(" ) WHERE ROW_ID > ? ");
        return sqlBuilder.toString();
    }

MySql的实现如下:

    public String getPageSql(String sql, Page page, CacheKey pageKey) {
        StringBuilder sqlBuilder = new StringBuilder(sql.length() + 14);
        sqlBuilder.append(sql);
        if (page.getStartRow() == 0) {
            sqlBuilder.append(" LIMIT ? ");
        } else {
            sqlBuilder.append(" LIMIT ?, ? ");
        }

        pageKey.update(page.getPageSize());
        return sqlBuilder.toString();
    }

6.3 总结

1.pageHelper先根据分页参数和plugin配置以及默认值去组装page对象,然后放在ThreadLocal中。
2.实现mybatis的插件,即拦截器,去拦截query方法。
3.拦截后,在生成SQL后,会从ThreadLocal中获取到page,拿到各种参数值,对SQL进行改装,针对不同的数据源做对应的语法改装。
对ORALCE的分页,使用ROWNUM去改装SQL;
对MySql的分页,使用LIMIT去改装SQL。

7. pageHelper对RowBounds的支持

7.1 pageHelper默认支持RowBounds

在mybatis的全局配置文件中,没有配置pageHelper插件时,我们运行RowBounds逻辑分页,发现它确实是逻辑分页。
现在我们引入了RowBounds,再次运行上面的程序:

    // 检测mybatis自带的逻辑分页功能RowBounds。
    @Test
    public void testFindUserAll() throws Exception {
        IMybatisCacheDao<CommonPo> mybatisCacheDao = new MybatisCacheDaoImpl(sqlSessionFactory);
        List<CommonPo> list = mybatisCacheDao.findUserAll();
        Iterator<CommonPo> iterator = list.iterator();
        System.out.println("全量查询的users表的record如下:");
        while (iterator.hasNext()) {
            System.out.println(iterator.next());
        }
    }

请注意,mybatisCacheDao中的findUserAll方法中设置的RowBounds参数如下:

RowBounds rb = new RowBounds(4, 3);// 设置RowBounds分页查询参数

检查日志:

==>  Preparing: SELECT * FROM ( SELECT TMP_PAGE.*, ROWNUM ROW_ID FROM ( select * from zeh_user ) TMP_PAGE WHERE ROWNUM <= ? ) WHERE ROW_ID > ? 
==> Parameters: 7(Integer), 4(Integer)
<==    Columns: ID, NAME, AGE, SEX, HOBBY, ROW_ID
<==        Row: 8, getMyName, 27, 不男不女, 唱歌、写作、编程, 5
<==        Row: 8, getMyName, 27, 不男不女, 唱歌、写作、编程, 6
<==        Row: 8, getMyName, 27, 不男不女, 唱歌、写作、编程, 7
<==      Total: 3
Resetting autocommit to true on JDBC Connection [oracle.jdbc.driver.T4CConnection@794cb805]
Closing JDBC Connection [oracle.jdbc.driver.T4CConnection@794cb805]
Returned connection 2035070981 to pool.
全量查询的users表的record如下:
[id = 8;name = getMyName;age = 27;sex = 不男不女;hobby = 唱歌、写作、编程]
[id = 8;name = getMyName;age = 27;sex = 不男不女;hobby = 唱歌、写作、编程]
[id = 8;name = getMyName;age = 27;sex = 不男不女;hobby = 唱歌、写作、编程]

很神奇有没有?
我们明明还是使用RowBounds进行逻辑分页,但是从日志上看,却变成了物理分页,而且默认对RowBounds的参数转换成了对应SQL的分页参数。

这是因为PageHelper插件默认对原生RowBounds的逻辑分页做了支持,即只要我们在全局配置中引入了pageHelper的插件,那么当我们使用mybatis自带的RowBounds进行逻辑分页时,pageHelper自动帮我们做了增强,让其变成了对应的物理分页。

7.2 从源码上解读

public Object intercept(Invocation invocation) throws Throwable {
        Object var22;
        try {
            List resultList;
            // 既然使用RowBounds后,它还是进入了物理分页,说明在这里它走了else分支
            if (this.dialect.skip(ms, parameter, rowBounds)) {
                resultList = executor.query(ms, parameter, rowBounds, resultHandler, cacheKey, boundSql);
            } else {
            }
        } finally {
            this.dialect.afterAll();
        }
        return var22;
    }

skip的实现:
com.github.pagehelper.PageHelper

    public boolean skip(MappedStatement ms, Object parameterObject, RowBounds rowBounds) {
        if (ms.getId().endsWith("_COUNT")) {
            throw new RuntimeException("在系统中发现了多个分页插件,请检查系统配置!");
        } else {
            Page page = this.pageParams.getPage(parameterObject, rowBounds);
            if (page == null) {
                return true;
            } else {
                if (StringUtil.isEmpty(page.getCountColumn())) {
                    page.setCountColumn(this.pageParams.getCountColumn());
                }

                this.autoDialect.initDelegateDialect(ms);
                return false;
            }
        }
    }

核心就在“Page page = this.pageParams.getPage(parameterObject, rowBounds);”这一行代码了:
com.github.pagehelper.page.PageParams

    public Page getPage(Object parameterObject, RowBounds rowBounds) {
        // 获取page
        Page page = PageHelper.getLocalPage();
        // page为null,说明没有调用PageHelper.startPage()方法
        if (page == null) {
            // 如果rowBounds参数不是默认的,说明是我们手动设置的RowBounds
            if (rowBounds != RowBounds.DEFAULT) {
                // 下面的逻辑,将rowBounds的offSet和limit参数,重新构建了一个新的page对象,然后返回了
                if (this.offsetAsPageNum) {
                    page = new Page(rowBounds.getOffset(), rowBounds.getLimit(), this.rowBoundsWithCount);
                } else {
                    page = new Page(new int[]{rowBounds.getOffset(), rowBounds.getLimit()}, this.rowBoundsWithCount);
                    page.setReasonable(false);
                }

                if (rowBounds instanceof PageRowBounds) {
                    PageRowBounds pageRowBounds = (PageRowBounds)rowBounds;
                    page.setCount(pageRowBounds.getCount() == null || pageRowBounds.getCount());
                }
            } else if (this.supportMethodsArguments) {
                try {
                    page = PageObjectUtil.getPageFromObject(parameterObject, false);
                } catch (Exception var5) {
                    return null;
                }
            }

            if (page == null) {
                return null;
            }

            PageHelper.setLocalPage(page);
        }

        if (page.getReasonable() == null) {
            page.setReasonable(this.reasonable);
        }

        if (page.getPageSizeZero() == null) {
            page.setPageSizeZero(this.pageSizeZero);
        }

        return page;
    }

7.3 总结

pageHelper对mybatis的原生RowBounds逻辑分页做了增强。
只要mybatis引入了pageHelper拦截器,那么使用原来的RowBounds进行逻辑分页,也会将其转换为对应的物理分页。
当然,从源码也可以看到,如果同时使用了PageHelper.startPage()和RowBounds设置了不同的分页参数,pageHelper会优先使用PageHelper.startPage()中设置的参数。

8. SQL层面的物理分页

从上面的运行日志可以看出来,当我们使用pageHelper插件对mybatis进行物理分页时,它会自动根据数据源的类型,将SQL进行拦截,然后转换成对应的分页SQL。
看一个案例:

    @Test
    public void testPageHelperForCracle() throws Exception {
        IPageHelperDao<CommonPo> pageHelperDao = new PageHelperDaoImpl(sqlSessionFactory);
        PageHelper.startPage(1, 5, Boolean.TRUE);
        pageHelperDao.findUserByIdForPageHelper(null);
    }

观察日志:

==>  Preparing: SELECT count(0) FROM zeh_user WHERE 1 = 1 
==> Parameters: 
<==    Columns: COUNT(0)
<==        Row: 101
<==      Total: 1
Cache Hit Ratio [pagehelper]: 0.0
==>  Preparing: SELECT * FROM ( SELECT TMP_PAGE.*, ROWNUM ROW_ID FROM ( select * from zeh_user where 1=1 ) TMP_PAGE WHERE ROWNUM <= ? ) WHERE ROW_ID > ? 
==> Parameters: 5(Integer), 0(Integer)
<==    Columns: ID, NAME, AGE, SEX, HOBBY, ROW_ID
<==        Row: 4, 更新, 29, 不男不女, 吃饭喝酒, 1
<==        Row: 8, getMyName, 27, 不男不女, 唱歌、写作、编程, 2
<==        Row: 8, getMyName, 27, 不男不女, 唱歌、写作、编程, 3
<==        Row: 8, getMyName, 27, 不男不女, 唱歌、写作、编程, 4
<==        Row: 8, getMyName, 27, 不男不女, 唱歌、写作、编程, 5
<==      Total: 5

我们使用的是ORACLE,看下pageHelper转换后的ORACLE的分页SQL:

SELECT
	* 
FROM
	( SELECT TMP_PAGE.*, ROWNUM ROW_ID FROM ( SELECT * FROM zeh_user WHERE 1 = 1 ) TMP_PAGE WHERE ROWNUM <= (pageNum*pageSize) ) 
WHERE
	ROW_ID > ((pageNum-1)*pageSize)

而这个案例中,pageHelper传入的参数是:

PageHelper.startPage(1, 5, Boolean.TRUE);

其中,pageNum是1,pageSize是5。表示从结果集中,去第一页数据,取5条。
pageHelper会自动将这个参数转换为对应SQL的物理分页参数,即转换为:

pageNum由1转为0;
pageSize由5转为5;

从上面源码也可以看到,其转换算法如下:

   this.startRow = this.pageNum > 0 ? (this.pageNum - 1) * this.pageSize : 0;
   this.endRow = this.startRow + this.pageSize * (this.pageNum > 0 ? 1 : 0);

上面的日志是不排序的SQL,再截取一段pageHelper自动将排序SQL转为为ORACLE:

SELECT
	* 
FROM
	(
SELECT
	TMP_PAGE.*,
	ROWNUM ROW_ID 
FROM
	( SELECT * FROM zeh_user WHERE 1 = 1 AND id = ? ORDER BY NAME ) TMP_PAGE 
WHERE
	ROWNUM <= (pageNum*pageSize) ) WHERE ROW_ID > ((pageNum-1)*pageSize)

排序SQL和不排序SQL的组装原理一样,只是在最内层加入了排序字段而已。

8.1 ORACLE物理分页

从上面案例也可以看到,Oracle分页SQL比较复杂一点,里面有个字段叫做 ROWNUM。

8.1.1 什么是ROMNUM?

对于习惯了MySql的LIMIT语法分页的人来说,可能我们首先想到,分页查询就是查询从某一起始行到某一终止行之间的数据,我们很容器写出下面的SQL:

SELECT * FROM T_TEST WHERE ROWNUM > 3 AND ROWNUM <=5;

这条SQL表示我们通过ORACLE查询第4条和第5条数据。
但是运行之后,发现没有任何数据!
因为,第一条数据的行号是1,不符合>3的条件,因此第一条数据不会被查出来;
接着查询第二条数据,之前的第二条数据成为新的第一行,被读取到虚表中它的行号又是1,又不符合条件,又被过滤…
如此下去,一直到最后一行,读取到虚表中的数据都没法满足条件,全部被过滤,最终SELECT出来的结果就是没有任何数据。
因此,对于ORACLE而言,这种分页写法是错误的。

在ORACLE数据库中,分页没有MySql简单,它需要依靠ROWNUM来实现。

1.ROWNUM表示一条记录的行号,FROM后面的数据被读取到内存虚表中时,第一条数据的行号是1,第二条数据的行号是2,以此类推。
2.需要注意,ROWNUM是ORACLE在FROM语句加载每一条数据到虚表之后,才被加上去的一个伪列,如果这条数据被过滤,FROM将重新加载下一条数据,此时的ROWNUM又变为1,前一条数据被废弃。   

使用ROWNUM要记住:
它是ORACLE为FROM虚表提供的一个伪列,即是一个FROM虚表的隐含列字段,表示FROM虚表中记录的行号,总是从1开始;它的实际表示ORACLE数据库从数据文件或者缓冲区中加载数据的顺序。
它和FROM虚表中的结果集是绑定的,先有FROM对应的虚表结果,再为这个结果添加上对应的ROWNUM列表示其记录顺序。


明白ROWNUM的含义之后,也就理解了,ORACLE对于ROWNUM支持如下运算:

< :小于
= : 等于(只支持等于1)
<= : 小于等于
!= : 不等于(比1大的数字,比如!=6,相当于查询出前5条数据)

不支持(虽然编译成功,但是查不出任何数据):

> :大于
>= :大于等于
between...and

8.1.2 ROWNUM案例分析

(1)下面的语句,ORACLE都不支持:

  SELECT ROWNUM,a.* FROM zeh_user a WHERE ROWNUM > 2;
  SELECT ROWNUM,a.* FROM zeh_user a WHERE ROWNUM >= 2;

从FROM虚表读出第一条,ROWNUM为1,不满足大于2的条件,舍弃;再读取第二条,ROWNUM又为1,不满足,舍弃;到最后一条满足条件的也没有。
(2)使用不等于:

SELECT ROWNUM,a.* FROM zeh_user a WHERE ROWNUM != 10;

这条语句和下面的语句查询出来的结果是一样的:

SELECT ROWNUM,a.* FROM zeh_user a WHERE ROWNUM < 10;

因为当从FROM中读取到第10条数据时,发现不满足!=10的的条件(因为第10条数据的ROWNUM就是10),因此舍弃,再读取下一条,ROWNUM又是10,又舍弃;直到最后舍弃完了,就剩下已经读取到的前9条数据了。
(3)ROWNUM=1:

SELECT ROWNUM,a.* FROM zeh_user a WHERE ROWNUM = 1;

只能查出第一条数据。
(4)看下面的区别:

SELECT ROWNUM,a.* FROM zeh_user a WHERE ROWNUM > 0;
SELECT ROWNUM,a.* FROM zeh_user a WHERE ROWNUM >= 1;

查出所有数据。

SELECT ROWNUM,a.* FROM zeh_user a WHERE ROWNUM > 1;

查不出数据。
这是因为查出来一条数据,ROWNUM为1,不满足>1的条件,舍弃;第2条的ROWNUM又为1,又不满足条件,舍弃;最后一条也没有。

8.1.3 使用ROWNUM正确分页

基于上面ROWNUM的特性,因此,ORACLE使用ROWNUM进行分页,要通过子查询形成一个带有ROWNUM的虚表后,再进行分页。
一般标准的是3层SELECT嵌套:

SELECT
	* 
FROM
	( SELECT TMP_PAGE.*, ROWNUM ROW_ID FROM ( SELECT * FROM zeh_user WHERE 1 = 1 ) TMP_PAGE WHERE ROWNUM <= 40 ) 
WHERE
	ROW_ID > 20

第一层:上面最内层的子查询。

SELECT * FROM zeh_user WHERE 1 = 1

表示不进行任何分页的原始结果集,形成的一个虚表。
第二层:从这个虚表中查询出来所有的数据,和ROWNUM,并为ROWNUM重起一个别名 ROW_ID。

SELECT TMP_PAGE.*, ROWNUM ROW_ID FROM ( SELECT * FROM zeh_user WHERE 1 = 1 ) TMP_PAGE WHERE ROWNUM <= 40

注意看,这个语句中WHERE条件中,要使用原始的ROWNUM去过滤,而不能使用别名ROW_ID。
因此,从SQL的执行顺序来说,先有FROM,然后是WHERE条件,最后才是SELECT后的结果集。
FROM形成的虚表,此时只有伪列ROWNUM,而起别名是在SELECT后才对伪列起的,因此WHERE中只能使用伪列ROWNUM。
这个子查询,会在原始数据的基础上,查询ROWNUM<=40的数据,即查询出前40条数据。
最终SELECT后的虚表有前40条数据,和这40条数据的伪列ROWNUM的别名ROW_ID。
第三层:从上面第二层的虚表中,再查询最后的结果集。

SELECT
	* 
FROM
	( SELECT TMP_PAGE.*, ROWNUM ROW_ID FROM ( SELECT * FROM zeh_user WHERE 1 = 1 ) TMP_PAGE WHERE ROWNUM <= 40 ) 
WHERE
	ROW_ID > 20

因此第二步的虚表中,已经有前40条数据,和对应的ROW_ID序号,因此WHERE条件中使用ROW_ID > 20,来选择第20条之后的数据。
即满足了我们的要求,查询出第21条到第40条的数据(包含21条和40条)。

ROWNUM分页原理:

1.执行FROM操作,形成FROM虚表;
2.将FROM虚表中的第一行数据的ROWNUM设置为1;
3.将得到的数据行与WHERE条件比较,如果不匹配,则抛弃当前行;如果匹配则返回当前行;
4.ORACLE获取下一行,然后将FROM虚表中读取的ROWNUM自增1;
5.返回第3步;
6.直到超过ROWNUM的限制条件。

ROWNUM使用注意事项:

1.ROWNUM不能以任何基表的别名作为前缀,因为它不属于物理表的列,只是一个FROM虚表中产生的伪列;
2.子查询中的ROWNUM必须要起别名,否则还是查不出来数据,因为ROWNUM不是某个表的列,如果不起别名的话,外层查询无法知道ROWNUM是子查询的还是主查询形成的伪列。
3.查询ROWNUM在某区间的数据,ROWNUM对小于某值的查询条件为true;对大于某值的查询条件直接认为是false的,但是可以通过子查询间接实现。

8.1.4 ROWNUM分页和排序

先看一下,ROWNUM和ORDER BY有关系吗? 从上图可知,ROWNUM和ORDER BY 没有关系。
前面反复强调,ROWNUM是在FROM虚表产生时,就已经生成了。也就是说,FROM加载了表的内容到虚表后,这个ROWNUM就确定了,它的顺序就表示FROM从物理表加载到虚表后的顺序。
而由SQL执行顺序可知,ORDER BY 远在 FROM 执行顺序之后。
因此,ORDER BY 排序后的结果不影响 ROWNUM 的值。
假如我们要获取ID前五名的数据:

  SELECT ROWNUM,a.* FROM zeh_user a WHERE ROWNUM <= 5 ORDER BY a.id ;

从上图可看出,它只是对原表中的数据取出前5条后按照id进行排序而已,这种写法是错误的!
先获取了前5条数据,然后对这5条数据排序。
而不是先排序后,再获取排序后的前5条。
因为ROWNUM在排序前就已经确定了。
因此,我们要先排序,然后用排序后的数据重新生成ROWNUM,再取前5条。

要想重新生成ROWNUM,就得重新使用FROM虚表,所以必须加入一个新的子查询:
SELECT
	ROWNUM,
	b.* 
FROM
	( SELECT a.* FROM zeh_user a ORDER BY a.id ) b 
WHERE
	ROWNUM <= 5;

上面的结果是对的。
搞一个子查询,先按照id排序,排序后的结果形成一个新的虚表作为FROM的目标表,然后执行外层查询时,FROM虚表获取的ROWNUM就是从排序后的虚表中重新生成的。
这样可以保证它的顺序是基于排序后的数据重新生成的顺序,获取前5名即可。
总结:对于排序的ORACLE分页,最内层先排序;第2层取ROWNUM<=范围;最外层取ROWNUM>范围

8.1.5 ROWNUM分页的两种写法性能对比

第一种:就是上面的案例,推荐

SELECT
	* 
FROM
	( SELECT TMP_PAGE.*, ROWNUM ROW_ID FROM ( SELECT * FROM zeh_user WHERE 1 = 1 ) TMP_PAGE WHERE ROWNUM <= 40 ) 
WHERE
	ROW_ID > 20

这种分页查询大多数情况下有比较高的效率,其原因体现在第二步的子查询提前使用了WHERE ROWNUM <= 40,将子查询数据提前过滤,形成了较小的虚表。

第二种:不推荐

SELECT
	* 
FROM
	( SELECT TMP_PAGE.*, ROWNUM ROW_ID FROM ( SELECT * FROM zeh_user WHERE 1 = 1 ) TMP_PAGE) 
WHERE
	ROW_ID > 20 AND ROW_ID <= 40

或者使用BETWEEN :

SELECT
	* 
FROM
	( SELECT TMP_PAGE.*, ROWNUM ROW_ID FROM ( SELECT * FROM zeh_user WHERE 1 = 1 ) TMP_PAGE) 
WHERE
	ROW_ID BETWEEN  21 AND 40

第二种写法是在整个分页查询的最外层控制最大值和最小值范围。

大多数情况下,第一种效率要远远高于第二种效率。

这是由于在CBO优化模式下,ORACLE可以将外层的查询条件推到内层查询中(只能在语法满足的条件下向内推进一层),以提高内层查询的效率。
对于第一个查询语句,第二层的查询条件 WHERE ROWNUM <= 40 就可以被ORACLE推入到内层查询中,这样ORACLE一旦在执行内层查询语句“SELECT * FROM zeh_user WHERE 1 = 1”时一旦发现超过了ROWNUM限制条件,就终止查询将结果返回了。
如下我们也可以手动将第一种的条件推到最内层(但手动推进到最内层的这种写法在有的情况下是错误的,下面会分析):

SELECT
	* 
FROM
	( SELECT TMP_PAGE.*, ROWNUM ROW_ID FROM ( SELECT * FROM zeh_user WHERE 1 = 1 AND ROWNUM <= 40) TMP_PAGE ) 
WHERE
	ROW_ID > 20

而第二个查询语句中,由于查询条件 ROW_ID > 20 AND ROW_ID <= 40 在第三层,而ORACLE不能将这个条件推到第一层。
即便推到最内层,也无法知道ROW_ID是什么。
也不能推到第二层,因为在第二层中,ORACLE也不知道ROW_ID是什么,ROW_ID的产生是在第二层SELECT结束才形成的别名。
因此,第二个语句中,ORACLE最内层返回给中间层的是所有满足条件的数据,中间层返回给最外层的也是所有满足条件的数据,过滤操作是在最外层进行的。
很显然,这种形成的中间虚表是很膨胀的,性能很低。
上面这种分析不仅仅针对简单的单表查询,对于最内层查询是复杂的多表级联查询的或者最内层包含排序查询的也同样适用!

8.1.6 不建议模拟ORACLE优化策略手动将ROWNUM条件推进到最内层

ROWNUM分页标准的写法是3层SELECT嵌套,而且ROWNUM<=的条件应该在第2层;ROWNUM>的条件应该在第3层;不带任何ROWNUM的原始SQL查询结果集应该在第1层即最内层。

这样写是有讲究的,必须保证最内层产生的数据是最原始的,即不带任何ROWNUM过滤的。

至于第二层WHERE中的条件,ORACLE到底要不要帮我们推荐到最内层做性能的优化,这个选择是智能的,它优化的前提是保证数据的准确性的,而不是说任何情况下它都会推进到最内层。
向上面的案例中,我们手动将第二层的ROWNUM条件推荐到最内层,这是不建议的!
下面看一个案例:
(1)在最内层对结果集去重复,然后查询前5条数据:

SELECT
	* 
FROM
	( SELECT TMP_PAGE.*, ROWNUM ROW_ID FROM ( SELECT DISTINCT ( NAME ) FROM zeh_user WHERE 1 = 1 ) TMP_PAGE WHERE ROWNUM <= 5 ) 
WHERE
	ROW_ID > 0;

执行结果: (2)模拟ORACLE优化策略,手动将2层的ROWNUM条件推进到最内层:

SELECT
	* 
FROM
	( SELECT TMP_PAGE.*, ROWNUM ROW_ID FROM ( SELECT DISTINCT ( NAME ) FROM zeh_user WHERE 1 = 1 AND ROWNUM <= 5 ) TMP_PAGE ) 
WHERE
	ROW_ID > 0;

执行结果:

手动推荐后的分页结果竟然完全不同!
第(1)种是标准的,最内层是原始SQL,先对所有NAME字段去重复,形成一个结果集;再取出这个结果集中的前5条;最后再整体取出分页范围。
第(2)种是我们手动模拟ORACLE优化策略将ROWNUM条件推进到最内层中,它表示先取出结果集的前5条;然后对这前5条数据中的NAME字段去重复;最后再整体取出分页范围。它是在前5条的基础上获取ROWNUM>0的数据范围的。
这显然和我们的实际诉求不符合。
因此,对DISTINCT这种操作的分页查询,ORACLE是不会帮我们优化的,因为这样优化导致它变成了一个业务错误的SQL!
实际上,对最内层的子查询包含了如下操作的:

UNION
UNION ALL
MINUS
INTERSECT
GROUP BY
DISTINCT
UNIQUE
以及聚集函数
MAX
MIN
和分析函数

上面这些操作都是针对全部符合条件的数据结果集所做的一个操作,它需要把全部的数据查询出来后再去做对应的操作。
因此,对于标准3层SELECT嵌套的分页语句,如果最内层包含了上述操作的任意一个,ORACLE都不会将ROWNUM条件从第2层推荐到最内层,因为这样会导致SQL的业务含义发生错误。
所以,针对这些操作,ORACLE对于分页的性能优化是没有执行的。

这也是为啥对于ORACLE分页语句中包含上述操作会导致性能低下的原因。

文档信息

Search

    Table of Contents