searchusermenu
  • 发布文章
  • 消息中心
点赞
收藏
评论
分享
原创

PageHelper的使用及底层原理

2023-09-06 09:55:36
118
0

      业务系统在项目中引入 PageHelper 来实现分页方案,在实际使用过程中,发现在对 Mysql 数据库进行查询时,每条语句的末尾都自动拼接上了limit 子句,且每次查询会先执行一次“select count(0)...”语句,与预期不符,因此想探究一下 PageHelper 的底层原理。

1、PageHelper的使用

      PageHelper 是国内非常优秀的一款开源 mybatis 分页插件,它支持常用的主流数据库,例如 Oracle、Mysql、MariaDB、SQLite、Hsqldb 等。

      PageHelper 的安装很简单,只需要在 pom.xml 中加入以下依赖即可:

<dependency>
    <groupId>com.github.pagehelper</groupId>
    <artifactId>pagehelper</artifactId>
    <version>5.2.0</version>
</dependency>

      PageHelper 的使用也非常简单,只需要在查询之前调 PageHelper.startPage() 方法即可开始分页。例如:

// 开始分页
PageHelper.startPage(pageNum, pageSize);
// 查询
List<User> userList = userService.getUserList();
// 封装分页对象
PageInfo<User> pageInfo = new PageInfo<>(userList);

      其中,pageNum 表示要查询的页码,pageSize 表示每页的记录数。调用 startPage 方法之后,PageHelper 会自动将下一次查询作为分页查询,并且会在查询之后返回一个 Page 对象,然后可以将这个对象转换为 PageInfo 对象,从而获得分页相关的信息。

      这里其实存在两个问题:1、为什么查询之后会返回 Page 对象,而不是 List 对象?2、为什么不直接将 list 返回,而是需要封装一次再返回PageInfo 对象?这里我们稍后再回答,先继续说明一些 PageHelper 的一些使用技巧。

      Page page = PageHelper.startPage(pageNum, pageSize, true); - true 表示需要统计总数,这样会多进行一次请求 select count(0),不传默认为 true。

    1)统计总数(将SQL语句变为 select count(0) from xxx,只对简单SQL语句其效果,复杂SQL语句需要自己写)

    Page<?> page = PageHelper.startPage(1,-1);

    long count = page.getTotal();

   2)使用PageHelper查全部(不分页)

    PageHelper.startPage(1,0);

    List<?> allList = queryForList( xxx.class , "queryAll" , param);

2、PageHelper的底层原理

  • 首先调用 PageHelper 的 startPage 方法开启分页,方法中会将分页参数存到一个变量 ThreadLocal<Page> LOCAL_PAGE中;
  • 然后调用 mapper 进行查询,这里实际上会被 PageInterceptor 类拦截,执行其重写的 interceptor 方法,该方法中主要做了以下两件事:
    • 获取到 MappedStatement,拿到业务写好的 sql,将 sql 改造成 select count(0) 并执行查询,并将执行结果存到 LOCAL_PAGE 里的Page 中的 total 属性,表示总条数
    • 获取到 xml 中的 sql 语句,并 append 一些分页 sql 段,然后执行,将执行结果存到 LOCAL_PAGE 里的 Page 中的 list 属性,这里的Page 类实际是 ArrayList 的子类。
  • 可以看出,结果是封装到了 Page 中,最后交由 PageInfo,从中可以获取到总条数、总页数等参数。

2.1 分页参数储存

      首先看PageHelper.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;
    }

     其实主要就是把分页参数给到 Page ,然后将实例 Page 存储到 ThreadLocal 中。

public abstract class PageMethod {
    protected static final ThreadLocal<Page> LOCAL_PAGE = new ThreadLocal();

    public PageMethod() {
    }

    protected static void setLocalPage(Page page) {
        LOCAL_PAGE.set(page);
    }
}

      继续看执行 sql 是怎么做的

2.2 拦截器改造 SQL

