Mybatis-Pagehelper详细解析及优化插件开发

简介: 项目数据库数据量较大,分页查询要很久,所以要对分页优化,项目使用的分页是mybatis的Pagehelper,于是在Pagehelper的基础上进行了本次分页查询的优化

背景

项目数据库数据量较大,分页查询要很久,所以要对分页优化,项目使用的分页是mybatis的Pagehelper,于是在Pagehelper的基础上进行了本次分页查询的优化

Mybatis-Pagehelper

优化是基于mybatis-Pagehelper的,我们先看一下mybatis-Pagehelper这个插件,他是怎么实现mybatis分页的,比如,基本上我们每个人在分页查询时都会看到先查询count 再 具体查询,那他们在哪个环节查询,这里就有答案,首先我们先对这个插件进行集成

  1. 集成

    <dependency>
    
    <groupId>com.github.pagehelper</groupId>
    
    <artifactId>pagehelper-spring-boot-starter</artifactId>
    
  2. 配置

    pagehelper.helperDialect=mysql
    
    pagehelper.reasonable=false
    
  3. 使用
    使用起来很简单,这样就可以完成了

    public PageInfo<MessageSmsSendTaskVO> queryByPage(MessageSmsSendTaskQuery messageSmsSendTaskQuery) {
    
    messageSmsSendTaskQuery.enablePage();
    
    List<MessageSmsSendTaskVO> list = this.messageSmsSendTaskMapper.queryList(messageSmsSendTaskQuery);
    
    STranslationConvertUtil.convertList(list);
    
    return toPageInfo(list);
    
    }
    
    /**
    * 初始化分页信息,且开启分页线程控制器
    */
    
    public <E> Page<E> enablePage() {
        pageInfoInit();
        return PageHelper.startPage(pageNum, pageSize);
    }
    
    /**
    * 初始化分页信息
    */
    
    public void pageInfoInit() {
        if (pageNum == null || pageNum < 1) {
        pageNum = 1;
        }
        if (pageSize == null || pageSize < 0) {
        pageSize = 15;
        }
    }
    
    4. 源码分析
    
    我们可以在idea上直接看源码,也可以在码云下载源码项目分析,

    项目地址 : http://git.oschina.net/free/Mybatis_PageHelper

    我们从PageHelper.startPage(pageNum, pageSize)这个调用开始分析
    他调用的是这个方法

    public static Page 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;

    }

    其实这个方法就一个目的就是初始化Page对象,也就是setLocalPage(page)中完成初始化

    public abstract class PageMethod {

       protected static final ThreadLocal<Page> LOCAL_PAGE = new ThreadLocal<Page>();
       protected static boolean DEFAULT_COUNT = true;
    
       /**
        * 设置 Page 参数
        *
        * @param page
        */
       protected static void setLocalPage(Page page) {
           LOCAL_PAGE.set(page);
       }
    我们看到最终把实例化好的page对象放入到了ThreadLocal<Page>中,ThreadLocal之前我有写文章说过,他是线程本地变量,使用他不会存在并发问题,因为他会为每一个放在他里面的对象都开辟一块独立的内存空间。
    这个方法就结束了,我们发现后面只调用了new PageInfo<>(list)方法,并没有看到任何关于count查询的方法,那么这部分查询在哪里呢,其实是在实现了拦截器的PageInterceptor中
    

    public class PageInterceptor implements Interceptor {

       private volatile Dialect dialect;
       private String countSuffix = "_COUNT";
       protected Cache<String, MappedStatement> msCountMap = null;
       private String default_dialect_class = "com.github.pagehelper.PageHelper";
       @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();
           }
       }

    }

       
       这个方法里面实现了count查询和方法查询,但是其实它不仅仅是只有分页查询调用,部分页面也会调用,那他又是被谁调用的,我们继续前推
    
    
          public class Plugin implements InvocationHandler {
    
            private final Object target;
            private final Interceptor interceptor;
            private final Map<Class<?>, Set<Method>> signatureMap;
    
            private Plugin(Object target, Interceptor interceptor, Map<Class<?>, Set<Method>> signatureMap) {
              this.target = target;
              this.interceptor = interceptor;
              this.signatureMap = signatureMap;
            }
    
            public static Object wrap(Object target, Interceptor interceptor) {
              Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
              Class<?> type = target.getClass();
              Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
              if (interfaces.length > 0) {
                return Proxy.newProxyInstance(
                    type.getClassLoader(),
                    interfaces,
                    new Plugin(target, interceptor, signatureMap));
              }
              return target;
            }
    
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
              try {
                Set<Method> methods = signatureMap.get(method.getDeclaringClass());
                if (methods != null && methods.contains(method)) {
                  return interceptor.intercept(new Invocation(target, method, args));
                }
                return method.invoke(target, args);
              } catch (Exception e) {
                throw ExceptionUtil.unwrapThrowable(e);
              }
            }
