Mybatis分页插件PageHelper

Mybatis插件

MyBatis允许你在映射语句执行过程中的某一点进行拦截调用。默认情况下,MyBatis 允许使用插件来拦截的方法调用包括:

  • Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
  • ParameterHandler (getParameterObject, setParameters)
  • ResultSetHandler (handleResultSets, handleOutputParameters)
  • StatementHandler (prepare, parameterize, batch, update, query)

Mybatis的插件都需要实现Interceptor接口,通过对intercept方法的重写,来进行方法增强。接口实现类通过@Intercepts + @Signature注解来对可以增强的方法进行描述,将能进行方法增强的method,放入一个signatureMap,后续符合条件的method,则执行intercept方法。分页插件PageHelper相关的Interceptor接口实现类是PageInterceptor,它的增强点是Executor的4个参数和6个参数的query方法。

分页插件PageHelper的使用

代码准备

创建mybatis-config.xml

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

    <properties>
        <property name="driver" value="com.mysql.cj.jdbc.Driver"/>
        <property name="url" value="jdbc:mysql://127.0.0.1/test?useUnicode=true&amp;characterEncoding=utf-8&amp;useSSL=false&amp;serverTimezone=Asia/Shanghai&amp;allowPublicKeyRetrieval=true"/>
        <property name="username" value="root"/>
        <property name="password" value="123456"/>
    </properties>

    <settings>
        <setting name="mapUnderscoreToCamelCase" value="true"/>
    </settings>

    <plugins>
        <plugin interceptor="com.github.pagehelper.PageInterceptor" />
    </plugins>

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

    <mappers>
        <mapper resource="mapper/PluginMapper.xml" />
    </mappers>

</configuration>

创建PluginMapper.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="com.ys.mybatis.mapper.PluginMapper">

    <select id="listAllEmployee" resultType="com.ys.mybatis.DO.EmployeeDO" >
        select * from employee
    </select>

    <select id="listIPageEmployee" resultType="com.ys.mybatis.DO.EmployeeDO" >
        select * from employee
    </select>

    <select id="listArgsEmployee" resultType="com.ys.mybatis.DO.EmployeeDO" >
        select * from employee
    </select>

</mapper>

创建PluginMapper

public interface PluginMapper {

    List<EmployeeDO> listAllEmployee();

    List<EmployeeDO> listIPageEmployee(IPage iPage);

    List<EmployeeDO> listArgsEmployee(Map<String, Object> map);
}

创建EmployeeDO

@Data
@NoArgsConstructor
@AllArgsConstructor
public class EmployeeDO implements Serializable {

    private Integer id;

    private String name;

    private Integer age;

    private String phone;
}

创建PluginTest 

@Slf4j
public class PluginTest {
    private SqlSessionFactory sqlSessionFactory;

    @BeforeEach
    public void before() {
        InputStream inputStream = ConfigurationTest.class.getClassLoader().getResourceAsStream("mybatis-config.xml");
        sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
    }
}
1.使用PageHelper.startPage
@Test
public void testStartPage() {
    // 获取sqlSession
    SqlSession sqlSession = sqlSessionFactory.openSession();

    // 获取PluginMapper对象
    PluginMapper pluginMapper = sqlSession.getMapper(PluginMapper.class);

    // 设置分页
    PageHelper.startPage(1, 2);

    // 分页查询
    List<EmployeeDO> list = pluginMapper.listAllEmployee();

    // 封装查询结果
    PageInfo<EmployeeDO> pageInfo = new PageInfo<>(list);

    System.out.println(pageInfo.getList());
    System.out.println(pageInfo.getTotal());
}

2.参数为IPage

创建IPageRequest

@Data
@AllArgsConstructor
@NoArgsConstructor
public class IPageRequest implements IPage {

    private Integer pageNum;

    private Integer pageSize;

    private String orderBy;

}
@Test
public void testIPage() {
    // 获取sqlSession
    SqlSession sqlSession = sqlSessionFactory.openSession();

    // 获取PluginMapper对象
    PluginMapper pluginMapper = sqlSession.getMapper(PluginMapper.class);

    // 分页查询
    List<EmployeeDO> list = pluginMapper.listIPageEmployee(new IPageRequest(1, 2, null));

    // 封装查询结果
    PageInfo<EmployeeDO> pageInfo = new PageInfo<>(list);

    System.out.println(pageInfo.getList());
    System.out.println(pageInfo.getTotal());
}

3.supportMethodsArguments属性为true

将属性SupportMethodsArguments设置为true

<plugins>
    <plugin interceptor="com.github.pagehelper.PageInterceptor" >
         <property name="supportMethodsArguments" value="true"/>
    </plugin>
