• Mybatis分页插件: pageHelper的使用及其原理解析


      在实际工作中,很进行列表查询的场景,我们往往都需要做两个步骤:1. 查询所需页数对应数据;2. 统计符合条件的数据总数;而这,又会导致我们必然至少要写2个sql进行操作。这无形中增加了我们的工作量,另外,当发生需要变动时,我们又需要同时改动这两个sql,否则必然导致结果的不一致。

      因此,我们需要一个简单易用的分页工具来帮我们完成这个工作了,需求明确,至于如何实现则各有千秋。而我们要说的 pageHelper则是这其中实现比较好的一件的组件了,我们就一起来看看如何使用它进行提升工作效率吧!

    1. pageHelper 的依赖引入

      pom.xml 中引入pageHelper依赖:

      1. 如果是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.2.12</version>
            </dependency>

      2. 如果是普通的springmvc 类的项目,则引入 pageHelper 即可。

            <!-- pageHelper -->
            <dependency>
              <groupId>com.github.pagehelper</groupId>
              <artifactId>pagehelper</artifactId>
              <version>5.1.10</version>
            </dependency>

    2. pagehelper插件配置

      1. 如果是springboot,则直接配置几个配置项即可:

    # mybatis 相关配置
    mybatis:
      #... 其他配置信息
      configuration-properties:
        offsetAsPageNum: true
        rowBoundsWithCount: true
        reasonable: true
      mapper-locations: mybatis/mapper/*.xml
     

      简单回顾看下db配置:

    # db 配置
    spring:
      datasource:
        driver-class-name: com.mysql.cj.jdbc.Driver
        username: root
        password: 123
        url: jdbc:mysql://localhost:3306/testdb?useUnicode=true&charactorEncoding=utf8&&serverTimezone=Asia/Shanghai

      2. 普通springmvc项目配置: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>
      <plugins>
        <plugin interceptor="com.github.pagehelper.PageInterceptor">
          <!-- 该参数默认为false -->
          <!-- 设置为true时,会将RowBounds第一个参数offset当成pageNum页码使用 -->
          <!-- 和startPage中的pageNum效果一样-->
          <property name="offsetAsPageNum" value="true"/>
          <!-- 该参数默认为false -->
          <!-- 设置为true时,使用RowBounds分页会进行count查询 -->
          <property name="rowBoundsWithCount" value="true"/>
          <!-- 设置为true时,如果pageSize=0或者RowBounds.limit = 0就会查询出全部的结果 -->
          <!-- (相当于没有执行分页查询,但是返回结果仍然是Page类型)-->
          <property name="pageSizeZero" value="true"/>
          <!-- 3.3.0版本可用 - 分页参数合理化,默认false禁用 -->
          <!-- 启用合理化时,如果pageNum<1会查询第一页,如果pageNum>pages会查询最后一页 -->
          <!-- 禁用合理化时,如果pageNum<1或pageNum>pages会返回空数据 -->
          <property name="reasonable" value="true"/>
          <!-- 3.5.0版本可用 - 为了支持startPage(Object params)方法 -->
          <!-- 增加了一个`params`参数来配置参数映射,用于从Map或ServletRequest中取值 -->
          <!-- 可以配置pageNum,pageSize,count,pageSizeZero,reasonable,orderBy,不配置映射的用默认值 -->
          <!-- 不理解该含义的前提下,不要随便复制该配置 -->
          <property name="params" value="pageNum=start;pageSize=limit;"/>
          <!-- 支持通过Mapper接口参数来传递分页参数 -->
          <property name="supportMethodsArguments" value="true"/>
          <!-- always总是返回PageInfo类型,check检查返回类型是否为PageInfo,none返回Page -->
          <property name="returnPageInfo" value="check"/>
        </plugin>
      </plugins>
    </configuration>

      并在配置数据源的时候,将mybatis配置文件指向以上文件。

    3. pagehelper 的使用

      使用的时候,只需在查询list前,调用 startPage 设置分页信息,即可使用分页功能。

        public Object getUsers(int pageNum, int pageSize) {
            PageHelper.startPage(pageNum, pageSize);
            // 不带分页的查询
            List<UserEntity> list = userMapper.selectAllWithPage(null);
            // 可以将结果转换为 Page , 然后获取 count 和其他结果值
            com.github.pagehelper.Page listWithPage = (com.github.pagehelper.Page) list;
            System.out.println("listCnt:" + listWithPage.getTotal());
            return list;
        }

      即使用时, 只需提前声明要分页的信息, 得到的结果就是有分页信息的了. 如果不想进行count, 只要查分页数据, 则调用: PageHelper.startPage(pageNum, pageSize, false); 即可, 避免了不必要的count消耗.

    4. pageHelper 实现原理1: interceptor

      mybatis 有个插件机制,可以支持外部应用进行任意扩展。它在启动的时候会将 interceptor 添加到mybatis的上下文中。然后在进行查询时再触发实例化动作.

    4.1 springboot 中接入interceptor

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

        // com.github.pagehelper.autoconfigure.PageHelperAutoConfiguration#addPageInterceptor
        @PostConstruct
        public void addPageInterceptor() {
            // 初始化 com.github.pagehelper.PageInterceptor
            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) {
                // 添加inteceptor到 mybatis 中
                sqlSessionFactory.getConfiguration().addInterceptor(interceptor);
            }
        }
      // org.apache.ibatis.session.Configuration#addInterceptor
      public void addInterceptor(Interceptor interceptor) {
        interceptorChain.addInterceptor(interceptor);
      }
      
      // org.apache.ibatis.plugin.InterceptorChain#addInterceptor
      public void addInterceptor(Interceptor interceptor) {
        // 使用 ArrayList 保存intceptor
        interceptors.add(interceptor);
      }

      借助springboot的自动配置, 获取mybatis的sqlSessionFactoryList, 依次将 pagehelper 接入其中。

    4.2 interceptor的初始化

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

      // org.apache.ibatis.session.Configuration#newExecutor
      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);
        }
        // 以interceptorChain包装 executor, 以便inteceptor发挥作用
        executor = (Executor) interceptorChain.pluginAll(executor);
        return executor;
      }
      
      // org.apache.ibatis.plugin.InterceptorChain#pluginAll
      public Object pluginAll(Object target) {
        for (Interceptor interceptor : interceptors) {
          // 使用plugin一层层包装 target, 具体实现为使用代理包装 target
          // 所以, interceptor 的使用顺序是按照添加的顺序来的, 并不能自行设置
          target = interceptor.plugin(target);
        }
        return target;
      }
      
        // com.github.pagehelper.PageInterceptor#plugin
        @Override
        public Object plugin(Object target) {
            return Plugin.wrap(target, this);
        }
      // org.apache.ibatis.plugin.Plugin#wrap
      public static Object wrap(Object target, Interceptor interceptor) {
        // 获取注解中说明的方式列表 @Intercepts -> @Signature, 下面我们看 pageInterceptor的注解
        Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
        Class<?> type = target.getClass();
        // 过滤需要进行代理的接口, 而非全部代理
        Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
        if (interfaces.length > 0) {
          // 使用jdk方式生成动态代理
          return Proxy.newProxyInstance(
              type.getClassLoader(),
              interfaces,
              // 使用 Plugin 包装代理实现
              new Plugin(target, interceptor, signatureMap));
        }
        return target;
      }
      // pageInterceptor的注解, 即定义要拦截的方法列表
    @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}),
            }
    )
      // 过滤代理的接口
      private static Class<?>[] getAllInterfaces(Class<?> type, Map<Class<?>, Set<Method>> signatureMap) {
        Set<Class<?>> interfaces = new HashSet<>();
        while (type != null) {
          for (Class<?> c : type.getInterfaces()) {
            // 只有设置了的接口才会被添加
            if (signatureMap.containsKey(c)) {
              interfaces.add(c);
            }
          }
          type = type.getSuperclass();
        }
        return interfaces.toArray(new Class<?>[interfaces.size()]);
      }

      这样, interceptor 就和executor绑定了, 后续的查询将会看到interceptor 的作用.

    4.3 interceptor的调用过程

      在executor被代理后, 会继续执行查询动作, 这时就会被interceptor拦截了.

      // org.apache.ibatis.plugin.Plugin#invoke
      @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)) {
            // 匹配的方法会被拦截, 即 query 方法
            return interceptor.intercept(new Invocation(target, method, args));
          }
          return method.invoke(target, args);
        } catch (Exception e) {
          throw ExceptionUtil.unwrapThrowable(e);
        }
      }
        // pageHelper 正式起作用的入口
        // com.github.pagehelper.PageInterceptor#intercept
        @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();
    
                List resultList;
                //调用方法判断是否需要进行分页,如果不需要,直接返回结果
                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 {
                if(dialect != null){
                    dialect.afterAll();
                }
            }
        }

      以上就是 pageHelper 的大体执行框架了:

        1. 先解析各位置参数;
        2. 初始化 pageHelper 实例, 即 dialect;
        3. 调用方法判断是否需要进行分页,如果不需要,直接返回结果;
        4. 判断是否要进行count, 如果需要则实现一次count, ;
        5. 查询分页结果;
        6. 封装带分页的结果返回;

      下面我们就每个细节依次看看实现吧.

    4.4 是否跳过分页判定

      首先会进行是否需要跳过分页逻辑,如果跳过, 则直接执行mybatis的核心逻辑继续查询. 而是否要跳过分页, 则是通过直接获取page分页参数来决定的,没有分页参数设置,则跳过, 否则执行分页查询. 这算是分页的一个入口判定呢。

        /**
         * 跳过 count 和 分页查询
         *
         * @param ms              MappedStatement
         * @param parameterObject 方法参数
         * @param rowBounds       分页参数
         * @return true 跳过,返回默认查询结果,false 执行分页查询
         */
         // com.github.pagehelper.PageHelper#skip
        @Override
        public boolean skip(MappedStatement ms, Object parameterObject, RowBounds rowBounds) {
            if (ms.getId().endsWith(MSUtils.COUNT)) {
                throw new RuntimeException("在系统中发现了多个分页插件,请检查系统配置!");
            }
            // 如果 page 返回null, 则不需要进行分页, 即是否调用  PageHelper.start(pageNo, pageSize) 方法
            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;
            }
        }
        // com.github.pagehelper.page.PageAutoDialect#initDelegateDialect
        //多数据动态获取时,每次需要初始化
        public void initDelegateDialect(MappedStatement ms) {
            if (delegate == null) {
                if (autoDialect) {
                    // 比如 MySqlDialect
                    this.delegate = getDialect(ms);
                } else {
                    dialectThreadLocal.set(getDialect(ms));
                }
            }
        }
    
        /**
         * 获取分页参数
         */
        // com.github.pagehelper.page.PageParams#getPage
        public Page getPage(Object parameterObject, RowBounds rowBounds) {
            Page page = PageHelper.getLocalPage();
            if (page == null) {
                if (rowBounds != RowBounds.DEFAULT) {
                    if (offsetAsPageNum) {
                        page = new Page(rowBounds.getOffset(), rowBounds.getLimit(), rowBoundsWithCount);
                    } else {
                        page = new Page(new int[]{rowBounds.getOffset(), rowBounds.getLimit()}, rowBoundsWithCount);
                        //offsetAsPageNum=false的时候,由于PageNum问题,不能使用reasonable,这里会强制为false
                        page.setReasonable(false);
                    }
                    if(rowBounds instanceof PageRowBounds){
                        PageRowBounds pageRowBounds = (PageRowBounds)rowBounds;
                        page.setCount(pageRowBounds.getCount() == null || pageRowBounds.getCount());
                    }
                } else if(parameterObject instanceof IPage || supportMethodsArguments){
                    try {
                        page = PageObjectUtil.getPageFromObject(parameterObject, false);
                    } catch (Exception e) {
                        return null;
                    }
                }
                if(page == null){
                    return null;
                }
                PageHelper.setLocalPage(page);
            }
            //分页合理化
            if (page.getReasonable() == null) {
                page.setReasonable(reasonable);
            }
            //当设置为true的时候,如果pagesize设置为0(或RowBounds的limit=0),就不执行分页,返回全部结果
            if (page.getPageSizeZero() == null) {
                page.setPageSizeZero(pageSizeZero);
            }
            return page;
        }

      才上判定决定了后续的分页效果,主要是利用 ThreadLocal 来保存分页信息,从而与用户代码产生关联。

    4.5 pageHelper 的 count 操作

      判断是否是否需要count,  这些判定都会以 PageHelper 作为门面类进行接入, 而特殊地方则由具体方言实现.

        // com.github.pagehelper.PageHelper#beforeCount
        @Override
        public boolean beforeCount(MappedStatement ms, Object parameterObject, RowBounds rowBounds) {
            return autoDialect.getDelegate().beforeCount(ms, parameterObject, rowBounds);
        }
    
        // com.github.pagehelper.dialect.AbstractHelperDialect#beforeCount
        @Override
        public boolean beforeCount(MappedStatement ms, Object parameterObject, RowBounds rowBounds) {
            // 获取page参数信息, 该参数设置在 ThreadLocal 中
            Page page = getLocalPage();
            return !page.isOrderByOnly() && page.isCount();
        }
        // 如果需要进行count, 则需要自行组装count逻辑进行查询.
        // com.github.pagehelper.PageInterceptor#count
        private Long count(Executor executor, MappedStatement ms, Object parameter,
                           RowBounds rowBounds, ResultHandler resultHandler,
                           BoundSql boundSql) throws SQLException {
            // 在原有list 查询后添加  _COUNT 代表count查询id
            String countMsId = ms.getId() + countSuffix;
            Long count;
            //先判断是否存在手写的 count 查询
            MappedStatement countMs = ExecutorUtil.getExistedMappedStatement(ms.getConfiguration(), countMsId);
            if (countMs != null) {
                count = ExecutorUtil.executeManualCount(executor, countMs, parameter, boundSql, resultHandler);
            } else {
                countMs = msCountMap.get(countMsId);
                //自动创建
                if (countMs == null) {
                    //根据当前的 ms 创建一个返回值为 Long 类型的 ms
                    countMs = MSUtils.newCountMappedStatement(ms, countMsId);
                    msCountMap.put(countMsId, countMs);
                }
                count = ExecutorUtil.executeAutoCount(dialect, executor, countMs, parameter, boundSql, rowBounds, resultHandler);
            }
            return count;
        }
        // 创建count ms
        // com.github.pagehelper.util.MSUtils#newCountMappedStatement(org.apache.ibatis.mapping.MappedStatement, java.lang.String)
        public static MappedStatement newCountMappedStatement(MappedStatement ms, String newMsId) {
            // 直接基于原有 sql 构建新的 MappedStatement
            MappedStatement.Builder builder = new MappedStatement.Builder(ms.getConfiguration(), newMsId, ms.getSqlSource(), ms.getSqlCommandType());
            builder.resource(ms.getResource());
            // 注意此处并未使用到用户设置的分页参数 
            builder.fetchSize(ms.getFetchSize());
            builder.statementType(ms.getStatementType());
            builder.keyGenerator(ms.getKeyGenerator());
            if (ms.getKeyProperties() != null && ms.getKeyProperties().length != 0) {
                StringBuilder keyProperties = new StringBuilder();
                for (String keyProperty : ms.getKeyProperties()) {
                    keyProperties.append(keyProperty).append(",");
                }
                keyProperties.delete(keyProperties.length() - 1, keyProperties.length());
                builder.keyProperty(keyProperties.toString());
            }
            builder.timeout(ms.getTimeout());
            builder.parameterMap(ms.getParameterMap());
            //count查询返回值int
            List<ResultMap> resultMaps = new ArrayList<ResultMap>();
            ResultMap resultMap = new ResultMap.Builder(ms.getConfiguration(), ms.getId(), Long.class, EMPTY_RESULTMAPPING).build();
            resultMaps.add(resultMap);
            builder.resultMaps(resultMaps);
            builder.resultSetType(ms.getResultSetType());
            builder.cache(ms.getCache());
            builder.flushCacheRequired(ms.isFlushCacheRequired());
            builder.useCache(ms.isUseCache());
    
            return builder.build();
        }
    
        /**
         * 执行自动生成的 count 查询
         */
         // com.github.pagehelper.util.ExecutorUtil#executeAutoCount
        public static Long executeAutoCount(Dialect dialect, Executor executor, MappedStatement countMs,
                                            Object parameter, BoundSql boundSql,
                                            RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
            Map<String, Object> additionalParameters = getAdditionalParameter(boundSql);
            //创建 count 查询的缓存 key
            CacheKey countKey = executor.createCacheKey(countMs, parameter, RowBounds.DEFAULT, boundSql);
            //调用方言获取 count sql
            String countSql = dialect.getCountSql(countMs, boundSql, parameter, rowBounds, countKey);
            //countKey.update(countSql);
            BoundSql countBoundSql = new BoundSql(countMs.getConfiguration(), countSql, boundSql.getParameterMappings(), parameter);
            //当使用动态 SQL 时,可能会产生临时的参数,这些参数需要手动设置到新的 BoundSql 中
            for (String key : additionalParameters.keySet()) {
                countBoundSql.setAdditionalParameter(key, additionalParameters.get(key));
            }
            //执行 count 查询
            Object countResultList = executor.query(countMs, parameter, RowBounds.DEFAULT, resultHandler, countKey, countBoundSql);
            Long count = (Long) ((List) countResultList).get(0);
            return count;
        }
        // com.github.pagehelper.PageHelper#getCountSql
        @Override
        public String getCountSql(MappedStatement ms, BoundSql boundSql, Object parameterObject, RowBounds rowBounds, CacheKey countKey) {
            // 委托给各方言实现 sql 组装
            return autoDialect.getDelegate().getCountSql(ms, boundSql, parameterObject, rowBounds, countKey);
        }
    
        // com.github.pagehelper.dialect.AbstractHelperDialect#getCountSql
        @Override
        public String getCountSql(MappedStatement ms, BoundSql boundSql, Object parameterObject, RowBounds rowBounds, CacheKey countKey) {
            Page<Object> page = getLocalPage();
            String countColumn = page.getCountColumn();
            if (StringUtil.isNotEmpty(countColumn)) {
                return countSqlParser.getSmartCountSql(boundSql.getSql(), countColumn);
            }
            return countSqlParser.getSmartCountSql(boundSql.getSql());
        }
    
        /**
         * 获取智能的countSql
         *
         * @param sql
         * @param name 列名,默认 0
         * @return
         */
         // com.github.pagehelper.parser.CountSqlParser#getSmartCountSql(java.lang.String, java.lang.String)
        public String getSmartCountSql(String sql, String name) {
            //解析SQL
            Statement stmt = null;
            //特殊sql不需要去掉order by时,使用注释前缀
            if(sql.indexOf(KEEP_ORDERBY) >= 0){
                return getSimpleCountSql(sql, name);
            }
            try {
                stmt = CCJSqlParserUtil.parse(sql);
            } catch (Throwable e) {
                //无法解析的用一般方法返回count语句
                return getSimpleCountSql(sql, name);
            }
            Select select = (Select) stmt;
            SelectBody selectBody = select.getSelectBody();
            try {
                //处理body-去order by
                processSelectBody(selectBody);
            } catch (Exception e) {
                //当 sql 包含 group by 时,不去除 order by
                return getSimpleCountSql(sql, name);
            }
            //处理with-去order by
            processWithItemsList(select.getWithItemsList());
            //处理为count查询
            sqlToCount(select, name);
            String result = select.toString();
            return result;
        }
        /**
         * 将sql转换为count查询
         *
         * @param select
         */
         // com.github.pagehelper.parser.CountSqlParser#sqlToCount
        public void sqlToCount(Select select, String name) {
            SelectBody selectBody = select.getSelectBody();
            // 是否能简化count查询
            List<SelectItem> COUNT_ITEM = new ArrayList<SelectItem>();
            // 如 select * from user 将会被转化为 select count(0) from user
            COUNT_ITEM.add(new SelectExpressionItem(new Column("count(" + name +")")));
            if (selectBody instanceof PlainSelect && isSimpleCount((PlainSelect) selectBody)) {
                // 简单sql直接转换select字段为 count(0) 即可, 而这个sql是否支持这种方式则得仔细验证
                ((PlainSelect) selectBody).setSelectItems(COUNT_ITEM);
            } else {
                // 如果对于复杂的sql查询, 则只能在现有sql外围加一个 select count(0) from (xxxxx) as table_count
                PlainSelect plainSelect = new PlainSelect();
                SubSelect subSelect = new SubSelect();
                subSelect.setSelectBody(selectBody);
                subSelect.setAlias(TABLE_ALIAS);
                // 将原sql作为临时表放入 plainSelect 中
                plainSelect.setFromItem(subSelect);
                plainSelect.setSelectItems(COUNT_ITEM);
                // 替换原有 select
                select.setSelectBody(plainSelect);
            }
        }
        /**
         * 是否可以用简单的count查询方式
         */
         // net.sf.jsqlparser.statement.select.PlainSelect
        public boolean isSimpleCount(PlainSelect select) {
            //包含group by的时候不可以
            if (select.getGroupBy() != null) {
                return false;
            }
            //包含distinct的时候不可以
            if (select.getDistinct() != null) {
                return false;
            }
            for (SelectItem item : select.getSelectItems()) {
                //select列中包含参数的时候不可以,否则会引起参数个数错误
                if (item.toString().contains("?")) {
                    return false;
                }
                //如果查询列中包含函数,也不可以,函数可能会聚合列
                if (item instanceof SelectExpressionItem) {
                    Expression expression = ((SelectExpressionItem) item).getExpression();
                    if (expression instanceof Function) {
                        String name = ((Function) expression).getName();
                        if (name != null) {
                            String NAME = name.toUpperCase();
                            if(skipFunctions.contains(NAME)){
                                //go on
                            } else if(falseFunctions.contains(NAME)){
                                return false;
                            } else {
                                for (String aggregateFunction : AGGREGATE_FUNCTIONS) {
                                    if(NAME.startsWith(aggregateFunction)){
                                        falseFunctions.add(NAME);
                                        return false;
                                    }
                                }
                                skipFunctions.add(NAME);
                            }
                        }
                    }
                }
            }
            return true;
        }

      大体上讲就是分析sql, 如果是简单查询, 则直接将字段内容转换为 count(0) 即可, 这和我们普通认为的在select外部简单包一层还不太一样哦. 但是对于复杂查询咱们还是只能使用外包一层的实现方式了. 当然了,以上实现是针对mysql的,其他语言可能会有不一样的实现.

    4.6 select list 的改装

      在执行完count后, 分页的功能完成了一半. 我们可以给到用户这个计数值, 另外,我们可以根据该值得到后续分页还有多少数据, 如果没有自然不用再查了, 如果有则组装limit语句.

        // com.github.pagehelper.dialect.AbstractHelperDialect#afterCount
        @Override
        public boolean afterCount(long count, Object parameterObject, RowBounds rowBounds) {
            Page page = getLocalPage();
            page.setTotal(count);
            if (rowBounds instanceof PageRowBounds) {
                ((PageRowBounds) rowBounds).setTotal(count);
            }
            //pageSize < 0 的时候,不执行分页查询
            //pageSize = 0 的时候,还需要执行后续查询,但是不会分页
            if (page.getPageSize() < 0) {
                return false;
            }
            // 还没到最后一页, 则需要进行分页查询
            return count > ((page.getPageNum() - 1) * page.getPageSize());
        }
        
        /**
         * 分页查询
         */
        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;
                //处理参数对象, 将会加入 pageStart, pageSize 等参数
                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));
                }
                //执行分页查询
                return executor.query(ms, parameter, RowBounds.DEFAULT, resultHandler, pageKey, pageBoundSql);
            } else {
                //不执行分页的情况下,也不执行内存分页
                return executor.query(ms, parameter, RowBounds.DEFAULT, resultHandler, cacheKey, boundSql);
            }
        }
        // com.github.pagehelper.dialect.AbstractHelperDialect#processParameterObject
        @Override
        public Object processParameterObject(MappedStatement ms, Object parameterObject, BoundSql boundSql, CacheKey pageKey) {
            //处理参数
            Page page = getLocalPage();
            //如果只是 order by 就不必处理参数
            if (page.isOrderByOnly()) {
                return parameterObject;
            }
            Map<String, Object> paramMap = null;
            if (parameterObject == null) {
                paramMap = new HashMap<String, Object>();
            } else if (parameterObject instanceof Map) {
                //解决不可变Map的情况
                paramMap = new HashMap<String, Object>();
                paramMap.putAll((Map) parameterObject);
            } else {
                paramMap = new HashMap<String, Object>();
                //动态sql时的判断条件不会出现在ParameterMapping中,但是必须有,所以这里需要收集所有的getter属性
                //TypeHandlerRegistry可以直接处理的会作为一个直接使用的对象进行处理
                boolean hasTypeHandler = ms.getConfiguration().getTypeHandlerRegistry().hasTypeHandler(parameterObject.getClass());
                MetaObject metaObject = MetaObjectUtil.forObject(parameterObject);
                //需要针对注解形式的MyProviderSqlSource保存原值
                if (!hasTypeHandler) {
                    for (String name : metaObject.getGetterNames()) {
                        paramMap.put(name, metaObject.getValue(name));
                    }
                }
                //下面这段方法,主要解决一个常见类型的参数时的问题
                if (boundSql.getParameterMappings() != null && boundSql.getParameterMappings().size() > 0) {
                    for (ParameterMapping parameterMapping : boundSql.getParameterMappings()) {
                        String name = parameterMapping.getProperty();
                        if (!name.equals(PAGEPARAMETER_FIRST)
                                && !name.equals(PAGEPARAMETER_SECOND)
                                && paramMap.get(name) == null) {
                            if (hasTypeHandler
                                    || parameterMapping.getJavaType().equals(parameterObject.getClass())) {
                                paramMap.put(name, parameterObject);
                                break;
                            }
                        }
                    }
                }
            }
            return processPageParameter(ms, paramMap, page, boundSql, pageKey);
        }
    
        // 加入 page 参数
        // com.github.pagehelper.dialect.helper.MySqlDialect#processPageParameter
        @Override
        public Object processPageParameter(MappedStatement ms, Map<String, Object> paramMap, Page page, BoundSql boundSql, CacheKey pageKey) {
            // First_PageHelper, Second_PageHelper
            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, Integer.class).build());
                } else {
                    newParameterMappings.add(new ParameterMapping.Builder(ms.getConfiguration(), PAGEPARAMETER_FIRST, Integer.class).build());
                    newParameterMappings.add(new ParameterMapping.Builder(ms.getConfiguration(), PAGEPARAMETER_SECOND, Integer.class).build());
                }
                MetaObject metaObject = MetaObjectUtil.forObject(boundSql);
                metaObject.setValue("parameterMappings", newParameterMappings);
            }
            return paramMap;
        }
        // 组装分页sql
        // com.github.pagehelper.dialect.AbstractHelperDialect#getPageSql
        @Override
        public String getPageSql(MappedStatement ms, BoundSql boundSql, Object parameterObject, RowBounds rowBounds, CacheKey pageKey) {
            String sql = boundSql.getSql();
            Page page = getLocalPage();
            //支持 order by
            String orderBy = page.getOrderBy();
            if (StringUtil.isNotEmpty(orderBy)) {
                pageKey.update(orderBy);
                sql = OrderByParser.converToOrderBySql(sql, orderBy);
            }
            if (page.isOrderByOnly()) {
                return sql;
            }
            return getPageSql(sql, page, pageKey);
        }
        // com.github.pagehelper.dialect.helper.MySqlDialect#getPageSql
        @Override
        public String getPageSql(String sql, Page page, CacheKey pageKey) {
            StringBuilder sqlBuilder = new StringBuilder(sql.length() + 14);
            sqlBuilder.append(sql);
            // 分页sql拼接, limit xxx
            if (page.getStartRow() == 0) {
                sqlBuilder.append(" LIMIT ? ");
            } else {
                sqlBuilder.append(" LIMIT ?, ? ");
            }
            return sqlBuilder.toString();
        }
        

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

        // com.github.pagehelper.PageHelper#afterPage
        @Override
        public Object afterPage(List pageList, Object parameterObject, RowBounds rowBounds) {
            //这个方法即使不分页也会被执行,所以要判断 null
            AbstractHelperDialect delegate = autoDialect.getDelegate();
            if (delegate != null) {
                return delegate.afterPage(pageList, parameterObject, rowBounds);
            }
            return pageList;
        }
        
        // com.github.pagehelper.dialect.AbstractHelperDialect#afterPage
        @Override
        public Object afterPage(List pageList, Object parameterObject, RowBounds rowBounds) {
            // 取出本线程的page变量, 放入list
            Page page = getLocalPage();
            if (page == null) {
                return pageList;
            }
            page.addAll(pageList);
            // count 值临时变换, 用于应对没有进行count的场景, 使外部表现一致
            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;
        }

      至此, 一个完整的分页功能就完成了. 核心逻辑最开始也已看到, 就是判断是否需要分页, 是否需要count, 然后添加分页sql取数的这么个过程. 其本身并无太多银弹, 但却是能让我们节省不少时间. 另外就是, 在应对数据库可能发生切换的场景, 我们也可以无需更改此部分代码, 从而减轻了历史负担. 用用又何乐而不为呢?

      最后, 我们再来看下oracle的核心分页的时候, 以理解pagehelper 的良苦用心.

    5. oracle sql 变换

      前面我们以mysql为样例, 看了pagehelper的转换过程, 其核心自然是 对count和select sql 的变换. 下面我们看看oracle如何变换吧!

    // com.github.pagehelper.dialect.helper.OracleDialect
    public class OracleDialect extends AbstractHelperDialect {
    
        @Override
        public Object processPageParameter(MappedStatement ms, Map<String, Object> paramMap, Page page, BoundSql boundSql, CacheKey pageKey) {
            paramMap.put(PAGEPARAMETER_FIRST, page.getEndRow());
            paramMap.put(PAGEPARAMETER_SECOND, page.getStartRow());
            //处理pageKey
            pageKey.update(page.getEndRow());
            pageKey.update(page.getStartRow());
            //处理参数配置
            handleParameter(boundSql, ms);
            return paramMap;
        }
        // 获取带分页的sql
        @Override
        public String getPageSql(String sql, Page page, CacheKey pageKey) {
            StringBuilder sqlBuilder = new StringBuilder(sql.length() + 120);
            // 很明显, oracle 和 mysql 的分页实现是不一样的, oracle 使用 row_id 实现, 而 mysql 使用 limit 实现 
            sqlBuilder.append("SELECT * FROM ( ");
            sqlBuilder.append(" SELECT TMP_PAGE.*, ROWNUM ROW_ID FROM ( ");
            sqlBuilder.append(sql);
            sqlBuilder.append(" ) TMP_PAGE)");
            sqlBuilder.append(" WHERE ROW_ID <= ? AND ROW_ID > ?");
            return sqlBuilder.toString();
        }
    
    }

      从OracleDialect的实现中,我们看到它与mysql的差异仅在参数设置和获取分页sql时的差别, count 操作都是一样的. 虽然是这样, 但假设我们没有使用分页插件, 那么你会发现, 各个同学实现的count和分页查询相差甚大, 这必将给以后的改造带来许多麻烦, 这就没必要了.

      pagehelper 支持的几个方言如下:

       它们与oracle的实现方式都差不多,也就是说 count 都一样,只是分页的sql不一样而已。

      遗留个思考题:pagehelper通过ThreadLocal来共享分页信息,那么它是何时进行清除的呢?如果不清理那不就乱套了吗?思考完成后点击以下查看答案!

        // 实际上在每次运行完成pageInterceptor之后,都会在finnaly中进行一次清理工作
            try {
                // do page things
            } finally {
                // afterAll 即为清理任务
                if(dialect != null){
                    dialect.afterAll();
                }
            }
        // com.github.pagehelper.PageHelper#afterAll
        @Override
        public void afterAll() {
            //这个方法即使不分页也会被执行,所以要判断 null
            AbstractHelperDialect delegate = autoDialect.getDelegate();
            if (delegate != null) {
                // 默认为空
                delegate.afterAll();
                // delegate 移除,这里也是使用 ThreadLocal 实现,直接remove即可
                autoDialect.clearDelegate();
            }
            // 清理 page对象,下次不再有该设置,也就是说 page 设置是一次性的
            clearPage();
        }
        // com.github.pagehelper.page.PageMethod#clearPage
        public static void clearPage() {
            LOCAL_PAGE.remove();
        }
        // 下次再进行分页时,重新调用  PageHelper.startPage(x, x, x); 即可
    查看答案
    不要害怕今日的苦,你要相信明天,更苦!
  • 相关阅读:
    Oracle存储过程基础
    LoadRunner常用知识点-----LoadRunner日志输出
    Action(8):Error-26608:HTTP Status-Code=504(Gateway Time-out)
    Action(8):Error -27728:Step download timeout(120 seconds)has expired when downloading
    windows7无声音,提示未插入扬声器或耳机的解决
    解决loadrunner 脚本和replaylog中的中文乱码问题
    LoadRunner11-遇到问题及解决办法(汇总)
    java内存溢出分析工具
    Loadrunner乱码问题
    LoadRunner常用函数
  • 原文地址:https://www.cnblogs.com/yougewe/p/13513892.html
Copyright © 2020-2023  润新知