365bet最稳定网址-365bet官网注册开户-77365bet体育在线投注

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

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

走过路过不要错过

点击蓝字关注我们

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

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

1. pageHelper 的依赖引入

pom.xml 中引入pageHelper依赖:

1. 如果是springboot, 则可以直接引入 pagehelper-spring-boot-starter, 它会帮我们省去许多不必要的配置。

代码语言:javascript复制

com.github.pagehelper

pagehelper-spring-boot-starter

1.2.12

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

代码语言:javascript复制

com.github.pagehelper

pagehelper

5.1.10

2. pagehelper插件配置

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

代码语言:javascript复制# mybatis 相关配置

mybatis:

#... 其他配置信息

configuration-properties:

offsetAsPageNum: true

rowBoundsWithCount: true

reasonable: true

mapper-locations: mybatis/mapper/*.xml简单回顾看下db配置:

代码语言:javascript复制# 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/Shanghai2. 普通springmvc项目配置:mybatis-config.xml

代码语言:javascript复制

PUBLIC "-//mybatis.org//DTD Config 3.0//EN"

"http://mybatis.org/dtd/mybatis-3-config.dtd">

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

3. pagehelper 的使用

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

代码语言:javascript复制public Object getUsers(int pageNum, int pageSize) {

PageHelper.startPage(pageNum, pageSize);

// 不带分页的查询

List 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 中接入interceptorspringboot 中接入pagehelper非常简单, 主要受益于初始化的方式, 它会自动加载配置.

代码语言:javascript复制 // 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上下文后, 会在每次调用查询时进行拦截请求, 它的初始化也会在这时候触发.

代码语言:javascript复制// 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, Set> 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, Set> signatureMap) {

Set> 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拦截了.

代码语言:javascript复制// org.apache.ibatis.plugin.Plugin#invoke

@Override

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

try {

Set 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分页参数来决定的,没有分页参数设置,则跳过, 否则执行分页查询. 这算是分页的一个入口判定呢。

代码语言:javascript复制 /**

* 跳过 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 作为门面类进行接入, 而特殊地方则由具体方言实现.

代码语言:javascript复制// 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 resultMaps = new ArrayList();

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 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 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 COUNT_ITEM = new ArrayList();

// 如 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语句.

代码语言:javascript复制// 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 List 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 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 paramMap = null;

if (parameterObject == null) {

paramMap = new HashMap();

} else if (parameterObject instanceof Map) {

//解决不可变Map的情况

paramMap = new HashMap();

paramMap.putAll((Map) parameterObject);

} else {

paramMap = new HashMap();

//动态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 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 newParameterMappings = new ArrayList(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.

代码语言:javascript复制 // 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如何变换吧!

代码语言:javascript复制// com.github.pagehelper.dialect.helper.OracleDialect

public class OracleDialect extends AbstractHelperDialect {

@Override

public Object processPageParameter(MappedStatement ms, Map 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来共享分页信息,那么它是何时进行清除的呢?如果不清理那不就乱套了吗?思考完成后点击以下查看答案!

代码语言:javascript复制 // 实际上在每次运行完成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); 即可往期精彩推荐

腾讯、阿里、滴滴后台面试题汇总总结 — (含答案)面试:史上最全多线程面试题 !最新阿里内推Java后端面试题JVM难学?那是因为你没认真看完这篇文章—END—

相关推荐