PageHelper分页插件最新源码解读及使用

简介: PageHelper分页插件最新源码解读及使用

PageHelper分页插件最新源码解读及使用

相信有很多同学在开发过程中都使用过PageHelper,这是一款强大的分页插件,今天的文章会从以下几个角度来介绍PageHelper,分别为PageHelper的简单介绍使用场景、如何集成到mybatis中以及PageHelper源码解析。

还要在啰嗦一句,如果有使用经验的同学请直接跳转到源码解析~

1、PageHelper介绍

PageHelper是适用于MyBatis框架的一个分页插件,它支持基本主流与常用的数据库,如MySQL、Oracle、MariaDB、SQLite、Hsqldb等。

PageHelper的使用方式非常便捷,可以在原始SQL查询语句之前添加PageHelper.startPage(pageNum, pageSize);来启动分页。在查询结束后,通过PageInfo对象可以获取分页信息,如总记录数、总页数、每页大小等。

PageHelper的实现原理基于拦截器(Interceptor),在执行相关SQL之前会拦截并做分页处理。通过ThreadLocal机制,将分页参数保存在当前线程中,确保了分页参数的安全性和准确性。

PageHelper还提供了丰富的配置选项和自定义功能,可以根据实际需求进行灵活配置和使用。例如,可以配置是否支持带有“for update”的查询语句,是否支持嵌套查询等。

总的来说,PageHelper是一款功能强大且易于使用的分页插件,适用于MyBatis框架的分页处理,可以大大简化开发人员的工作量,提高开发效率。

2、PageHelper集成

相信很多同学在工作中可能会用到mybatis-plus,直接调用Mapper接口的selectPage方法就可以了实现分页了,那为啥还要用插件呢

比如:

@Override
    public Page<TaskAuth> pageTaskAuthVO(Page<TaskAuth> page, TaskAuthDTO taskAuthDTO) {
        QueryWrapper<TaskAuth> wrapper = new QueryWrapper<>();
        wrapper.eq("status",0);
        if (StringUtils.isNotEmpty(taskAuthDTO.getSysCode())){
            wrapper.like("sys_code",taskAuthDTO.getSysCode());
        }
        if (StringUtils.isNotEmpty(taskAuthDTO.getAuthFlag())){
            wrapper.eq("auth_flag",taskAuthDTO.getAuthFlag());
        }
        if (StringUtils.isNotEmpty(taskAuthDTO.getClientId())){
            wrapper.like("client_id",taskAuthDTO.getClientId());
        }
        Page<TaskAuth> selectedPage = taskAuthMapper.selectPage(page, wrapper);
        return selectedPage;
    }

上述代码直接调用taskAuthMapper.selectPage() 返回分页信息;

但是我们可以看到上述方法好像仅支持单表查询,QueryWrapper wrapper = new QueryWrapper<>(); 限定了Wrapper的泛型。

所以如果碰见几个表那种的关联查询还需要分页的话就比较麻烦,那怎么办呢,解决办法就是使用PageHelper分页插件来实现

2.1 引入PageHelper依赖
<dependency>
            <groupId>com.github.pagehelper</groupId>
            <artifactId>pagehelper-spring-boot-starter</artifactId>
            <version>2.1.0</version>
            <exclusions>
                <exclusion>
                    <groupId>org.mybatis</groupId>
                    <artifactId>mybatis</artifactId>
                </exclusion>
                <exclusion>
                    <groupId>org.mybatis</groupId>
                    <artifactId>mybatis-spring</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

有同学可以看到上述依赖排除了mybatis的相关配置,是因为我项目中也引入了mybatis-plus 会有依赖冲突,所以就排除了

建议大家还是使用相对较新的版本 我这里使用的1.4.7版本

大家也可以直接去maven官方仓库搜索 地址给大家贴这里了 https://mvnrepository.com/artifact/com.github.pagehelper/pagehelper-spring-boot-starter

2.2 PageHelper的使用

在Mapper.xml中实现自己的sql

我这里演示一个关联查询的