</plugins>
@Test
public void testSupportMethodsArguments() {
    // 获取sqlSession
    SqlSession sqlSession = sqlSessionFactory.openSession();

    // 获取PluginMapper对象
    PluginMapper pluginMapper = sqlSession.getMapper(PluginMapper.class);

    // 封装查询参数
    Map<String, Object> map = new HashMap<>();
    map.put("pageNum", 1);
    map.put("pageSize", 2);

    // 分页查询
    List<EmployeeDO> list = pluginMapper.listArgsEmployee(map);

    // 封装查询结果
    PageInfo<EmployeeDO> pageInfo = new PageInfo<>(list);

    System.out.println(pageInfo.getList());
    System.out.println(pageInfo.getTotal());
}

源码解析

解析配置文件
XMLConfigBuilder#pluginElement
private void pluginElement(XNode parent) throws Exception {
    if (parent != null) {
        for (XNode child : parent.getChildren()) {
            // 获取interceptor全限定名
            String interceptor = child.getStringAttribute("interceptor");
            // 获取自定义的properties
            Properties properties = child.getChildrenAsProperties();
            // 实例化Interceptor对象
            Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).getDeclaredConstructor().newInstance();
            // 将自定义的properties设置到相关属性
            interceptorInstance.setProperties(properties);
            configuration.addInterceptor(interceptorInstance);
        }
    }
}
PageInterceptor#setProperties
@Override
public void setProperties(Properties properties) {
    // 缓存一些count查询
    msCountMap = CacheFactory.createCache(properties.getProperty("msCountCache"), "ms", properties);
    
    // 获取分页插件的实现类,默认是想是PageHelper
    // 自定义分页插件,需要手动设置dialect
    String dialectClass = properties.getProperty("dialect");
    if (StringUtil.isEmpty(dialectClass)) {
        dialectClass = default_dialect_class;
    }
    try {
        Class<?> aClass = Class.forName(dialectClass);
        dialect = (Dialect) aClass.newInstance();
    } catch (Exception e) {
        throw new PageException(e);
    }

    // 通过手动设置的参数,覆盖一些属性的默认值
    dialect.setProperties(properties);

    String countSuffix = properties.getProperty("countSuffix");
    if (StringUtil.isNotEmpty(countSuffix)) {
        this.countSuffix = countSuffix;
    }
}   
PageHelper#setProperties
@Override
public void setProperties(Properties properties) {
    setStaticProperties(properties);
    pageParams = new PageParams();
    autoDialect = new PageAutoDialect();
    pageParams.setProperties(properties);
    autoDialect.setProperties(properties);
    CountSqlParser.addAggregateFunctions(properties.getProperty("aggregateFunctions"));
}

PageHelper存在两个属性PageParams,PageAutoDialect

  • PageParams :分页相关设置
  • PageAutoDialect : 方言自动推断

PageParams相关属性含义

  • offsetAsPageNum : RowBounds参数offset作为PageNum使用
  • rowBoundsWithCount:RowBounds是否进行count查询
  • pageSizeZero:当设置为true的时候,如果pagesize设置为0(或RowBounds的limit=0),就不执行分页,返回全部结果
  • reasonable :分页合理化
  • supportMethodsArguments:是否支持接口参数来传递分页参数,默认false
  • countColumn :默认count(0)
动态代理

当我们新建一个SqlSession的时候,如果配置了相关插件,则对其绑定的Executor进行动态代理

执行PageInterceptor的intercept方法
@Override
public Object intercept(Invocation invocation) throws Throwable {
    try {
        // 省略部分代码

        List resultList;
        // 重点关注这个skip方法,如果skip返回true,则不执行分页插件逻辑
        if (!dialect.skip(ms, parameter, rowBounds)) {
            // 是否执行count查询
            if (dialect.beforeCount(ms, parameter, rowBounds)) {
                // 查询总数
                Long count = count(executor, ms, parameter, rowBounds, resultHandler, boundSql);
                // 处理查询总数,返回 true 时继续分页查询,false时直接返回
                if (!dialect.afterCount(count, parameter, rowBounds)) {
                    // 当查询总数为 0 时,直接返回空的结果
                    return dialect.afterPage(new ArrayList(), parameter, rowBounds);
                }
            }
            // 执行分页插件逻辑
            resultList = ExecutorUtil.pageQuery(dialect, executor,
                    ms, parameter, rowBounds, resultHandler, boundSql, cacheKey);
        } else {
            //rowBounds用参数值,不使用分页插件处理时,仍然支持默认的内存分页
            resultList = executor.query(ms, parameter, rowBounds, resultHandler, cacheKey, boundSql);
        }
        return dialect.afterPage(resultList, parameter, rowBounds);
    } finally {
        dialect.afterAll();
    }
}
Dialect#skip方法
@Override
public boolean skip(MappedStatement ms, Object parameterObject, RowBounds rowBounds) {
    if (ms.getId().endsWith(MSUtils.COUNT)) {
        throw new RuntimeException("在系统中发现了多个分页插件,请检查系统配置!");
    }
    // 返回已经存在或者新构建的page对象
    Page page = pageParams.getPage(parameterObject, rowBounds);
    if (page == null) {
        return true;
    } else {
        //设置默认的 count 列
        if (StringUtil.isEmpty(page.getCountColumn())) {
            page.setCountColumn(pageParams.getCountColumn());
        }
        // 根据url动态获取断言类型或者通过helperDialect属性,指定Dialect类型
        autoDialect.initDelegateDialect(ms);
        return false;
    }
}

