PageHelper源码分析

一、介绍

  • 实际工作中,当我们对数据进行列表查询时,往往需要进行2部分工作:1. 查询所需列表页数所对应的数据;2. 统计符合条件的数据总数。这必然造成至少需要编写写2个sql,无形中会增加我们的工作量。此外,当发生业务变动时,我们又需要同时改动2个sql。否则,必然会引起数据结果的不一致。
  • 因此,我们需要一个简单易用的分页工具来辅助我们完成这个工作。
  • PageHelper就是一款好用的开源免费的Mybatis第三方物理分页插件。

二、MyBatis

2.1 Mybatis框架

        首先需要要在配置文件中配置一些东西,然后根据这些配置去创建一个会话工厂,再根据会话工厂创建会话,会话发出操作数据库的sql语句,然后通过执行器操作数据,再使用mappedStatement对数据进行封装。

2.2 Mybatis插件

        Mybatis的插件,主要作用在Executor执行器与mappedeStatement之间,即mybatis可以在插件中获得要执行的sql语句。然后,在sql语句中添加limit语句,再对sql进行封装,从而可以实现分页处理。MyBatis插件通过拦截器实现,拦截执行Sql的四大对象:Executor、StatementHandler、ParameterHandler、ResultSetHandler。需要注意的是,并不是这四大对象中的所有方法都能被拦截:

        通过@Intercepts声明当前类是一个拦截器,后面的@Signature是标识需要拦截的方法签名,通过以下三个参数来确定

type:被拦截的类名
method:被拦截的方法名
args:标注方法的参数类型

Mybatis插件执行流程:

三、PageHelper初识

3.1 pageHelper依赖引入

  • springboot:直接引入pagehelper-spring-boot-starter
<!-- https://mvnrepository.com/artifact/com.github.pagehelper/pagehelper-spring-boot-starter -->
<dependency>
    <groupId>com.github.pagehelper</groupId>
    <artifactId>pagehelper-spring-boot-starter</artifactId>
    <version>1.3.1</version>
</dependency>
  • springmvc:引入 pageHelper
<!-- https://mvnrepository.com/artifact/com.github.pagehelper/pagehelper -->
<dependency>
    <groupId>com.github.pagehelper</groupId>
    <artifactId>pagehelper</artifactId>
    <version>5.2.1</version>
</dependency>

3.2 pagehelper插件配置

  • springboot:mybatis、db相关配置

        reasonable属性:分页参数合理化,默认false禁用。

        启用合理化时,如果pageNum<1会查询第一页,如果pageNum>pages会查询最后一页

        禁用合理化时,如果pageNum<1或pageNum>pages会返回空数据。

        supportMethodsArguments属性:是否自动分页

        params: count=countSql属性:如果POJO或者Map中发现了countSql属性,就作为count参数使用

  • springmvc:配置mybatis-config.xml,并在配置数据源的时候,将mybatis配置文件指向以上文件。
  • 3.3 Pagehelper使用

    使用的时候,只需在查询list前,调用 startPage 设置分页信息,即可使用分页功能。如果不想进行count, 只要查分页数据, 则调用: PageHelper.startPage(pageNum, pageSize, false); 即可, 避免了不必要的count消耗。

  /**
     * 开始分页
     *
     * @param pageNum      页码
     * @param pageSize     每页显示数量
     * @param count        是否进行count查询
     * @param reasonable   分页合理化,null时用默认配置
     * @param pageSizeZero true且pageSize=0时返回全部结果,false时分页,null时用默认配置
     */
    public static <E> Page<E> startPage(int pageNum, int pageSize, boolean count, Boolean reasonable, Boolean pageSizeZero) {
        Page<E> page = new Page<E>(pageNum, pageSize, count);
        page.setReasonable(reasonable);
        page.setPageSizeZero(pageSizeZero);
        //当已经执行过orderBy的时候
        Page<E> oldPage = getLocalPage();
        if (oldPage != null && oldPage.isOrderByOnly()) {
            page.setOrderBy(oldPage.getOrderBy());
        }
        setLocalPage(page);
        return page;
    }

四、 pageHelper实现原理