<select id="pageTaskSysVO" resultType="com.demo.center.vo.TaskSysVO">
        select
            tg.id ,
            tg.src_code ,
            tg.task_group_name ,
            tg.task_group_img_url ,
            tg.msg_flag_memo,
            tg.create_time ,
            tg.src_codes ,
            tg.sys_type ,
            ta.auth_flag ,
            ta.client_id
        from
            task_group tg
                left join task_auth ta on
                tg.src_code = ta.sys_code
        where
            1=1
        <if test="srcCode != null and srcCode != ''">
            and tg.src_code = #{srcCode}
        </if>
        <if test="taskGroupName != null and taskGroupName != ''">
            and tg.task_group_name like CONCAT('%',#{taskGroupName},'%')
        </if>
        <if test="authFlag != null and authFlag != ''">
            and ta.auth_flag = #{authFlag}
        </if>
        <if test="clientId != null and clientId != ''">
            and ta.client_id = #{clientId}
        </if>
        <if test="id != null and id != ''">
            and tg.id = #{id}
        </if>
            and tg.p_task_group_code = '1'
        order by
            create_time desc
    </select>

Mapper接口的实现

List<TaskSysVO> pageTaskSysVO(TaskSysDTO taskSysDTO);

service实现类实现

@Override
    public Page<TaskSysVO> pageTaskSysVO(TaskSysDTO taskSysDTO) {
        if (taskSysDTO.getCurrent() == null){
            throw new TaskCenterException("分页数据不能为空");
        }
        if (taskSysDTO.getSize() == null){
            throw new TaskCenterException("分页数据不能为空");
        }
        Page<TaskSysVO> page = new Page<>();
        PageHelper.startPage(Integer.valueOf(taskSysDTO.getCurrent().toString()),   Integer.valueOf(taskSysDTO.getSize().toString()));
        List<TaskSysVO> voList = taskGroupMapper.pageTaskSysVO(taskSysDTO);
        page.setTotal(new PageInfo(voList).getTotal());
        page.setRecords(voList);
        return page;
    }

主要代码为:PageHelper.startPage();

注意上述代码中,我把得到的分页信息给到了Page这个对象,是因为我得业务中用到的是这个分页对象, 大家可以直接返回 new PageInfo(voList);

下面给一个示例:

@Service
public class UserService {
    @Autowired
    private UserMapper userMapper;
    public PageInfo<User> findAll(int pageNum, int pageSize) {
        PageHelper.startPage(pageNum, pageSize);
        List<User> users = userMapper.findAll();
        return new PageInfo<>(users);
    }
}

这就搞定了,我们就可以得到如下的分页信息

3、PageHelper源码解读

3.1 PageHelper的工作原理

PageHelper的工作原理基于拦截器(Interceptor)。当调用PageHelper.startPage时,在当前线程上下文中设置一个ThreadLocal变量,用于存储分页参数。在查询执行时,PageHelper 会自动对查询语句进行拦截并进行分页处理。查询结束后,在finally语句中清除ThreadLocal中的查询参数。由于PageHelper方法使用了静态的ThreadLocal参数,分页参数和线程是绑定的,确保了在PageHelper方法调用后紧跟MyBatis查询方法的安全性。

3.2 PageHelper.startPage() 方法入口

我们点进这个方法里

一直跟一直跟,到这个方法 然后看到有一个setLocalPage(page);的方法

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);
        //当已经执行过orderBy的时候
        Page<E> oldPage = getLocalPage();
        if (oldPage != null && oldPage.isOrderByOnly()) {
            page.setOrderBy(oldPage.getOrderBy());
        }
        setLocalPage(page);
        return page;
    }

点进去,可以看到已经把分页对象set到了当前线程的上下文中

下面就来了解下sql是如何被拦截并且加上了分页语句

我这里专门下载了 pagehelper-spring-boot-starter 1.4.7 的源码包

感兴趣的同学可以点从这里下载:https://github.com/pagehelper/Mybatis-PageHelper

3.3 PageHelper初始化

我们首先找到PageHelperAutoConfiguration类,这个类就是PageHelper在springboot中的自动装配

可以看到上图,创建了一个PageInterceptor的拦截器对象,这个对象实现了Interceptor

然后把这个分页拦截器对象注册到了mybatis的配置文件中

3.4 那是怎么拦截的哩

mybatis在执行mapper方法时,创建Executor,执行pluginAll方法,然后会进入Interceptor的实现类PageInterceptorplugin方法。我们来看下PageInterceptor:

下面标一下整个流程的主要方法

当我们执行mapper时会进入MapperProxy的invoke方法

接下来会通过sqlSession执行查询方法

然后会创建一个执行sql语句的插件Executor

在接着跟代码,添加插件到拦截器执行链

这里使用的时候都是用动态代理将多个插件用责任链的方式添加的,最后返回的是一个代理对象,拿到分页插件的拦截器

最后动态代理生成和调用的过程都在 Plugin.wrap中

可以看到这个执行器对应了两个查询,方法返回的jdk动态代理类(增强了目标类)

public static Object wrap(Object target, Interceptor interceptor) {
        Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor); // 获取map key就是插件,value就是查询方法
        Class<?> type = target.getClass(); // 拦截目标(ParameterHandler|ResultSetHandler|StatementHandler|Executor)
        Class<?>[] interfaces = getAllInterfaces(type, signatureMap);// 获取目标接口
        // 生成代理
        return interfaces.length > 0 ? Proxy.newProxyInstance(type.getClassLoader(), interfaces, new Plugin(target, interceptor, signatureMap)) : target;
    }