PageParam的getPage()方法很重要,它详细说明了如何定义参数,才能返回一个Page对象。如果不能成功返回一个Page对象,分页插件的功能就会失效。可以返回page对象的方式,分成了以下几种情况 :

  • 1.Pape已经存在 (PageHelper.startPage)
  • 2.Pape不存在
    • 2.1 RowBounds不等于默认值
      • 2.1.1 offsetAsPageNum属性为 true
      • 2.1.2 offsetAsPageNum属性为 fasle
    • 2.2 参数类型是IPage 或者 supportMethodsArguments 属性为 true
      • 2.2.1 通过IPage构建page
      • 2.2.2 通过javax.servlet.ServletRequest.getParameterMap方法构建
      • 2.2.3 通过参数进行构建
    • 2.3 返回null

上文的三个案例,分别对应1、2.2.1、2.2.3这三种情况。更多运用,可以查看getPage()方法明细。如果需要修改默认值,则需要手动配置,示例如下:

<plugins>
    <plugin interceptor="com.github.pagehelper.PageInterceptor" >
         <property name="supportMethodsArguments" value="true"/>
         <property name="offsetAsPageNum" value="true"/>
         <property name="reasonable" value="true"/>
    </plugin>
</plugins>
几种方式比较

第一优先级:1;PageHelper的startPage()方法会返回一个page对象,我们可以按需设置属性

第二优先级:2.2.2 、2.2.3 ; 可以手动设置pageNum、pageSize、orderBy、count、reasonable、pageSizeZero等属性

第三优先级:2.1.1 、2.1.2、2.2.1; 只能设置pageNum、pageSize、orderBy等属性。count、reasonable、pageSizeZero等属性,只能为全局配置的值或默认值,不用单独设置,灵活性较差

Dialect#beforeCount方法

通过skip方法,会构建出一个Page对象。如果其 orderByOnly 属性为 false ,count 属性为 true,则执行count方法

Dialect#count方法

根据Page对象countColumn属性,构建出类似于 select count(0) from ( 原始sql ) 这样的新Sql。我们也可以通过设置,将countColumn属性改成 id、*等。我们还可以自定义count查询,比如上文中的使用案例1(listAllEmployee),如果我们自定义了一个id为listAllEmployee_COUNT的查询,则最终将使用我们自定义的查询。演示如下:

<select id="listAllEmployee_COUNT" resultType="int" >
    select count(*) from employee where 1 = 1
</select>

ExecutorUtil#pageQuery方法

主要有三个关注点,我们以MySqlDialect为例

  • 关注点1:进入if条件 (Page对象的 orderByOnly 属性为 true 或 pageSize 大于0)
  • 关注点2:参数处理
    • 2.1 会将参数统一构建成Map对象
    • 2.2 给Map对象put两个key(First_PageHelperSecond_PageHelper), 其值分别为Page对象的startRow和pageSize。
  • 关注点3:拼接sql。如果Page对象的startRow为0,则拼接" LIMIT ? ",否则拼接" LIMIT ?, ? "。就是Map对象First_PageHelper、Second_PageHelper这两个key对应的值

MySqlDialect#getPageSql

@Override
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 ?, ? ");
    }
    return sqlBuilder.toString();
}

 一些细节问题

1.如果reasonable的默认值被设置为true,且构建出的page的reasonable属性为null,page对象有一次优化分页的机会。但是上文中2.1.1这种情况不可以优化分页,因为构建出的page对象的reasonable属性被设置成了false

2.如果是单数据源,或者多数据源都是同类型的数据库产品,可以自定义helperDialect属性,避免默认情况下,每次分页查询都需要根据url来推断Dialect类型。以MySqlDialect为例,设置如下:

<plugins>
    <plugin interceptor="com.github.pagehelper.PageInterceptor" >
         <property name="helperDialect" value="com.github.pagehelper.dialect.helper.MySqlDialect"/>
    </plugin>
</plugins>

3.默认情况下,2.1.1 、2.1.2这两种构建page的方式,不会执行count查询 

  • 32
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值