• PageHelper分页原理解析


    大家好,我是Leo! 今天给大家带来的是关于PageHelper原理的解析,最近遇到一个SQL优化的问题,顺便研究了一下PageHelper的原理,毕竟也是比较常用,源码也比较好看的懂,如果感兴趣的小伙伴可以跟着过程去DEBUG源码,相信会有一定收获,源码也采用了策略、工厂等设计模式

    总体流程

    在调用startPage时,将分页对象Page参数保存下来,留意setLocalPage方法,该方法是保存分页参数的关键,采用ThreadLocal来保存分页参数。

    1. public static <E> Page<E> startPage(int pageNum, int pageSize, boolean count, Boolean reasonable, Boolean pageSizeZero) {
    2. Page<E> page = new Page<E>(pageNum, pageSize, count);
    3. page.setReasonable(reasonable);
    4. page.setPageSizeZero(pageSizeZero);
    5. //当已经执行过orderBy的时候
    6. Page<E> oldPage = getLocalPage();
    7. if (oldPage != null && oldPage.isOrderByOnly()) {
    8. page.setOrderBy(oldPage.getOrderBy());
    9. }
    10. setLocalPage(page);
    11. return page;
    12. }
    13. // 保存分页参数
    14. // protected static final ThreadLocal<Page> LOCAL_PAGE = new ThreadLocal<Page>();

    那么PageHelper是如何判断是否需要分页的呢,这里就需要涉及到MyBatis的Interceptor接口,PageHelper中有一个PageInterceptor实现了该接口,并在intercept方法中写了分页的核心逻辑,boundSql可以看到需要执行的sql,再往下看进入skip方法可以看到是否需要进行分页,主要是判断系统是否有使用多个分页插件。

    继续往下可以看到beforeCounnt方法,会判断是否需要进行count查询,跟进该方法,你会发现在AbstractHelperDialect类中有getLocalPage方法,获取的是我们开始调用startPage方法中保存的page对象(ThreadLocal)。

    1. public Object intercept(Invocation invocation) throws Throwable {
    2. try {
    3. Object[] args = invocation.getArgs();
    4. MappedStatement ms = (MappedStatement) args[0];
    5. Object parameter = args[1];
    6. RowBounds rowBounds = (RowBounds) args[2];
    7. ResultHandler resultHandler = (ResultHandler) args[3];
    8. Executor executor = (Executor) invocation.getTarget();
    9. CacheKey cacheKey;
    10. BoundSql boundSql;
    11. //由于逻辑关系,只会进入一次
    12. if (args.length == 4) {
    13. //4 个参数时
    14. // boundSql就是我们需要执行
    15. boundSql = ms.getBoundSql(parameter);
    16. cacheKey = executor.createCacheKey(ms, parameter, rowBounds, boundSql);
    17. } else {
    18. //6 个参数时
    19. cacheKey = (CacheKey) args[4];
    20. boundSql = (BoundSql) args[5];
    21. }
    22. checkDialectExists();
    23. List resultList;
    24. //调用方法判断是否需要进行分页,如果不需要,直接返回结果
    25. if (!dialect.skip(ms, parameter, rowBounds)) {
    26. //判断是否需要进行 count 查询
    27. if (dialect.beforeCount(ms, parameter, rowBounds)) {
    28. //查询总数
    29. Long count = count(executor, ms, parameter, rowBounds, resultHandler, boundSql);
    30. //处理查询总数,返回 true 时继续分页查询,false 时直接返回
    31. if (!dialect.afterCount(count, parameter, rowBounds)) {
    32. //当查询总数为 0 时,直接返回空的结果
    33. return dialect.afterPage(new ArrayList(), parameter, rowBounds);
    34. }
    35. }
    36. resultList = ExecutorUtil.pageQuery(dialect, executor,
    37. ms, parameter, rowBounds, resultHandler, boundSql, cacheKey);
    38. } else {
    39. //rowBounds用参数值,不使用分页插件处理时,仍然支持默认的内存分页
    40. resultList = executor.query(ms, parameter, rowBounds, resultHandler, cacheKey, boundSql);
    41. }
    42. return dialect.afterPage(resultList, parameter, rowBounds);
    43. } finally {
    44. dialect.afterAll();
    45. }
    46. }

    然后接着跟进count方法,然后你可以看到countMsId就是这条SQL的的路径再拼上_COUNT,

    然后ExecutorUtil.getExistMapperdStatment方法中是通过MyBatis的Configuration来获取是否存在这样的一个查询,如果我们有重写COUNT方法,它就会执行我们写的count语句,如果没有写,它会帮我们拼装一条count语句。

    1. private Long count(Executor executor, MappedStatement ms, Object parameter,
    2. RowBounds rowBounds, ResultHandler resultHandler,
    3. BoundSql boundSql) throws SQLException {
    4. String countMsId = ms.getId() + countSuffix;
    5. Long count;
    6. //先判断是否存在手写的 count 查询
    7. MappedStatement countMs = ExecutorUtil.getExistedMappedStatement(ms.getConfiguration(), countMsId);
    8. if (countMs != null) {
    9. count = ExecutorUtil.executeManualCount(executor, countMs, parameter, boundSql, resultHandler);
    10. } else {
    11. countMs = msCountMap.get(countMsId);
    12. //自动创建
    13. if (countMs == null) {
    14. //根据当前的 ms 创建一个返回值为 Long 类型的 ms
    15. countMs = MSUtils.newCountMappedStatement(ms, countMsId);
    16. msCountMap.put(countMsId, countMs);
    17. }
    18. count = ExecutorUtil.executeAutoCount(dialect, executor, countMs, parameter, boundSql, rowBounds, resultHandler);
    19. }
    20. return count;
    21. }

    接着进入afterCount语句,执行完后需要保存total总数,后续分页需要使用。

    1. @Override
    2. public boolean afterCount(long count, Object parameterObject, RowBounds rowBounds) {
    3. Page page = getLocalPage();
    4. page.setTotal(count);
    5. if (rowBounds instanceof PageRowBounds) {
    6. ((PageRowBounds) rowBounds).setTotal(count);
    7. }
    8. //pageSize < 0 的时候,不执行分页查询
    9. //pageSize = 0 的时候,还需要执行后续查询,但是不会分页
    10. if (page.getPageSize() < 0) {
    11. return false;
    12. }
    13. return count > 0;
    14. }

    然后回到PageInterceptor中ExecutorUtil.pageQuery中执行分页查询,在dialect.getPageSql,然后跟进到AbstaractHelpDialect.getPageSql,这里也会拿出Page对象,然后再调用对应数据库的分页查询的拼接SQL,这里我的是MySQL,最终调用到MySqlDialect中的getPageSql方法,然后我们就可以看到LIMIT 关键字是如何拼接上去的了,然后再填充动态sql的参数

    1. public static <E> List<E> pageQuery(Dialect dialect, Executor executor, MappedStatement ms, Object parameter,
    2. RowBounds rowBounds, ResultHandler resultHandler,
    3. BoundSql boundSql, CacheKey cacheKey) throws SQLException {
    4. //判断是否需要进行分页查询
    5. if (dialect.beforePage(ms, parameter, rowBounds)) {
    6. //生成分页的缓存 key
    7. CacheKey pageKey = cacheKey;
    8. //处理参数对象
    9. parameter = dialect.processParameterObject(ms, parameter, boundSql, pageKey);
    10. //调用方言获取分页 sql
    11. String pageSql = dialect.getPageSql(ms, boundSql, parameter, rowBounds, pageKey);
    12. BoundSql pageBoundSql = new BoundSql(ms.getConfiguration(), pageSql, boundSql.getParameterMappings(), parameter);
    13. Map<String, Object> additionalParameters = getAdditionalParameter(boundSql);
    14. //设置动态参数
    15. for (String key : additionalParameters.keySet()) {
    16. pageBoundSql.setAdditionalParameter(key, additionalParameters.get(key));
    17. }
    18. //执行分页查询
    19. return executor.query(ms, parameter, RowBounds.DEFAULT, resultHandler, pageKey, pageBoundSql);
    20. } else {
    21. //不执行分页的情况下,也不执行内存分页
    22. return executor.query(ms, parameter, RowBounds.DEFAULT, resultHandler, cacheKey, boundSql);
    23. }
    24. }
    1. @Override
    2. public String getPageSql(String sql, Page page, CacheKey pageKey) {
    3. StringBuilder sqlBuilder = new StringBuilder(sql.length() + 14);
    4. sqlBuilder.append(sql);
    5. if (page.getStartRow() == 0) {
    6. sqlBuilder.append(" LIMIT ? ");
    7. } else {
    8. sqlBuilder.append(" LIMIT ?, ? ");
    9. }
    10. return sqlBuilder.toString();
    11. }

    然后通过调用executer.query来查询sql.

    总体流程图

    总结

    以上就是PageHelper源码和其执行流程,其实看了源码发现,它是帮我们去管理了分页逻辑,核心是通过MyBatis的拦截器来进行分页的。

  • 相关阅读:
    使用环信提供的uni-app Demo,快速实现一对一单聊
    【STL***vector容器二】
    selenium环境+元素定位大法
    VIM去掉utf-8 bom头
    【mmWave】二、IWR6843ISK-ODS毫米波雷达【固件开发】流程
    基于SSM的视频播放系统的设计与实现
    二维平面的变换
    【剑指Offer】34.二叉树中和为某一值的路径(二)
    每日一题 2698. 求一个整数的惩罚数(中等,暴力)
    css知识点总结
  • 原文地址:https://blog.csdn.net/zly03/article/details/132738807