使用若依框架的时候,在做分页查询的时候,由于一个业务可能会进行多个单表查询.这时候有个疑问,分页是对第一条查询生效,还是对后面所有的查询都生效呢?带着这个疑问,探究一下分页.
以若依的用户管理功能页面对用户的查询为例进行分析.
1. 前端传入分页相关参数
先从前端开始,看下入参:
http://localhost/dev-api/system/user/list?pageNum=1&pageSize=10
2.startPage()开启分页前的准备
get请求,将分页参数拼接在url路径中,传递给后端.那么后端干了什么呢?
/**
* 获取用户列表
*/
@PreAuthorize("@ss.hasPermi('system:user:list')")
@GetMapping("/list")
public TableDataInfo list(SysUser user)
{
startPage();
List<SysUser> list = userService.selectUserList(user);
return getDataTable(list);
}
可以看到startPage()开启了分页.
一路点进去:
protected void startPage()
{
PageUtils.startPage();
}
// PageUtils
public static void startPage()
{
PageDomain pageDomain = TableSupport.buildPageRequest();
Integer pageNum = pageDomain.getPageNum();
Integer pageSize = pageDomain.getPageSize();
String orderBy = SqlUtil.escapeOrderBySql(pageDomain.getOrderBy());
Boolean reasonable = pageDomain.getReasonable();
PageHelper.startPage(pageNum, pageSize, orderBy).setReasonable(reasonable);
}
可以看到,若依使用的PageHelper的分页.接着点PageHelper.startPage(),就追着startPage一路点,最终可以看到PageMethod的这部分代码:
protected static final ThreadLocal<Page> LOCAL_PAGE = new ThreadLocal<Page>();
protected static boolean DEFAULT_COUNT = true;
/**
* 设置 Page 参数
*
* @param page
*/
protected static void setLocalPage(Page page) {
LOCAL_PAGE.set(page);
}
...
public static <E> Page<E> startPage(int pageNum, int pageSize) {
return startPage(pageNum, pageSize, DEFAULT_COUNT);
}
....
public static <E> Page<E> startPage(int pageNum, int pageSize, boolean count, Boolean reasonable, Boolean pageSizeZero) {
Page<E> page = new Page<E>(pageNum, pageSize, count);
page.setReasonable(reasonable);
page.setPageSizeZero(pageSizeZero);
//当已经执行过orderBy的时候
Page<E> oldPage = getLocalPage();
if (oldPage != null && oldPage.isOrderByOnly()) {
page.setOrderBy(oldPage.getOrderBy());
}
setLocalPage(page);
return page;
}
主要干了两件事:
1.调用startPage构造了一个带有pageNum,pageSize,count为True的Page对象
2.把那个Page对象保存到ThreadLocal中(ThreadLocal中的数据对于同一线程共享,具体的不是太清楚,有时间再研究)
3.PageInterceptor拦截器处理sql执行
重点就在PageInterceptor的intercept方法:
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();
//对 boundSql 的拦截处理
if (dialect instanceof BoundSqlInterceptor.Chain) {
boundSql = ((BoundSqlInterceptor.Chain) dialect).doBoundSql(BoundSqlInterceptor.Type.ORIGINAL, boundSql, cacheKey);
}
List resultList;
//调用方法判断是否需要进行分页,如果不需要,直接返回结果
if (!dialect.skip(ms, parameter, rowBounds)) {
//开启debug时,输出触发当前分页执行时的PageHelper调用堆栈
// 如果和当前调用堆栈不一致,说明在启用分页后没有消费,当前线程再次执行时消费,调用堆栈显示的方法使用不安全
debugStackTraceLog();
//判断是否需要进行 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);
}
return dialect.afterPage(resultList, parameter, rowBounds);
} finally {
// 清空paage信息
if (dialect != null) {
dialect.afterAll();
}
}
}
代码很长,但是对于我们的疑问,关键部分就一丢丢
//
if (!dialect.skip(ms, parameter, rowBounds)) {
...
resultList = ExecutorUtil.pageQuery(dialect, executor,
ms, parameter, rowBounds, resultHandler, boundSql, cacheKey);
}
就是那个skip方法,能进去走的就是分页查询,进不去就是普通查询.
那skip是怎么判断出来是分页还是不分页呢?
点进去看下实现:
有多个实现类,选择PageHelper的那个实现:
@Override
public boolean skip(MappedStatement ms, Object parameterObject, RowBounds rowBounds) {
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, page.getDialectClass());
return false;
}
}
pageParams.getPage(parameterObject, rowBounds):
由于startPage()设置了page,因此不走page==null的逻辑,这一块只是对page中的一些为空字段设置默认值
//Page page = pageParams.getPage(parameterObject, rowBounds);
public Page getPage(Object parameterObject, RowBounds rowBounds) {
Page page = PageHelper.getLocalPage();
if (page == null) {
.....
}
//分页合理化
if (page.getReasonable() == null) {
page.setReasonable(reasonable);
}
//当设置为true的时候,如果pagesize设置为0(或RowBounds的limit=0),就不执行分页,返回全部结果
if (page.getPageSizeZero() == null) {
page.setPageSizeZero(pageSizeZero);
}
if (page.getKeepOrderBy() == null) {
page.setKeepOrderBy(keepOrderBy);
}
if (page.getKeepSubSelectOrderBy() == null) {
page.setKeepSubSelectOrderBy(keepSubSelectOrderBy);
}
return page;
}
回到skip方法,显然page==null不成立,整个方法最终返回false,所以 !dialect.skip(ms, parameter, rowBounds=true,会去执行分页逻辑.
那这个page信息会一直存在在当前线程吗?如果存在,那么下一条sql不就还会分页吗?
这个page不会存在.
可以看到PageInterceptor的intercept方法最后的finally块:
可以看到,最后huiyichupage信息.
if (dialect != null) {
dialect.afterAll();
}
@Override
public void afterAll() {
//这个方法即使不分页也会被执行,所以要判断 null
AbstractHelperDialect delegate = autoDialect.getDelegate();
if (delegate != null) {
delegate.afterAll();
autoDialect.clearDelegate();
}
clearPage();
}
public static void clearPage() {
LOCAL_PAGE.remove();
}
在list接口里在加一句 List<SysUser> list2 = userService.selectUserList(user);
打个断点,看看会不会被分页:
@PreAuthorize("@ss.hasPermi('system:user:list')")
@GetMapping("/list")
public TableDataInfo list(SysUser user)
{
startPage();
List<SysUser> list = userService.selectUserList(user);
List<SysUser> list2 = userService.selectUserList(user);
return getDataTable(list);
}
跟上面一样,进入:
@Override
public boolean skip(MappedStatement ms, Object parameterObject, RowBounds rowBounds) {
//返回null
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, page.getDialectClass());
return false;
}
}
只不过pageParams.getPage(parameterObject, rowBounds)进入走的是:
public Page getPage(Object parameterObject, RowBounds rowBounds) {
Page page = PageHelper.getLocalPage();
if (page == null) {
if (rowBounds != RowBounds.DEFAULT) {
//条件不成立不执行此处逻辑
} else if (parameterObject instanceof IPage || supportMethodsArguments) {
try {
// 执行此处逻辑,返回null
page = PageObjectUtil.getPageFromObject(parameterObject, false);
} catch (Exception e) {
return null;
}
}
if (page == null) {
return null;
}
PageHelper.setLocalPage(page);
}
....
return page;
}
打断点发现supportMethodsArguments=true,执行的是: page = PageObjectUtil.getPageFromObject(parameterObject, false);
public static <T> Page<T> getPageFromObject(Object params, boolean required) {
if (params == null) {
throw new PageException("无法获取分页查询参数!");
}
if(params instanceof IPage){
// 条件不成立不执行此处逻辑
IPage pageParams = (IPage) params;
Page page = null;
...
return page;
}
int pageNum;
int pageSize;
MetaObject paramsObject = null;
if (hasRequest && requestClass.isAssignableFrom(params.getClass())) {
//条件不成立,不支持此处逻辑
...
} else {
// 条件成立执行此处逻辑,解析参数,最终结果不包含pageNum,pageSize
// 没有orderby
paramsObject = MetaObjectUtil.forObject(params);
}
if (paramsObject == null) {
throw new PageException("分页查询参数处理失败!");
}
Object orderBy = getParamValue(paramsObject, "orderBy", false);
boolean hasOrderBy = false;
if (orderBy != null && orderBy.toString().length() > 0) {
hasOrderBy = true;
}
try {
Object _pageNum = getParamValue(paramsObject, "pageNum", required);
Object _pageSize = getParamValue(paramsObject,"pageSize",required);
if (_pageNum == null || _pageSize == null) {
if(hasOrderBy){
Page page = new Page();
page.setOrderBy(orderBy.toString());
page.setOrderByOnly(true);
return page;
}
// 不满足上面的条件,不执行创建Page操作,直接返回null
return null;
}
...
} catch (NumberFormatException e) {
throw new PageException("分页参数不是合法的数字类型!", e);
}
....
return page;
}
最终:skip返回的是true,执行的是else分支的语句:即不采用分页查询.
else {
//rowBounds用参数值,不使用分页插件处理时,仍然支持默认的内存分页
resultList = executor.query(ms, parameter, rowBounds, resultHandler, cacheKey, boundSql);
}
4.总结
startPage()调用后只会在其后面执行的一句sql生效,后续sql不在执行分页操作.建议在startPage()方法紧靠分页结果的sql前,不要不管方法立有几个sql语句上来就加startPage()方法放在最前面