2.2.1. 统计总数

      PageHelper 是通过拦截器底层执行 sql,对应的拦截器是 PageInterceptor,首先来看看这个类头部的定义,可以看出拦截了 Executor 的 query方法,毕竟 Mybatis 底层查询实际是借助 SqlSeesion 调用 Executor#query。

@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}
)})
public class PageInterceptor implements Interceptor {
    private static final Log log = LogFactory.getLog(PageInterceptor.class);
    private static boolean debug = false;
    protected Cache<String, MappedStatement> msCountMap = null;
    protected CountMsIdGen countMsIdGen;
    private volatile Dialect dialect;
    private String countSuffix;
    private String default_dialect_class;

      然后重点看下 intercept 方法,方法中传入的 Invocation 是JDK进行动态代理的时候,Plugin 类将反射信息封装到 Invocation 里,然后传给 intercept。

      看下在哪里进行了总条数查询,进入 count 方法内看下:

      继续追踪代码执行过程,发现进入 executeAutoCount 方法内,这个方法内有个变量为 countSql,其内容正是"select count(0)...",说明 PageHelper 在此处进行了总条数查询。

2.2.2 分页查询

      再看下 intercept 方法中如何进行如何分页查询:

      在 pageQuery 方法中进行实际查询操作:

      方法中的 pageSql 即为分页查询语句,看下 getPageSql 是如何实现的:

      getPageSql 这个方法会根据不同的数据库,对 sql 进行不同的改造,这里关注下 MySql 是如何改造的:

      很明显到这里就能看出 PageHelper 在针对分页查询时对每一个查询 sql 末尾都增加了 limit 子句,最大的一个问题得到了解决,代码后续就是拼接后Sql 的执行返回过程。值得注意的是,在 intercept 方法末尾的 finally 中调用 afferAll 方法对 ThreadLocal 进行 remove。

2.3 PageInfo

      实际代码中进行分页查询得到 list 之后,还要将其封装进 PageInfo 类中,才能获取到分页信息。我们关注下 PageInfo 中的构造器:

      在这段代码中,将 list 强转为 Page,再看下 Page 类中的设计,Page 类实际上是 ArrayList 的子类,且 Page 类中包含了分页的具体信息,而分页查询返回的 list 实际类型就是 Page,所以将其封装为 PageInfo 再返回也是合理且正确的。

3、安全性问题      

      PageHelper 的 startPage 方法使用了静态的 ThreadLocal 参数,分页参数和线程是绑定的。 只要保证在 startPage 方法调用后紧跟 MyBatis 查询方法,这就是安全的。因为 PageHelper 在 finally 代码段中自动清除了 ThreadLocal 存储的对象。但是例如下面这样的代码,就是不安全的用法:

PageHelper.startPage(1, 10);
List<Country> list;
if (param1 != null) {
    list = countryMapper.selectIf(param1);
} else {
    list = new ArrayList<Country>();
}

      这种情况下由于 param1 存在 null 的情况,就会导致 PageHelper 生产了一个分页参数,但是没有被消费,这个参数就会一直保留在这个线程上。当这个线程再次被使用时,就可能导致不该分页的方法去消费这个分页参数,这就产生了莫名其妙的分页。

      上面这个代码,应该写成下面这个样子:

List<Country> list;
if (param1 != null) {
    PageHelper.startPage(1, 10);
    list = countryMapper.selectIf(param1);
} else {
    list = new ArrayList<Country>();
}

      这种写法就能保证安全。当然也可以手动清理 ThreadLocal 存储的分页参数,如下代码所示,但是这种写法不好看,且没有必要,推荐上面的写法。

List<Country> list;
if (param1 != null) {
    PageHelper.startPage(1, 10);
    try {
        list = countryMapper.selectAll();
    } finally {
        PageHelper.clearPage();
    }
} else {
    list = new ArrayList<Country>();
}
0条评论
0 / 1000
lixx
3文章数
0粉丝数
lixx
3 文章 | 0 粉丝
lixx
3文章数
0粉丝数
lixx
3 文章 | 0 粉丝
原创

PageHelper的使用及底层原理

2023-09-06 09:55:36
118
0