我们看到调用他的类是Plugin类,而且是jdk动态代理来实现的,所以真相大白了,这个就是mybatis通过动态代理查询。回到PageInterceptor # intercept方法,他取出参数后通过skip方法判断要不要进行分页,我们看下skip方法的实现

```
public boolean skip(MappedStatement ms, Object parameterObject, RowBounds rowBounds) {
    if (ms.getId().endsWith(MSUtils.COUNT)) {
        throw new RuntimeException("在系统中发现了多个分页插件,请检查系统配置!");
    }
    Page page = pageParams.getPage(parameterObject, rowBounds);
    if (page == null) {
        return true;
    } else {
        //设置默认的 count 列
        if (StringUtil.isEmpty(page.getCountColumn())) {
            page.setCountColumn(pageParams.getCountColumn());
        }
        autoDialect.initDelegateDialect(ms);
        return false;
    }
}
```

他会调用getPage方法获取page,老铁们这个就和开头对应上了,因为开头时我们只是做一个赋值操作,没有做其他的,而这个通过判断有没有值来判断是不是分页,所以很合理。
如果不进行分页,那么就回调用
```
resultList = executor.query(ms, parameter, rowBounds, resultHandler, cacheKey, boundSql);
```
这个方法就是获取mapper文件中的sql来执行查询了。这些就是分页插件的基本原理了,现在回到最开始里的问题,数据量很大,我们要提速,要基于mybatis-Pagehelper新写功能
  1. 基于mybatis-Pagehelper的插件

    新建MidPageHelper类

     public class MidPageHelper extends PageMethod implements Dialect, BoundSqlInterceptor.Chain

    这个类和PageHelper一样,也就是在调用时初始化

    PageHelperHandler类
    这个类里面就是访写PageInterceptor类

    
     @Slf4j
     @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 PageHelperHandler implements MidExpandMybatisInterceptor {
    
         private volatile Dialect dialect;
         private String countSuffix = "_MID_COUNT";
         protected Cache<String, MappedStatement> msCountMap = null;
         private String default_dialect_class = "com.dst.mid.expand.page.MidPageHelper";
    
         @Override
         public Object intercept(Invocation invocation) throws Throwable {

    我们刚刚说

    resultList = executor.query(ms, parameter, rowBounds, resultHandler, cacheKey, boundSql);

    这个方法是最后的执行方法,其实这次分页的优化也是对这里进行处理
    我们在前面加一些逻辑,增加pageQuery方法

    public static <E> List<E> pageQuery方法(Dialect dialect, Executor executor, MappedStatement ms, Object parameter,
                                        RowBounds rowBounds, ResultHandler resultHandler,
                                        BoundSql boundSql, CacheKey cacheKey) throws SQLException, JSQLParserException {
        //生成分页的缓存 key
        CacheKey pageKey = cacheKey;
        //处理参数对象
        parameter = dialect.processParameterObject(ms, parameter, boundSql, pageKey);
        //调用方言获取分页 sql
        //String pageSql = dialect.getPageSql(ms, boundSql, parameter, rowBounds, pageKey);
        String pageSql = getPageQuerySql(boundSql.getSql(), PageHelperThreadLocal.getPage());
        BoundSql pageBoundSql = new BoundSql(ms.getConfiguration(), pageSql, boundSql.getParameterMappings(), parameter);
    
        Map<String, Object> additionalParameters = ExecutorUtil.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);
    }

    getPageSql方法就是这次优化的核心

    public static String getPageQuerySql(String sql, Page page) throws JSQLParserException {
        String innerSql = getInnerSql(sql);
        String fromSql = getFromSql(sql);
        String limit;
        if (page.getStartRow() == 0) {
            limit = "\n LIMIT ? ";
        } else {
            limit = "\n LIMIT ?, ? ";
        }
        String querySql = String.format("SELECT * FROM (%s) from_tbl INNER JOIN (%s \n %s) inner_tbl on from_tbl.id = inner_tbl.id", fromSql, innerSql, limit);
        log.info("querySql: {}", querySql);
        return querySql;
    }

    原理其实就是,先把要查看数据的id查出来,再把id结果集取一个别名和真正要查的所有数据关联获取结果,id在数据库里面是b+树中所以就算排序他也很快,如果用之前的方式,直接通过limit获取结果集再加上排序,时间就很长了,经过测试600w条数据从一分钟,优化到花费3s,缺点是对复杂查询还是有些不兼容,不过个人觉得瑕不掩瑜了。

相关文章
|
11天前
|
缓存 安全 PHP
【PHP开发专栏】Symfony框架核心组件解析
【4月更文挑战第30天】本文介绍了Symfony框架,一个模块化且高性能的PHP框架,以其可扩展性和灵活性备受开发者青睐。文章分为三部分,首先概述了Symfony的历史、特点和版本。接着,详细解析了HttpFoundation(处理HTTP请求和响应)、Routing(映射HTTP请求到控制器)、DependencyInjection(管理依赖关系)、EventDispatcher(实现事件驱动编程)以及Security(处理安全和认证)等核心组件。
|
18天前
|
SQL 数据库
数据库开发之子查询案例的详细解析
数据库开发之子查询案例的详细解析
13 0
|
18天前
|
SQL 数据库
数据库开发之子查询的详细解析
数据库开发之子查询的详细解析
18 0
|
18天前
|
SQL 存储 数据库
数据库开发表操作案例的详细解析
数据库开发表操作案例的详细解析
10 0
|
1天前
|
监控 供应链 数据可视化
深度解析BPM系统:优化业务流程,提升组织效率
本文探讨了业务流程管理系统(BPM)的核心价值和功能,以及低代码如何优化流程管理。BPM通过自动化和标准化流程,提高效率,降低技术复杂性,促进协作和监控。低代码平台加速了开发进程,增强了流程自动化,使得非专业开发者也能构建应用程序。结合低代码,企业能更轻松地适应市场变化,实现流程简化和业务增长。
7 1
|
4天前
|
Linux 开发工具 Android开发
移动应用与系统:开发与操作系统的深度解析
【5月更文挑战第6天】 在数字化时代,移动应用和操作系统是信息技术的核心组成部分。本文深入探讨了移动应用的开发过程、关键技术以及移动操作系统的架构和功能。通过对这些技术的详细分析,我们可以更好地理解移动应用和系统的工作原理,以及它们如何影响我们的生活和工作。
|
10天前
|
Java 数据库连接 数据库
Springboot整合mybatisPlus开发
MyBatis-Plus是一个MyBatis的增强工具,旨在简化开发和提高效率。它在不修改原有MyBatis的基础上提供额外功能。要将MyBatis-Plus集成到SpringBoot项目中,首先通过Maven添加mybatis-plus-boot-starter和相应数据库驱动依赖,然后配置application.yml中的数据库连接信息,并指定Mapper类的扫描路径。Mapper接口可继承BaseMapper实现基本的CRUD操作。
|
10天前
|
Dart 前端开发 开发者
【Flutter前端技术开发专栏】Flutter Dart语言基础语法解析
【4月更文挑战第30天】Dart是Google为Flutter框架打造的高效编程语言,具有易学性、接口、混入、抽象类等特性。本文概述了Dart的基础语法,包括静态类型(如int、String)、控制流程(条件、循环)、函数、面向对象(类与对象)和异常处理。此外,还介绍了库导入与模块使用,帮助开发者快速入门Flutter开发。通过学习Dart,开发者能创建高性能的应用。
【Flutter前端技术开发专栏】Flutter Dart语言基础语法解析
|
11天前
|
JSON 安全 Swift
【Swift开发专栏】Swift中的JSON解析与处理
【4月更文挑战第30天】本文介绍了Swift中的JSON解析与处理。首先,讲解了JSON的基础,包括其键值对格式和在Swift中的解析与序列化方法。接着,展示了如何使用`Codable`协议简化JSON操作,以及处理复杂结构的示例。通过这些内容,读者能掌握在Swift中高效地处理JSON数据的方法。
|
11天前
|
存储 数据库连接 PHP
【PHP开发专栏】深入解析PHP数据类型与运算符
【4月更文挑战第30天】本文深入探讨了PHP的编程基础——数据类型和运算符。PHP支持整型、浮点型、字符串、布尔型、数组、对象、资源等数据类型。运算符包括算术、字符串、赋值、比较、逻辑、位、错误控制及范围运算符。通过示例展示了如何计算圆面积、判断素数和求斐波那契数列,以帮助读者更好地理解和应用这些概念。

推荐镜像

更多