PageHelper不再那么香了

PageHelper

  1. 什么是PageHelper

    pageHelper是一款分页插件,它能很好的集成在spring boot中在它是一个基于mybatis的一款插件。它是的底层实现技术则是使用动态代理实现的。所以我们在使用它时,我们需要使用mybatis作为持久层框架。

  2. PageHelper基本配置

    如果我们需要使用pageHelper的话,我们需要在spring boot项目中引入pageHelper的依赖。以下是PageHelper的GitHub地址https://github.com/pagehelper/Mybatis-PageHelper

    在这里我们导入以下依赖:

            <dependency>
                <groupId>com.github.pagehelper</groupId>
                <artifactId>pagehelper-spring-boot-starter</artifactId>
                <version>1.2.5</version>
            </dependency>
    

    同样,我们在spring boot的application.properties中可以配置pageHelper的基本信息,

    # 指定数据库,不指定的话会默认自动检测数据库类型
    pagehelper.helperDialect=mysql
    # 是否启用分页合理化。
    # 如果启用,当pagenum<1时,会自动查询第一页的数据,当pagenum>pages时,自动查询最后一页数据;
    # 不启用的,以上两种情况都会返回空数据
    pagehelper.reasonable=true
    # 默认值false,分页插件会从查询方法的参数值中,自动根据上面 params 配置的字段中取值,查找到合适的值时就会自动分页。
    pagehelper.supportMethodsArguments=true
    # 用于从对象中根据属性名取值,
    # 可以配置 pageNum,pageSize,count,pageSizeZero,reasonable,不配置映射的用默认值,
    # 默认值为pageNum=pageNum;pageSize=pageSize;count=countSql;reasonable=reasonable;pageSizeZero=pageSizeZero
    pagehelper.params=count=countSql
    

    在1.2.5这个版本中我们可以不用配置,使用默认的就够用了。

  3. PageHelper具体使用

    做好以上准备工作好,只需要在具体的业务查询代码前增加

    PageHelper.start(page,size);
    
  4. PageHelper分页原理
    @Intercepts(@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}))
    public class PageHelper implements Interceptor {
        //sql工具类
        private SqlUtil sqlUtil;
        //属性参数信息
        private Properties properties;
        //配置对象方式
        private SqlUtilConfig sqlUtilConfig;
        //自动获取dialect,如果没有setProperties或setSqlUtilConfig,也可以正常进行
        private boolean autoDialect = true;
        //运行时自动获取dialect
        private boolean autoRuntimeDialect;
        //多数据源时,获取jdbcurl后是否关闭数据源
        private boolean closeConn = true;
        //缓存
        private Map<String, SqlUtil> urlSqlUtilMap = new ConcurrentHashMap<String, SqlUtil>();
        private ReentrantLock lock = new ReentrantLock();
    // ...
    }
    

    SqlUtil:数据库类型专用sql工具类,一个数据库url对应一个SqlUtil实例,SqlUtil内有一个Parser对象,如果是mysql,它是MysqlParser,如果是oracle,它是OracleParser,这个Parser对象是SqlUtil不同实例的主要存在价值。执行count查询、设置Parser对象、执行分页查询、保存Page分页对象等功能,均由SqlUtil来完成。

    autoRuntimeDialect:多个数据源切换时,比如mysql和oracle数据源同时存在,就不能简单指定dialect,这个时候就需要运行时自动检测当前的dialect。

    Map<String, SqlUtil> urlSqlUtilMap:它就用来缓存autoRuntimeDialect自动检测结果的,key是数据库的url,value是SqlUtil。由于这种自动检测只需要执行1次,所以做了缓存。

    ReentrantLock lock:这个lock对象是比较有意思的现象,urlSqlUtilMap明明是一个同步ConcurrentHashMap,又搞了一个lock出来同步ConcurrentHashMap做什么呢?是否是画蛇添足?在《Java并发编程实战》一书中有详细论述,简单的说,ConcurrentHashMap可以保证put或者remove方法一定是线程安全的,但它不能保证put、get、remove的组合操作是线程安全的,为了保证组合操作也是线程安全的,所以使用了lock。

    public class PageStaticSqlSource extends PageSqlSource {
        private String sql;
        private List<ParameterMapping> parameterMappings;
        private Configuration configuration;
        private SqlSource original;
        
        @Override
        protected BoundSql getDefaultBoundSql(Object parameterObject) {
            String tempSql = sql;
            String orderBy = PageHelper.getOrderBy();
            if (orderBy != null) {
                tempSql = OrderByParser.converToOrderBySql(sql, orderBy);
            }
            return new BoundSql(configuration, tempSql, parameterMappings, parameterObject);
        }
    
        @Override
        protected BoundSql getCountBoundSql(Object parameterObject) {
            // localParser指的就是MysqlParser或者OracleParser
            // localParser.get().getCountSql(sql),可以根据原始的sql,生成一个count查询的sql
            return new BoundSql(configuration, localParser.get().getCountSql(sql), parameterMappings, parameterObject);
        }
    
        @Override
        protected BoundSql getPageBoundSql(Object parameterObject) {
            String tempSql = sql;
            String orderBy = PageHelper.getOrderBy();
            if (orderBy != null) {
                tempSql = OrderByParser.converToOrderBySql(sql, orderBy);
            }
            // getPageSql可以根据原始的sql,生成一个带有分页参数信息的sql,比如 limit ?, ?
            tempSql = localParser.get().getPageSql(tempSql);
            // 由于sql增加了分页参数的?号占位符,getPageParameterMapping()就是在原有List<ParameterMapping>基础上,增加两个分页参数对应的ParameterMapping对象,为分页参数赋值使用
            return new BoundSql(configuration, tempSql, localParser.get().getPageParameterMapping(configuration, original.getBoundSql(parameterObject)), parameterObject);
        }
    }
    

    getDefaultBoundSql:获取原始的未经改造的BoundSql。

    getCountBoundSql:不需要写count查询,插件根据分页查询sql,智能的为你生成的count查询BoundSql。

    getPageBoundSql:获取分页查询的BoundSql。

    public class MysqlParser extends AbstractParser {
        @Override
        public String getPageSql(String sql) {
            StringBuilder sqlBuilder = new StringBuilder(sql.length() + 14);
            sqlBuilder.append(sql);
            sqlBuilder.append(" limit ?,?");
            return sqlBuilder.toString();
        }
    
        @Override
        public Map<String, Object> setPageParameter(MappedStatement ms, Object parameterObject, BoundSql boundSql, Page<?> page) {
            Map<String, Object> paramMap = super.setPageParameter(ms, parameterObject, boundSql, page);
            paramMap.put(PAGEPARAMETER_FIRST, page.getStartRow());
            paramMap.put(PAGEPARAMETER_SECOND, page.getPageSize());
            return paramMap;
        }
    }
    

    这里在查询语句后面拼接limit用于分页。

    // PageSqlSource装饰原SqlSource   
    public void processMappedStatement(MappedStatement ms) throws Throwable {
            SqlSource sqlSource = ms.getSqlSource();
            MetaObject msObject = SystemMetaObject.forObject(ms);
            SqlSource pageSqlSource;
            if (sqlSource instanceof StaticSqlSource) {
                pageSqlSource = new PageStaticSqlSource((StaticSqlSource) sqlSource);
            } else if (sqlSource instanceof RawSqlSource) {
                pageSqlSource = new PageRawSqlSource((RawSqlSource) sqlSource);
            } else if (sqlSource instanceof ProviderSqlSource) {
                pageSqlSource = new PageProviderSqlSource((ProviderSqlSource) sqlSource);
            } else if (sqlSource instanceof DynamicSqlSource) {
                pageSqlSource = new PageDynamicSqlSource((DynamicSqlSource) sqlSource);
            } else {
                throw new RuntimeException("无法处理该类型[" + sqlSource.getClass() + "]的SqlSource");
            }
            msObject.setValue("sqlSource", pageSqlSource);
            //由于count查询需要修改返回值,因此这里要创建一个Count查询的MS
            msCountMap.put(ms.getId(), MSUtils.newCountMappedStatement(ms));
        }
    
    // 执行分页查询
    private Page doProcessPage(Invocation invocation, Page page, Object[] args) throws Throwable {
            //保存RowBounds状态
            RowBounds rowBounds = (RowBounds) args[2];
            //获取原始的ms
            MappedStatement ms = (MappedStatement) args[0];
            //判断并处理为PageSqlSource
            if (!isPageSqlSource(ms)) {
                processMappedStatement(ms);
            }
            //设置当前的parser,后面每次使用前都会set,ThreadLocal的值不会产生不良影响
            ((PageSqlSource)ms.getSqlSource()).setParser(parser);
            try {
                //忽略RowBounds-否则会进行Mybatis自带的内存分页
                args[2] = RowBounds.DEFAULT;
                //如果只进行排序 或 pageSizeZero的判断
                if (isQueryOnly(page)) {
                    return doQueryOnly(page, invocation);
                }
    
                //简单的通过total的值来判断是否进行count查询
                if (page.isCount()) {
                    page.setCountSignal(Boolean.TRUE);
                    //替换MS
                    args[0] = msCountMap.get(ms.getId());
                    //查询总数
                    Object result = invocation.proceed();
                    //还原ms
                    args[0] = ms;
                    //设置总数
                    page.setTotal((Integer) ((List) result).get(0));
                    if (page.getTotal() == 0) {
                        return page;
                    }
                } else {
                    page.setTotal(-1l);
                }
                //pageSize>0的时候执行分页查询,pageSize<=0的时候不执行相当于可能只返回了一个count
                if (page.getPageSize() > 0 &&
                        ((rowBounds == RowBounds.DEFAULT && page.getPageNum() > 0)
                                || rowBounds != RowBounds.DEFAULT)) {
                    //将参数中的MappedStatement替换为新的qs
                    page.setCountSignal(null);
                    BoundSql boundSql = ms.getBoundSql(args[1]);
                    args[1] = parser.setPageParameter(ms, args[1], boundSql, page);
                    page.setCountSignal(Boolean.FALSE);
                    //执行分页查询
                    Object result = invocation.proceed();
                    //得到处理结果
                    page.addAll((List) result);
                }
            } finally {
                ((PageSqlSource)ms.getSqlSource()).removeParser();
            }
    
            //返回结果
            return page;
        }
    

    msCountMap.put(ms.getId(), MSUtils.newCountMappedStatement(ms)),创建count查询的MappedStatement对象,并缓存于msCountMap。如果count=true,则执行count查询,结果total值保存于page对象中,继续执行分页查询。执行分页查询,将查询结果保存于page对象中,page是一个ArrayList对象。args[2] = RowBounds.DEFAULT,改变Mybatis原有分页行为;args[1] = parser.setPageParameter(ms, args[1], boundSql, page),改变原有参数列表(增加分页参数)。

    总结:PageHelper会使用ThreadLocal获取到同一线程中的变量信息,各个线程之间的Threadlocal不会相互干扰,也就是Thread1中的ThreadLocal1之后获取到Tread1中的变量的信息,不会获取到Thread2中的信息 所以在多线程环境下,各个Threadlocal之间相互隔离,可以实现,不同thread使用不同的数据源或不同的Thread中执行不同的SQL语句。所以,PageHelper利用这一点通过拦截器获取到同一线程中的预编译好的SQL语句之后将SQL语句包装成具有分页功能的SQL语句,并将其再次赋值给下一步操作,所以实际执行的SQL语句就是有了分页功能的SQL语句。

  5. PageHelper为什么不被使用

    虽然这个分页插件看起来极为方便,但是当数据量发生变大时,你就会发现,一条普普通通的查询竟然会消耗你这么多时间;

    当数据量大到一定量级后,比如有几个text字段的大表100万条数据的时候
    这个时候执行sql

    select * from table limit 1000000 10;
    

    只查了10条数据,但是非常非常慢!
    因为根据limit的执行原理,每次收到分页请求时,数据库都需要进行低效的全表扫描。这里是查出前80W+10条数据,然后截掉前面的80W条数据。

    什么是全表扫描?全表扫描 (又称顺序扫描) 就是在数据库中进行逐行扫描,顺序读取表中的每一行记录,然后检查各个列是否符合查询条件。这种扫描是已知最慢的,因为需要进行大量的磁盘 I/O,而且从磁盘到内存的传输开销也很大。

  6. PageHelper的替代方案

    • 基于指针的分页

      select * from table where id > 10 limit 10
      

      每次查询时,将最后一条记录主键值传给前端,当处理下一个分页查询请求时,使用上一个请求的主键作为筛选条件进行查询。

