【Mybatis】Mybatis源码之分页原理

3,399 阅读2分钟

关于Mybatis分页的实现方式,请移步Mybatis之分页查询

RowBounds原理

  • 当查询执行完成后,对结果集ResultSet进行处理时,如果此时使用了RowBounds分页,那么就会在结果集中,将游标移动到分页参数指向的起始位置。
  • 当结果集ResultSet的游标被移动后,在while循环中处理时,就会直接从游标指向的分页起始位置开始处理,被跳过的数据不在处理范围内。
  • while循环执行时,也会判断是否需要处理更多数据,此条件也是根据RowBounds参数中limit参数来判断的。

DefaultResultSetHandler

private void handleRowValuesForSimpleResultMap(ResultSetWrapper rsw, ResultMap resultMap, ResultHandler<?> resultHandler, RowBounds rowBounds, ResultMapping parentMapping)
        throws SQLException {
    DefaultResultContext<Object> resultContext = new DefaultResultContext<>();
    // 当前要处理的结果集
    ResultSet resultSet = rsw.getResultSet();
    // 根据RowBounds翻页配置,跳过指定的行
    skipRows(resultSet, rowBounds);
    // 持续处理下一条结果,判断条件为;还有结果需要处理 && 结果集没有关闭 && 还有下一条结果
    while (shouldProcessMoreRows(resultContext, rowBounds) && !resultSet.isClosed() && resultSet.next()) {
        // 经过鉴别器鉴别,确定经过鉴别器分析的最终要使用resultMap
        ResultMap discriminatedResultMap = resolveDiscriminatedResultMap(resultSet, resultMap, null);
        // 拿到一行记录,并且将其转化为一个对象
        Object rowValue = getRowValue(rsw, discriminatedResultMap, null);
        // 把这一行记录转化出的对象存起来
        storeObject(resultHandler, resultContext, rowValue, parentMapping, resultSet);
    }
}

private void skipRows(ResultSet rs, RowBounds rowBounds) throws SQLException {
    if (rs.getType() != ResultSet.TYPE_FORWARD_ONLY) {
        // 进入该分支表示结果的游标不是只能单步前进
        if (rowBounds.getOffset() != RowBounds.NO_ROW_OFFSET) {
            // 直接让游标移动到起始位置
            rs.absolute(rowBounds.getOffset());
        }
    } else {
        // 进入到该分支表示结果的游标只能单步前进
        for (int i = 0; i < rowBounds.getOffset(); i++) {
            if (!rs.next()) {
                break;
            }
        }
    }
}

private boolean shouldProcessMoreRows(ResultContext<?> context, RowBounds rowBounds) {
    return !context.isStopped() && context.getResultCount() < rowBounds.getLimit();
}

PageHelper原理

  • PageHelper则是利用Mybatis的插件原理,对Executor接口的query方法进行了拦截。

    @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 PageInterceptor implements Interceptor {
        ...
    }
    
  • 在创建Executor时,如果有插件对Executor进行拦截,则会对Executor对象生成代理,在执行相对应的方法时进行增强处理。

    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);
        }
        // 根据配置文件中的 settings 节点cacheEnabled配置项确定是否启用缓存
        if (cacheEnabled) { // 如果配置启用该缓存
            // 使用CachingExecutor装饰实际的执行器
            executor = new CachingExecutor(executor);
        }
        // 为执行器增加拦截器(插件),以启用各个拦截器的功能
        executor = (Executor) interceptorChain.pluginAll(executor);
        return executor;
    }
    
  • 当程序走到DefaultSqlSession中时,会调用Executor的query方法,此时就会触发PageHelper的代理,进入PageHelper的逻辑

    public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
        try {
            /**
             * 获取查询语句
             * 设置MappedStatement映射,见{@link XMLStatementBuilder#parseStatementNode()}
             */
            MappedStatement ms = configuration.getMappedStatement(statement);
            /**
             * 交由执行器进行查询,由于全局配置cacheEnabled默认是打开的,因此此处的executor通常都是CachingExecutor
             * 获取executor,见{@link Configuration#newExecutor(org.apache.ibatis.transaction.Transaction, org.apache.ibatis.session.ExecutorType)}
             */
            return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
        } catch (Exception e) {
            throw ExceptionFactory.wrapException("Error querying database.  Cause: " + e, e);
        } finally {
            ErrorContext.instance().reset();
        }
    }
    
  • PageHelper拦截器PageInterceptor执行流程

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        try {
            ...
            //调用方法判断是否需要进行分页,如果不需要,直接返回结果
            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);
            }
            // 将查询结果设置到Page对象中
            return dialect.afterPage(resultList, parameter, rowBounds);
        } finally {
            if(dialect != null){
                dialect.afterAll();
            }
        }
    }
    
    graph TD
    A{"分页?"} -->|是| B{"count查询?"}
    B --是--> C{"count结果?"}
    B --否--> D["Executor执行分页查询"]
    C --大于0--> D
    A --否--> F["Executor执行普通查询"]
    C --小于等于0--> E["返回空结果"]
    F --> G["将查询结果设置到Page对象中并返回"]
    D --> G
    

以上便是Mybatis中各种分页方式实现的原理。