      业务系统在项目中引入 PageHelper 来实现分页方案,在实际使用过程中,发现在对 Mysql 数据库进行查询时,每条语句的末尾都自动拼接上了limit 子句,且每次查询会先执行一次“select count(0)...”语句,与预期不符,因此想探究一下 PageHelper 的底层原理。

1、PageHelper的使用

      PageHelper 是国内非常优秀的一款开源 mybatis 分页插件,它支持常用的主流数据库,例如 Oracle、Mysql、MariaDB、SQLite、Hsqldb 等。

      PageHelper 的安装很简单,只需要在 pom.xml 中加入以下依赖即可:

<dependency>
    <groupId>com.github.pagehelper</groupId>
    <artifactId>pagehelper</artifactId>
    <version>5.2.0</version>
</dependency>

      PageHelper 的使用也非常简单,只需要在查询之前调 PageHelper.startPage() 方法即可开始分页。例如:

// 开始分页
PageHelper.startPage(pageNum, pageSize);
// 查询
List<User> userList = userService.getUserList();
// 封装分页对象
PageInfo<User> pageInfo = new PageInfo<>(userList);

      其中,pageNum 表示要查询的页码,pageSize 表示每页的记录数。调用 startPage 方法之后,PageHelper 会自动将下一次查询作为分页查询,并且会在查询之后返回一个 Page 对象,然后可以将这个对象转换为 PageInfo 对象,从而获得分页相关的信息。

      这里其实存在两个问题:1、为什么查询之后会返回 Page 对象,而不是 List 对象?2、为什么不直接将 list 返回,而是需要封装一次再返回PageInfo 对象?这里我们稍后再回答,先继续说明一些 PageHelper 的一些使用技巧。

      Page page = PageHelper.startPage(pageNum, pageSize, true); - true 表示需要统计总数,这样会多进行一次请求 select count(0),不传默认为 true。

    1)统计总数(将SQL语句变为 select count(0) from xxx,只对简单SQL语句其效果,复杂SQL语句需要自己写)

    Page<?> page = PageHelper.startPage(1,-1);

    long count = page.getTotal();

   2)使用PageHelper查全部(不分页)

    PageHelper.startPage(1,0);

    List<?> allList = queryForList( xxx.class , "queryAll" , param);

2、PageHelper的底层原理

  • 首先调用 PageHelper 的 startPage 方法开启分页,方法中会将分页参数存到一个变量 ThreadLocal<Page> LOCAL_PAGE中;
  • 然后调用 mapper 进行查询,这里实际上会被 PageInterceptor 类拦截,执行其重写的 interceptor 方法,该方法中主要做了以下两件事:
    • 获取到 MappedStatement,拿到业务写好的 sql,将 sql 改造成 select count(0) 并执行查询,并将执行结果存到 LOCAL_PAGE 里的Page 中的 total 属性,表示总条数
    • 获取到 xml 中的 sql 语句,并 append 一些分页 sql 段,然后执行,将执行结果存到 LOCAL_PAGE 里的 Page 中的 list 属性,这里的Page 类实际是 ArrayList 的子类。
  • 可以看出,结果是封装到了 Page 中,最后交由 PageInfo,从中可以获取到总条数、总页数等参数。

2.1 分页参数储存

      首先看PageHelper.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;
    }

     其实主要就是把分页参数给到 Page ,然后将实例 Page 存储到 ThreadLocal 中。

public abstract class PageMethod {
    protected static final ThreadLocal<Page> LOCAL_PAGE = new ThreadLocal();

    public PageMethod() {
    }

    protected static void setLocalPage(Page page) {
        LOCAL_PAGE.set(page);
    }
}

      继续看执行 sql 是怎么做的

2.2 拦截器改造 SQL