然后代码会进入invoke方法

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        try {
            Set<Method> methods = (Set)this.signatureMap.get(method.getDeclaringClass()); // 取出拦截的目标方法
          // 判断这个方法是否在拦截范围内,在就拦截,不在就调用方法本身  
          return methods != null && methods.contains(method) ? this.interceptor.intercept(new Invocation(this.target, method, args)) : method.invoke(this.target, args);
        } catch (Exception var5) {
            throw ExceptionUtil.unwrapThrowable(var5);
        }
    }

mybatis添加插件的流程大致为:

mybatis 插件的拦截目标有四个,分别为 Executor、StatementHandler、ParameterHandler、ResultSetHandler 下面讲一下这四个插件的作用及区别:

  1. Executor
  • 作用:负责实际的 SQL 语句执行,包括事务管理和 SQL 语句的执行。
  • 区别:是核心插件,其他三个插件是为它服务的。
  1. StatementHandler
  • 作用:负责创建 JDBC 的 PreparedStatement 对象,并绑定参数。
  • 区别:它与 JDBC 的 PreparedStatement 紧密相关,负责设置 SQL 语句中的参数。
  1. ParameterHandler
  • 作用:负责设置参数到 JDBC 的 PreparedStatement 中。
  • 区别:它处理的是 SQL 语句中的参数部分,而 StatementHandler 负责的是整个 SQL 语句。
  1. ResultSetHandler
  • 作用:负责处理从数据库返回的结果集。
  • 区别:它处理的是从数据库查询返回的结果集,将结果集转换成 Java 对象。

最后是通过上面的invoke方法判断方法类型进入到了 PageInterceptor 也就是分页插件拦截器

方法大体如下:

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) {
                boundSql = ms.getBoundSql(parameter);
                cacheKey = executor.createCacheKey(ms, parameter, rowBounds, boundSql);
            } else {
                cacheKey = (CacheKey)args[4];
                boundSql = (BoundSql)args[5];
            }
            this.checkDialectExists();
            if (this.dialect instanceof BoundSqlInterceptor.Chain) {
                boundSql = ((BoundSqlInterceptor.Chain)this.dialect).doBoundSql(Type.ORIGINAL, boundSql, cacheKey);
            }
            List resultList;
            //调用方法判断是否需要进行分页,如果不需要,直接返回结果
            if (!this.dialect.skip(ms, parameter, rowBounds)) {
                this.debugStackTraceLog();
                //判断是否需要进行 count 查询
                if (this.dialect.beforeCount(ms, parameter, rowBounds)) {
                    Long count = this.count(executor, ms, parameter, rowBounds, (ResultHandler)null, boundSql);
                    if (!this.dialect.afterCount(count, parameter, rowBounds)) {
                        Object var12 = this.dialect.afterPage(new ArrayList(), parameter, rowBounds);
                        return var12;
                    }
                }
                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 {
            if (this.dialect != null) {
                this.dialect.afterAll();
            }
        }
    }
3.5 分页语句是怎么拼接到查询后面的呢

直接看这个page的查询方法

跟进这个方法里去,然后找到dialect.getPageSql()方法跟进去

接着走,找到getPageSql()方法,发现此时的sql还是没有分页的

返回执行到这个方法,this.getPageSql()

然后大家看到了吗?他为我们拼接上了limit语句

然后大家接着看这个方法的返回

最后大家可以看到这个sql的Parameters就是从这里打印的,查询执行的就是这个方法:method.invoke(this.statement, params);

到这里本篇文章就结束了,肯定还有很多不够完善的地方,如果有想到后面再补充。

最后送大家一句话白驹过隙,沧海桑田

相关文章
|
7月前
|
SQL Oracle 关系型数据库
若依框架---PageHelper分页(七)
若依框架---PageHelper分页(七)
162 0
|
7月前
|
SQL 数据库
若依框架---PageHelper分页(六)
若依框架---PageHelper分页(六)
177 0
|
7月前
|
SQL Java 数据库连接
若依框架---PageHelper分页(四)
若依框架---PageHelper分页(四)
176 0
|
2月前
|
SQL Java 数据库连接
Pagehelper超级好用的分页插件
Pagehelper超级好用的分页插件
202 0
|
7月前
|
SQL Oracle 关系型数据库
若依框架---PageHelper分页(五)
若依框架---PageHelper分页(五)
207 0
|
7月前
|
SQL Java 数据库连接
若依框架---PageHelper分页(三)
若依框架---PageHelper分页(三)
264 0
|
6月前
springboot2.4.5使用pagehelper分页插件
springboot2.4.5使用pagehelper分页插件
160 0
|
7月前
pagehelper分页插件
pagehelper分页插件
64 0
|
7月前
|
SQL 缓存
若依框架---PageHelper分页(九)
若依框架---PageHelper分页(九)
105 0
|
7月前
|
SQL 缓存
若依框架---PageHelper分页(八)
若依框架---PageHelper分页(八)
112 0