zoukankan      html  css  js  c++  java
  • 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); 即可
    查看答案
    不要害怕今日的苦,你要相信明天,更苦!
  • 相关阅读:
    左偏树
    论在Windows下远程连接Ubuntu
    ZOJ 3711 Give Me Your Hand
    SGU 495. Kids and Prizes
    POJ 2151 Check the difficulty of problems
    CodeForces 148D. Bag of mice
    HDU 3631 Shortest Path
    HDU 1869 六度分离
    HDU 2544 最短路
    HDU 3584 Cube
  • 原文地址:https://www.cnblogs.com/yougewe/p/13513892.html
Copyright © 2011-2022 走看看