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月前
|
弹性计算 运维 安全
优化管理与服务:操作系统控制平台的订阅功能解析
本文介绍了如何通过操作系统控制平台提升系统效率,优化资源利用。首先,通过阿里云官方平台开通服务并安装SysOM组件,体验操作系统控制平台的功能。接着,详细讲解了订阅管理功能,包括创建订阅、查看和管理ECS实例的私有YUM仓库权限。订阅私有YUM仓库能够集中管理软件包版本、提升安全性,并提供灵活的配置选项。最后总结指出,使用阿里云的订阅和私有YUM仓库功能,可以提高系统可靠性和运维效率,确保业务顺畅运行。
|
8月前
|
Java 数据库连接 数据库
Spring boot 使用mybatis generator 自动生成代码插件
本文介绍了在Spring Boot项目中使用MyBatis Generator插件自动生成代码的详细步骤。首先创建一个新的Spring Boot项目,接着引入MyBatis Generator插件并配置`pom.xml`文件。然后删除默认的`application.properties`文件,创建`application.yml`进行相关配置,如设置Mapper路径和实体类包名。重点在于配置`generatorConfig.xml`文件,包括数据库驱动、连接信息、生成模型、映射文件及DAO的包名和位置。最后通过IDE配置运行插件生成代码,并在主类添加`@MapperScan`注解完成整合
1371 1
Spring boot 使用mybatis generator 自动生成代码插件
|
8月前
|
Java 数据库连接 API
Java 对象模型现代化实践 基于 Spring Boot 与 MyBatis Plus 的实现方案深度解析
本文介绍了基于Spring Boot与MyBatis-Plus的Java对象模型现代化实践方案。采用Spring Boot 3.1.2作为基础框架,结合MyBatis-Plus 3.5.3.1进行数据访问层实现,使用Lombok简化PO对象,MapStruct处理对象转换。文章详细讲解了数据库设计、PO对象实现、DAO层构建、业务逻辑封装以及DTO/VO转换等核心环节,提供了一个完整的现代化Java对象模型实现案例。通过分层设计和对象转换,实现了业务逻辑与数据访问的解耦,提高了代码的可维护性和扩展性。
324 1
|
7月前
|
SQL Java 数据库连接
Spring、SpringMVC 与 MyBatis 核心知识点解析
我梳理的这些内容,涵盖了 Spring、SpringMVC 和 MyBatis 的核心知识点。 在 Spring 中,我了解到 IOC 是控制反转,把对象控制权交容器;DI 是依赖注入,有三种实现方式。Bean 有五种作用域,单例 bean 的线程安全问题及自动装配方式也清晰了。事务基于数据库和 AOP,有失效场景和七种传播行为。AOP 是面向切面编程,动态代理有 JDK 和 CGLIB 两种。 SpringMVC 的 11 步执行流程我烂熟于心,还有那些常用注解的用法。 MyBatis 里,#{} 和 ${} 的区别很关键,获取主键、处理字段与属性名不匹配的方法也掌握了。多表查询、动态
206 0
|
9月前
|
SQL Java 数据安全/隐私保护
发现问题:Mybatis-plus的分页总数为0,分页功能失效,以及多租户插件的使用。
总的来说,使用 Mybatis-plus 确实可以极大地方便我们的开发,但也需要我们理解其工作原理,掌握如何合适地使用各种插件。分页插件和多租户插件是其中典型,它们的运用可以让我们的代码更为简洁、高效,理解和掌握好它们的用法对我们的开发过程有着极其重要的意义。
851 15
|
10月前
|
SQL 存储 Java
Mybatis源码解析:详述初始化过程
以上就是MyBatis的初始化过程,这个过程主要包括SqlSessionFactory的创建、配置文件的解析和加载、映射文件的加载、SqlSession的创建、SQL的执行和SqlSession的关闭。这个过程涉及到了MyBatis的核心类和接口,包括SqlSessionFactory、SqlSessionFactoryBuilder、XMLConfigBuilder、XMLMapperBuilder、Configuration、SqlSession和Executor等。通过这个过程,我们可以看出MyBatis的灵活性和强大性,它可以很好地支持定制化SQL、存储过程以及高级映射,同时也避免了几
188 20
|
11月前
|
人工智能 API 开发者
HarmonyOS Next~鸿蒙应用框架开发实战:Ability Kit与Accessibility Kit深度解析
本书深入解析HarmonyOS应用框架开发,聚焦Ability Kit与Accessibility Kit两大核心组件。Ability Kit通过FA/PA双引擎架构实现跨设备协同,支持分布式能力开发;Accessibility Kit提供无障碍服务构建方案,优化用户体验。内容涵盖设计理念、实践案例、调试优化及未来演进方向,助力开发者打造高效、包容的分布式应用,体现HarmonyOS生态价值。
684 27
|
11月前
|
机器学习/深度学习 人工智能 JSON
Resume Matcher:增加面试机会!开源AI简历优化工具,一键解析简历和职位描述并优化
Resume Matcher 是一款开源AI简历优化工具,通过解析简历和职位描述,提取关键词并计算文本相似性,帮助求职者优化简历内容,提升通过自动化筛选系统(ATS)的概率,增加面试机会。
1390 18
Resume Matcher:增加面试机会!开源AI简历优化工具,一键解析简历和职位描述并优化
|
11月前
|
人工智能 API 语音技术
HarmonyOS Next~鸿蒙AI功能开发:Core Speech Kit与Core Vision Kit的技术解析与实践
本文深入解析鸿蒙操作系统(HarmonyOS)中的Core Speech Kit与Core Vision Kit,探讨其在AI功能开发中的核心能力与实践方法。Core Speech Kit聚焦语音交互,提供语音识别、合成等功能,支持多场景应用;Core Vision Kit专注视觉处理,涵盖人脸检测、OCR等技术。文章还分析了两者的协同应用及生态发展趋势,展望未来AI技术与鸿蒙系统结合带来的智能交互新阶段。
784 31
|
11月前
|
人工智能 小程序 前端开发
【一步步开发AI运动小程序】十九、运动识别中如何解析RGBA帧图片?
本文介绍了如何将相机抽取的RGBA帧图像解析为`.jpg`或`.png`格式,适用于体测、赛事等场景。首先讲解了RGBA图像结构,其为一维数组,每四个元素表示一个像素的颜色与透明度值。接着通过`uni.createOffscreenCanvas()`创建离屏画布以减少绘制干扰,并提供代码实现,将RGBA数据逐像素绘制到画布上生成图片。最后说明了为何不直接使用拍照API及图像转换的调用频率建议,强调应先暂存帧数据,运动结束后再进行转换和上传,以优化性能。

推荐镜像

更多
  • DNS