2.2.1. 统计总数

      PageHelper 是通过拦截器底层执行 sql,对应的拦截器是 PageInterceptor,首先来看看这个类头部的定义,可以看出拦截了 Executor 的 query方法,毕竟 Mybatis 底层查询实际是借助 SqlSeesion 调用 Executor#query。

@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}
)})
public class PageInterceptor implements Interceptor {
    private static final Log log = LogFactory.getLog(PageInterceptor.class);
    private static boolean debug = false;
    protected Cache<String, MappedStatement> msCountMap = null;
    protected CountMsIdGen countMsIdGen;
    private volatile Dialect dialect;
    private String countSuffix;
    private String default_dialect_class;

      然后重点看下 intercept 方法,方法中传入的 Invocation 是JDK进行动态代理的时候,Plugin 类将反射信息封装到 Invocation 里,然后传给 intercept。

      看下在哪里进行了总条数查询,进入 count 方法内看下:

      继续追踪代码执行过程,发现进入 executeAutoCount 方法内,这个方法内有个变量为 countSql,其内容正是"select count(0)...",说明 PageHelper 在此处进行了总条数查询。

2.2.2 分页查询

      再看下 intercept 方法中如何进行如何分页查询:

      在 pageQuery 方法中进行实际查询操作:

      方法中的 pageSql 即为分页查询语句,看下 getPageSql 是如何实现的:

      getPageSql 这个方法会根据不同的数据库,对 sql 进行不同的改造,这里关注下 MySql 是如何改造的:

      很明显到这里就能看出 PageHelper 在针对分页查询时对每一个查询 sql 末尾都增加了 limit 子句,最大的一个问题得到了解决,代码后续就是拼接后Sql 的执行返回过程。值得注意的是,在 intercept 方法末尾的 finally 中调用 afferAll 方法对 ThreadLocal 进行 remove。

2.3 PageInfo

      实际代码中进行分页查询得到 list 之后,还要将其封装进 PageInfo 类中,才能获取到分页信息。我们关注下 PageInfo 中的构造器:

      在这段代码中,将 list 强转为 Page,再看下 Page 类中的设计,Page 类实际上是 ArrayList 的子类,且 Page 类中包含了分页的具体信息,而分页查询返回的 list 实际类型就是 Page,所以将其封装为 PageInfo 再返回也是合理且正确的。

3、安全性问题      

      PageHelper 的 startPage 方法使用了静态的 ThreadLocal 参数,分页参数和线程是绑定的。 只要保证在 startPage 方法调用后紧跟 MyBatis 查询方法,这就是安全的。因为 PageHelper 在 finally 代码段中自动清除了 ThreadLocal 存储的对象。但是例如下面这样的代码,就是不安全的用法:

PageHelper.startPage(1, 10);
List<Country> list;
if (param1 != null) {
    list = countryMapper.selectIf(param1);
} else {
    list = new ArrayList<Country>();
}

      这种情况下由于 param1 存在 null 的情况,就会导致 PageHelper 生产了一个分页参数,但是没有被消费,这个参数就会一直保留在这个线程上。当这个线程再次被使用时,就可能导致不该分页的方法去消费这个分页参数,这就产生了莫名其妙的分页。

      上面这个代码,应该写成下面这个样子:

List<Country> list;
if (param1 != null) {
    PageHelper.startPage(1, 10);
    list = countryMapper.selectIf(param1);
} else {
    list = new ArrayList<Country>();
}

      这种写法就能保证安全。当然也可以手动清理 ThreadLocal 存储的分页参数,如下代码所示,但是这种写法不好看,且没有必要,推荐上面的写法。

List<Country> list;
if (param1 != null) {
    PageHelper.startPage(1, 10);
    try {
        list = countryMapper.selectAll();
    } finally {
        PageHelper.clearPage();
    }
} else {
    list = new ArrayList<Country>();
}
文章来自个人专栏
数据库技术
1 文章 | 1 订阅
0条评论
0 / 1000
请输入你的评论
0
0