4.1 springboot中接入interceptor

        springboot 中接入pagehelper非常简单, 益于PageHelperAutoConfiguration初始化的方式, 它会自动加载配置。

@Configuration
@ConditionalOnBean(SqlSessionFactory.class)
@EnableConfigurationProperties(PageHelperProperties.class)
@AutoConfigureAfter(MybatisAutoConfiguration.class)
@Lazy(false)
public class PageHelperAutoConfiguration {

    @Autowired
    private List<SqlSessionFactory> sqlSessionFactoryList;

    @Autowired
    private PageHelperProperties properties;

    /**
     * 接受分页插件额外的属性
     *
     * @return
     */
    @Bean
    @ConfigurationProperties(prefix = PageHelperProperties.PAGEHELPER_PREFIX)
    public Properties pageHelperProperties() {
        return new Properties();
    }

    @PostConstruct
    public void addPageInterceptor() {
        PageInterceptor interceptor = new PageInterceptor();
        Properties properties = new Properties();
        //先把一般方式配置的属性放进去
        properties.putAll(pageHelperProperties());
        //在把特殊配置放进去,由于close-conn 利用上面方式时,属性名就是 close-conn 而不是 closeConn,所以需要额外的一步
        properties.putAll(this.properties.getProperties());
        interceptor.setProperties(properties);
        for (SqlSessionFactory sqlSessionFactory : sqlSessionFactoryList) {
            org.apache.ibatis.session.Configuration configuration = sqlSessionFactory.getConfiguration();
            if (!containsInterceptor(configuration, interceptor)) {
                configuration.addInterceptor(interceptor);
            }
        }
    }

        借助springboot的自动配置, 获取mybatis的sqlSessionFactoryList, 依次将PageInterceptor,也就是PageHelper接入其中。

    @Override
    public void setProperties(Properties properties) {
        //缓存 count ms
        msCountMap = CacheFactory.createCache(properties.getProperty("msCountCache"), "ms", properties);
        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;
        }
    }

4.2 interceptor的初始化

        将 interceptor 添加到mybatis上下文后, 会在每次调用查询时进行拦截请求, 它的初始化也会在这时候触发,

  private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
    Transaction tx = null;
    try {
      final Environment environment = configuration.getEnvironment();
      final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
      tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
      final Executor executor = configuration.newExecutor(tx, execType);
      return new DefaultSqlSession(configuration, executor, autoCommit);
    } catch (Exception e) {
      closeTransaction(tx); // may have fetched a connection so lets call close()
      throw ExceptionFactory.wrapException("Error opening session.  Cause: " + e, e);
    } finally {
      ErrorContext.instance().reset();
    }
  }


  public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
    executorType = executorType == null ? defaultExecutorType : executorType;
    executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
    Executor executor;
    if (ExecutorType.BATCH == executorType) {
      executor = new BatchExecutor(this, transaction);
    } else if (ExecutorType.REUSE == executorType) {
      executor = new ReuseExecutor(this, transaction);
    } else {
      executor = new SimpleExecutor(this, transaction);
    }
    if (cacheEnabled) {
      executor = new CachingExecutor(executor);
    }
    executor = (Executor) interceptorChain.pluginAll(executor);
    return executor;
  }

