若依分页查询原理

使用若依框架的时候,在做分页查询的时候,由于一个业务可能会进行多个单表查询.这时候有个疑问,分页是对第一条查询生效,还是对后面所有的查询都生效呢?带着这个疑问,探究一下分页.

以若依的用户管理功能页面对用户的查询为例进行分析.

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()方法放在最前面

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值