https://wuukee.github.io/posts/pagehelper.html

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
PageHelper是一个用于实现分页功能的插件,它可以帮助我们简化分页查询的代码。引用中的内容介绍了如何在项目中使用PageHelper插件。 首先,在pom.xml文件中导入相关的依赖包,具体的配置如下所示: ```xml <dependency> <groupId>com.github.pagehelper</groupId> <artifactId>pagehelper-spring-boot-starter</artifactId> <version>1.2.3</version> </dependency> ``` 接下来,需要在application.properties文件中进行配置,配置项包括数据库方言、分页参数等,具体配置如下所示: ```properties # pagehelper配置 pagehelper.helper-dialect=mysql pagehelper.reasonable=true pagehelper.support-methods-arguments=true pagehelper.params=count=countSql ``` 在控制器中,我们可以通过使用@RequestMapping注解来定义一个分页查询的接口,具体的代码如引用所示。在这个接口中,我们可以通过@RequestParam注解来获取前端传递的分页参数pageNum和pageSize,并对这些参数进行非空判断和默认值设置。然后,我们可以调用业务逻辑层的方法进行分页查询,并将查询结果和分页信息传递到前端模板中进行展示。 最后,需要在HTML页面中进行相应的展示,具体的HTML代码可以根据具体需求来编写。关于PageHelper插件的版本问题,引用中提到了一个修改过的版本,用来解决条件查询时可能出现的异常问题。这个修改过的版本可以从特定的来源获取,但在中央仓库是无法直接下载到的。 综上所述,PageHelper是一个用于实现分页功能的插件,通过配置和调用相应的方法,我们可以在项目中使用PageHelper来简化分页查询的代码。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* [使用PageHelper实现分页查询(详细)](https://blog.csdn.net/m0_48736673/article/details/124805124)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] - *3* [pagehelper](https://download.csdn.net/download/anaitudou/10513398)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值