4.3 PageInterceptor的调用过程

        在executor被代理后, 会继续执行查询动作, 这时就会被PageInterceptor拦截。PageInterceptor的大体执行框架:

  1. 先解析各位置参数;

  2. 初始化 pageHelper 实例, 即 dialect;

  3. 调用方法判断是否需要进行分页,如果不需要,直接返回结果;

  4. 判断是否要进行count, 如果需要则实现一次count, ;

  5. 查询分页结果;

  6. 封装带分页的结果返回;

       这个方法的逻辑比较多,我们主要是关注ExecutorUtil.pageQuery这个调用,因为PageHelper在这里面改写了sql语句:

   @Override
    public Object intercept(Invocation invocation) throws Throwable {
        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) {
                //4 个参数时
                boundSql = ms.getBoundSql(parameter);
                cacheKey = executor.createCacheKey(ms, parameter, rowBounds, boundSql);
            } else {
                //6 个参数时
                cacheKey = (CacheKey) args[4];
                boundSql = (BoundSql) args[5];
            }
            checkDialectExists();
            //对 boundSql 的拦截处理
            if (dialect instanceof BoundSqlInterceptor.Chain) {
                boundSql = ((BoundSqlInterceptor.Chain) dialect).doBoundSql(BoundSqlInterceptor.Type.ORIGINAL, boundSql, cacheKey);
            }
            List resultList;
            //调用方法判断是否需要进行分页,如果不需要,直接返回结果
            if (!dialect.skip(ms, parameter, rowBounds)) {
                //判断是否需要进行 count 查询
                if (dialect.beforeCount(ms, parameter, rowBounds)) {
                    //查询总数
                    Long count = count(executor, ms, parameter, rowBounds, null, 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 {
            if(dialect != null){
                dialect.afterAll();
            }
        }
    }
    public static <E> List<E> pageQuery(Dialect dialect, Executor executor, MappedStatement ms, Object parameter,
                                        RowBounds rowBounds, ResultHandler resultHandler,
                                        BoundSql boundSql, CacheKey cacheKey) throws SQLException {
        //判断是否需要进行分页查询
        if (dialect.beforePage(ms, parameter, rowBounds)) {
            //生成分页的缓存 key
            CacheKey pageKey = cacheKey;
            //处理参数对象
            parameter = dialect.processParameterObject(ms, parameter, boundSql, pageKey);
            //调用方言获取分页 sql
            String pageSql = dialect.getPageSql(ms, boundSql, parameter, rowBounds, pageKey);
            BoundSql pageBoundSql = new BoundSql(ms.getConfiguration(), pageSql, boundSql.getParameterMappings(), parameter);

            Map<String, Object> additionalParameters = getAdditionalParameter(boundSql);
            //设置动态参数
            for (String key : additionalParameters.keySet()) {
                pageBoundSql.setAdditionalParameter(key, additionalParameters.get(key));
            }
            //对 boundSql 的拦截处理
            if (dialect instanceof BoundSqlInterceptor.Chain) {
                pageBoundSql = ((BoundSqlInterceptor.Chain) dialect).doBoundSql(BoundSqlInterceptor.Type.PAGE_SQL, pageBoundSql, pageKey);
            }
            //执行分页查询
            return executor.query(ms, parameter, RowBounds.DEFAULT, resultHandler, pageKey, pageBoundSql);
        } else {
            //不执行分页的情况下,也不执行内存分页
            return executor.query(ms, parameter, RowBounds.DEFAULT, resultHandler, cacheKey, boundSql);
        }
    }
/**
 * @author liuzh
 */
public class MySqlDialect extends AbstractHelperDialect {

    @Override
    public Object processPageParameter(MappedStatement ms, Map<String, Object> paramMap, Page page, BoundSql boundSql, CacheKey pageKey) {
        paramMap.put(PAGEPARAMETER_FIRST, page.getStartRow());
        paramMap.put(PAGEPARAMETER_SECOND, page.getPageSize());
        //处理pageKey
        pageKey.update(page.getStartRow());
        pageKey.update(page.getPageSize());
        //处理参数配置
        if (boundSql.getParameterMappings() != null) {
            List<ParameterMapping> newParameterMappings = new ArrayList<ParameterMapping>(boundSql.getParameterMappings());
            if (page.getStartRow() == 0) {
                newParameterMappings.add(new ParameterMapping.Builder(ms.getConfiguration(), PAGEPARAMETER_SECOND, int.class).build());
            } else {
                newParameterMappings.add(new ParameterMapping.Builder(ms.getConfiguration(), PAGEPARAMETER_FIRST, long.class).build());
                newParameterMappings.add(new ParameterMapping.Builder(ms.getConfiguration(), PAGEPARAMETER_SECOND, int.class).build());
            }
            MetaObject metaObject = MetaObjectUtil.forObject(boundSql);
            metaObject.setValue("parameterMappings", newParameterMappings);
        }
        return paramMap;
    }

    @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("\n LIMIT ? ");
        } else {
            sqlBuilder.append("\n LIMIT ?, ? ");
        }
        return sqlBuilder.toString();
    }

        经过上面的sql重组之后,就可以得到具体分页的list数据了, 返回的也是list数据. 那么, 用户如何获取其他的分页信息呢? 比如count值去了哪里? 实际上, 在list 返回之后, 还有一个 afterPage 的动作要做, 而它的作用就是封装list 为带page信息的list。

    @Override
    public Object afterPage(List pageList, Object parameterObject, RowBounds rowBounds) {
        Page page = getLocalPage();
        if (page == null) {
            return pageList;
        }
        page.addAll(pageList);
        if (!page.isCount()) {
            page.setTotal(-1);
        } else if ((page.getPageSizeZero() != null && page.getPageSizeZero()) && page.getPageSize() == 0) {
            page.setTotal(pageList.size());
        } else if (page.isOrderByOnly()) {
            page.setTotal(pageList.size());
        }
        return page;
    }

4.4 PageHelper只对startPage后的第一条select语句有效

    @Override
    public void afterAll() {
        //这个方法即使不分页也会被执行,所以要判断 null
        AbstractHelperDialect delegate = autoDialect.getDelegate();
        if (delegate != null) {
            delegate.afterAll();
            autoDialect.clearDelegate();
        }
        clearPage();
    }

        在finally内把ThreadLocal中的分页数据给清除掉了,所以只要执行一次查询语句就会清除分页信息,故而后面的select语句自然就无效了。

4.5 PageHelper分页失效问题

        通过前面的分析,我们了解到PageHelper经过sql重组之后,可以得到具体分页的list数据:

    @Override
    public List<TestVO> select(TestParam param) {
        return this.testMapper.queryList(param);
    }

        这里可能会有一些疑问,如上面的列子, 我们执行sql通常返回的是List类数据,而执行拦截器PageHelper返回的是Page类的数据,这两个类是如何转换的?实际上,Page类是List的子类:  


/**
 * Mybatis - 分页对象
 *
 * @author liuzh/abel533/isea533
 * @version 3.6.0
 * 项目地址 : http://git.oschina.net/free/Mybatis_PageHelper
 */
public class Page<E> extends ArrayList<E> implements Closeable {

        虽然,我们代码里sql查询声明的数据是List类,但实际上返回的是含有分页信息的Page类数据。所以,当对查询出来的数据进行一些处理后,再从controller里使用pagehelper时取出来的数据可能会有问题,比如total 始终等于pageSize。此时,需要检查处理封装后的数据list 是否为带page信息的list,如为否,需要对列表数据做一次转换:

   @Override
    public List<TestVo> select(TestParam param) {
        List<TestConfig> configList = this.testMapper.testList(param);
        PageInfo<Test> configListPageInfo = new PageInfo<>(configList);
        Page<TestVo> page = new Page<>(configListPageInfo.getPageNum(), configListPageInfo.getPageSize());
        page.setTotal(configListPageInfo.getTotal());
        for (TestConfig testConfig : configList) {
            TestConfigVo testConfigVo = new TestConfigVo();
            BeanUtils.copyProperties(testConfig , testConfigVo);
            page.add(testConfigVo );
        }
        return page;
    }

  • 3
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
对于PageHelper源码解析,我无法提供详细的源码解析,因为我没有访问和查看源码的权限。但是我可以告诉你一些关于PageHelper的基本信息。 PageHelper是一个用于处理分页的插件,可以在MyBatis中使用。它提供了一些方便的方法来实现分页功能,比如自动拦截SQL并重写为分页SQL,提供了一些额外的分页参数等。 根据引用中的说明,你可以通过在Spring Boot项目中添加pagehelper-spring-boot-starter依赖来引入PageHelper。而在Spring MVC项目中,你需要添加pagehelper依赖。 在使用PageHelper时,你可以使用pageNum参数来指定要查询的页码。引用中提到,如果pageNum小于1,PageHelper会查询第一页;如果pageNum大于总页数,PageHelper会查询最后一页。 具体实现分页功能的方法取决于你的代码结构和框架。引用中给出了一个示例,通过在mapper中调用queryList方法来进行查询。 总之,PageHelper是一个方便的分页插件,它可以简化在数据库中进行分页查询的操作。但是具体的源码解析需要查看PageHelper源码。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* *3* [PageHelper源码分析](https://blog.csdn.net/u012125445/article/details/120296170)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 100%"] [ .reference_list ]

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值