前言
大家好我是 提前退休的java猿,今天在看八股文的时候,才发现实现分页的形式有这么多种!今天就分享一下用法和核心原理,特别是不是知道分页插件原理的同学,一定要点赞收藏。
实现分页的写法
一、分页插件:PageHelper
分页插件用的最多的就是 PageHelper, 今天简单看看PageHelper 的使用方式,以及原理,还有需要注意的问题!
使用方式
对应的jar包肯定是要有的,这个就不用代码展示了。直接看使用代码吧:
// 开启分页
PageHelper.startPage(1, 10);
// 紧接着写查询SQL,不需要写limit,eg: `select * from user`
List<User> userList = userMapper.selectAllUsers();
// 转成分页对象
PageInfo<User> pageInfo = new PageInfo<>(userList);
核心原理
-
PageHelper.startPage(XX)核心逻辑主要讲 页码,页码大小 封装成Page对象 存到ThreadLocal对象中。
startPage核心代码如下:public static <E> Page<E> startPage(int pageNum, int pageSize, boolean count, Boolean reasonable, Boolean pageSizeZero) { Page<E> page = new Page(pageNum, pageSize, count); page.setReasonable(reasonable); page.setPageSizeZero(pageSizeZero); Page<E> oldPage = getLocalPage(); if (oldPage != null && oldPage.isOrderByOnly()) { page.setOrderBy(oldPage.getOrderBy()); } setLocalPage(page); return page; }setLocalPage():
//ThreadLocal 对象 protected static final ThreadLocal<Page> LOCAL_PAGE = new ThreadLocal(); // 将page对象存到ThreadLocal中 protected static void setLocalPage(Page page) { LOCAL_PAGE.set(page); } -
SQL拦截,
PageInterceptor会拦截所有查询SQL,会去TreadLocal中查询是否有Page信息,有则需要进行分页处理;
核心逻辑代码:PageInterceptor.intercept:public Object intercept(Invocation invocation) throws Throwable { try { ................................. List resultList; // 1. 是否执行分页判断:true 跳过,返回默认查询结果,false 执行分页查询 if (!this.dialect.skip(ms, parameter, rowBounds)) { this.debugStackTraceLog(); // 2.是否执行count 判断 if (this.dialect.beforeCount(ms, parameter, rowBounds)) { // 3.执行count 查询 Long count = this.count(executor, ms, parameter, rowBounds, (ResultHandler)null, boundSql); // 4.是否继续进行查询判断:如果count =0 就不用再select了,直接返回空数组 if (!this.dialect.afterCount(count, parameter, rowBounds)) { Object var12 = this.dialect.afterPage(new ArrayList(), parameter, rowBounds); return var12; } } //5. 对SQL 重新处理,并执行select 分页查询 resultList = ExecutorUtil.pageQuery(this.dialect, executor, ms, parameter, rowBounds, resultHandler, boundSql, cacheKey); } else { resultList = executor.query(ms, parameter, rowBounds, resultHandler, cacheKey, boundSql); } Object var16 = this.dialect.afterPage(resultList, parameter, rowBounds); return var16; } finally { ........ } }
注意事项
-
必须紧跟查询语句,避免分页参数泄露,或者错误读取.
PageHelper.startPage()需直接放在查询方法前,避免中间插入其他逻辑导致ThreadLocal被错误读取。错误示例一:本身是对
queryList分页,当Condition为false时就会错误的对bannerList分页PageHelper.startPage(1,10); //当条件不满足时,不执行 if(condition){ List<SaBanner> banners = saBannerMapper.queryList(); } bannerMapper.bannerList(null,null); return null;错误示例二:开启了分页,但是没有执行select查询,引发分页参数泄露。当改线程被重用时,并且执行到不用分页的
select语句,也会会对改select语句进行分页处理,因为ThreadLocal中有分页信息。PageHelper.startPage(1,10); //当条件不满足时,不执行 if(condition){ List<SaBanner> banners = saBannerMapper.queryList(); } return Collections.emptyList();错误示例三:
PageHelper.startPage()后跟其他逻辑代码,如果中间的逻辑代码报错,那么也会导致 分页拦截器不执行,不能清理分页参数,引发分页参数泄露。PageHelper.startPage(1,10); //如果方法报错 function(); List<SaBanner> banners = saBannerMapper.queryList(); bannerMapper.bannerList(null,null); return null; -
count 覆盖与优化
(分页插件默认也会对统计sql 简单的优化,比如去除select 末尾的order by,详情见源码:CountSqlParser)。
在实际开发中,可能会遇到需要优化的分页查询的情况,当select无法优化的时候,可以考虑从count入手;PageInterceptor.count 代码如下:
private Long count(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException { String countMsId = countMsIdGen.genCountMsId(ms, parameter, boundSql, countSuffix); Long count; //先判断是否存在手写的 count 查询,ms.getId() + countSuffix(默认:_COUNT 可配置) MappedStatement countMs = ExecutorUtil.getExistedMappedStatement(ms.getConfiguration(), countMsId); if (countMs != null) { count = ExecutorUtil.executeManualCount(executor, countMs, parameter, boundSql, resultHandler); } else { ............................... }
二、MyBatis-Plus 分页插件
mybatis-plus 现在也用的很多,plus 分页插件入口是MybatisPlusInterceptor,它会遍历拦截器链。分页插件的具体实现是PaginationInnerInterceptor拦截器中。
使用方式
1.注册插件:
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor());
return interceptor;
}
2.mapper 编写SQL写法:分页参数封装到page对象中就行了,具体的SQL实现就不用管分页参数了
public interface BanerMapper extends BaseMapper<Banner> {
/**
* 分页列表查询
* @param page
* @param req
* @return
*/
IPage<Banner> queryList(Page page, @Param("req") QueryReq req);
调用baseMapper写好的方法:
<P extends IPage<T>> P selectPage(P page, @Param(Constants.WRAPPER) Wrapper<T> queryWrapper);
核心原理
分页插件的原理都差不多,都是实现Interceptor对SQL进行拦截,然后再处理。
plus的分页插件执行原理 和 PageHelper的执行原理类似,并且count sql 也能使用覆盖方式,也能通过参数指定,也会对自动生成的count sql 进行优化。
详情见历史文章:mybatis plus 分页原理剖析
分页拦截器实现在PaginationInnerInterceptor.java中,核心方法是beforeQuery:
public void beforeQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
IPage<?> page = ParameterUtils.findPage(parameter).orElse(null);
if (null == page) {
return;
}
// 处理 orderBy 拼接
boolean addOrdered = false;
String buildSql = boundSql.getSql();
List<OrderItem> orders = page.orders();
if (CollectionUtils.isNotEmpty(orders)) {
addOrdered = true;
buildSql = this.concatOrderBy(buildSql, orders);
}
// size 小于 0 且不限制返回值则不构造分页sql
Long _limit = page.maxLimit() != null ? page.maxLimit() : maxLimit;
if (page.getSize() < 0 && null == _limit) {
if (addOrdered) {
PluginUtils.mpBoundSql(boundSql).sql(buildSql);
}
return;
}
handlerLimit(page, _limit);
IDialect dialect = findIDialect(executor);
final Configuration configuration = ms.getConfiguration();
DialectModel model = dialect.buildPaginationSql(buildSql, page.offset(), page.getSize());
PluginUtils.MPBoundSql mpBoundSql = PluginUtils.mpBoundSql(boundSql);
List<ParameterMapping> mappings = mpBoundSql.parameterMappings();
Map<String, Object> additionalParameter = mpBoundSql.additionalParameters();
model.consumers(mappings, configuration, additionalParameter);
mpBoundSql.sql(model.getDialectSql());
mpBoundSql.parameterMappings(mappings);
}
注意事项
1.count 覆盖与优化
和PageHelper插件类似自动构建的count sql 某些情况不会进行优化。
哪些情况count 优化限制:
- SQL 中 有 这些集合操作的 INTERSECT,EXCEPT,MINUS,UNION 直接不优化count
- 包含groupBy 不去除orderBy
- order by 里带参数,不去除order by
- 查看select 字段中是否动态条件,如果有条件字段,则不会优化 Count SQL
- 包含 distinct、groupBy不优化
- 如果 left join 是子查询,并且子查询里包含 ?(代表有入参) 或者 where 条件里包含使用 join 的表的字段作条件,就不移除 join
- 如果 where 条件里包含使用 join 的表的字段作条件,就不移除 join
- 如果 join 里包含 ?(代表有入参) 就不移除 join
详情见历史文章:mybatis plus 分页原理剖析
三、RowBounds分页
MyBatis的RowBounds分页方式是一个用于基于内存的分页,它包含两个属性offset和limit,分别表示分页查询的偏移量和每页查询的数据条数。
使用方式
在使用RowBounds进行逻辑分页的时候,我们的SQL语句中是不需要指定分页参数的,在查询的时候,将RowBounds当做一个参数传递:
1.查询SQL getUserList:
<select id="getUsers" resultType="User">
select * from user
</select>
2.执行分页查询:
int offset = 10;
// 偏移量
int limit = 5;
// 每页数据条数
RowBounds rowBounds = new RowBounds(offset, limit);
List<User> userList = sqlSession.selectList("getUserList", null, rowBounds);
核心原理
MyBatis的RowBounds分页方式是一个用于基于内存的分页。会根据 RowBounds 对象中的 offset 和 limit 属性,对结果集进行截取。这个过程主要在 DefaultResultSetHandler 类的 handleRowValues 方法中实现。以下是简化后的代码逻辑::
private void handleRowValues(ResultSetWrapper rsw, ResultMap resultMap, ResultHandler<?> resultHandler, RowBounds rowBounds, ResultMapping parentMapping) throws SQLException {
if (resultMap.hasNestedResultMaps()) {
ensureNoRowBounds();
checkResultHandler();
handleRowValuesForNestedResultMap(rsw, resultMap, resultHandler, rowBounds, parentMapping);
} else {
handleRowValuesForSimpleResultMap(rsw, resultMap, resultHandler, rowBounds, parentMapping);
}
}
private void handleRowValuesForSimpleResultMap(ResultSetWrapper rsw, ResultMap resultMap, ResultHandler<?> resultHandler, RowBounds rowBounds, ResultMapping parentMapping) throws SQLException {
DefaultResultContext<Object> resultContext = new DefaultResultContext<>();
skipRows(rsw.getResultSet(), rowBounds);
while (shouldProcessMoreRows(resultContext, rowBounds) && rsw.getResultSet().next()) {
ResultMap discriminatedResultMap = resolveDiscriminatedResultMap(rsw.getResultSet(), resultMap, null);
Object rowValue = getRowValue(rsw, discriminatedResultMap);
storeObject(resultHandler, resultContext, rowValue, parentMapping, rsw.getResultSet());
}
}
//根据 `offset` 属性,跳过前面的记录
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++) {
rs.next();
}
}
}
//根据 `limit` 属性,判断是否已经获取到足够的记录
private boolean shouldProcessMoreRows(ResultContext<?> context, RowBounds rowBounds) {
return !context.isStopped() && context.getResultCount() < rowBounds.getLimit();
}
注意事项
不推荐使用
在查询出所有数据后在内存中进行分页,当数据量较大时,会占用大量的内存。
四、基于 SQL 语句的分页
这个就是最原始的方法了,相信大家都清楚,就不细说了。
查询SQL:
<select id="getUsersByPage" resultType="User">
SELECT * FROM users
LIMIT #{offset}, #{limit}
</select>
需要统计总数,就再写一个count SQL ,where条件一样就行了。
总结
目前我收集到的就这四种,同时也给大家分析了每种插件的原理,以及使用插件分页时候的注意事项和优化手段,还不赶快点赞收藏!🧡