Pagehelper分页查询原理与实战:从配置到避坑全解析

发布时间:2026/10/14 21:26:31
Pagehelper分页查询原理与实战:从配置到避坑全解析 1. 项目概述Pagehelper分页查询是什么能解决什么问题分页是所有业务系统里几乎绕不开的一个需求。列表页、报表页、后台管理页只要是数据展示基本都得分页。早年用MyBatis写分页最原始的办法就是在SQL末尾手动拼LIMIT #{offset}, #{pageSize}每写一个查询就得复制粘贴一遍如果哪天表结构变了或者统计口径变了还得逐个排查SQL那滋味真的不好受。后来有了各种分页插件其中用起来最顺手、受众也最广的就是大伙儿熟知的Pagehelper。Pagehelper是MyBatis生态里的一个分页插件核心作用很简单在不改动原有SQL逻辑的前提下自动生成带分页参数的SQL并额外发出一次count查询拿到总记录数。你只需要在业务代码里设置页码和每页条数后面的事插件全包了。也就是说原来你要手写的东西SELECT * FROM orders WHERE status 1 ORDER BY create_time DESC LIMIT 0, 20 SELECT COUNT(*) FROM orders WHERE status 1现在可以完全不用碰SQL只写业务查询本身PageHelper.startPage(1, 20); ListOrder list orderMapper.selectByStatus(1)两句代码分页生效同时还能通过返回的PageInfo拿到当前页数据、总记录数、总页数、是否第一页/最后一页这些元数据。这个体验谁用谁知道。这篇文章适合哪些人看如果你是刚接触MyBatis分页的初学者可以把它当作一篇上手教程如果你已经在用Pagehelper但遇到一些诡异问题比如分页不生效、count结果不对、嵌套查询数据重复、多数据源下失效那接下来的排查经验应该能帮你少走不少弯路。我尽量把原理和实操串在一起讲不光告诉你怎么配也尽量讲清楚为什么这么配。2. 环境准备与基础配置2.1 引入依赖和插件配置的几种姿势不同项目用的构建工具不一样但引入Pagehelper的路径基本是一致的。Maven项目直接在pom.xml里加依赖dependency groupIdcom.github.pagehelper/groupId artifactIdpagehelper/artifactId version5.3.3/version /dependency如果你的项目用的是Spring Boot 3.x需要引入的是适配MyBatis 3.x的新包dependency groupIdcom.github.pagehelper/groupId artifactIdpagehelper-spring-boot-starter/artifactId version2.1.0/version /dependencyGradle项目对应改一下写法就行原理一样。依赖引入之后还需要把插件挂到MyBatis上。这里有三条路可选我分别说下适用场景方式一MyBatis全局XML配置原生MyBatis项目plugins plugin interceptorcom.github.pagehelper.PageInterceptor property namehelperDialect valuemysql/ property namereasonable valuetrue/ /plugin /plugins这种方式适合没有使用Spring Boot、直接裸配MyBatis的项目把所有配置集中在mybatis-config.xml里规则清晰。方式二Spring Boot自动配置推荐在application.yml或application.properties里加pagehelper: helper-dialect: mysql reasonable: true support-methods-arguments: true params: countcountSql这是目前最常见的用法尤其是配合pagehelper-spring-boot-starter使用几乎零配置就能跑起来。默认情况下starter会自动注入一个PageInterceptor实例不需要你再手动定义。方式三Java配置类手动注册如果项目里对MyBatis配置做了定制想手动控制插件加载顺序可以写一个配置类Configuration public class MyBatisConfig { Bean public PageInterceptor pageInterceptor() { PageInterceptor interceptor new PageInterceptor(); Properties properties new Properties(); properties.setProperty(helperDialect, mysql); properties.setProperty(reasonable, true); properties.setProperty(supportMethodsArguments, true); interceptor.setProperties(properties); return interceptor; } }不过有个坑需要注意如果项目里同时存在Spring Boot自动配置又手动定义了PageInterceptor很可能出现插件重复注册的问题导致每次查询被拦截两次分页参数行为变得混乱。所以用了starter就别再加手动配置反之亦然。2.2 关键参数详解helperDialect、reasonable与supportMethodsArguments很多新人配置Pagehelper时直接照抄网上的配置文件几个参数到底什么意思其实没搞明白。这里我把最关键的三个参数展开说下因为这直接决定了你在生产环境会不会踩坑。helperDialect指定数据库方言。这个参数的作用是让插件知道目标数据库的类型从而生成不同风格的分页SQL比如MySQL是LIMITOracle是ROWNUMPostgreSQL是LIMIT ... OFFSET。好的一面是插件会自动识别JDBC URL来判断方言但多个数据源混用或者连接池做了特殊路由时自动识别可能不准确最稳妥的做法还是手动指定。reasonable分页参数合理化。当这个值为true时如果请求页码小于1比如传了pageNum0或者-1插件会自动把页码校正为1如果请求页码大于总页数会自动把页码修正为最后一页。这个参数在面向用户的前端接口里非常有用因为你无法保证前端从来不会传一个越界的页码。但如果你的场景是需要严格校验边界、不允许用户随便翻到最后一页之外的那就保持false让越界请求直接返回空列表。supportMethodsArguments是否支持从Mapper接口方法参数中读取分页信息。默认值是false如果把它设为true允许你这样写ListOrder selectByStatus(Param(status) Integer status, Param(pageNum) Integer pageNum, Param(pageSize) Integer pageSize);插件会自动去匹配参数名中带有pageNum和pageSize的入参无需显式调用PageHelper.startPage()。这个模式适合团队统一封装分页参数对象的场景但我不建议在同一个项目里混用两种方式一会用startPage()一会依赖参数自动识别出了问题排查起来很麻烦因为你根本不知道这条查询的分页参数到底是从哪来的。注意supportMethodsArguments和PageHelper.startPage()同时存在时后者的优先级高于参数自动识别。但千万别在同一代码路径里两种方式都用实测下来容易出现分页参数被重复消费或者被覆盖的诡异行为。2.3 为什么推荐Spring Boot集成方式从我个人的项目经验来看现在新起的服务十有八九都是Spring Boot项目直接用pagehelper-spring-boot-starter是最省心的。它的自动配置做了几件事自动注册拦截器、自动加载配置文件中的pagehelper.*属性、自动适配当前MyBatis版本。最关键的是解决了版本匹配的问题。Pagehelper插件本身需要跟MyBatis版本兼容手动引入pagehelper包再去配MyBatis时经常遇到java.lang.NoSuchMethodError或者ClassNotFoundException这类版本冲突问题。starter把兼容性处理封装好了你基本不用关心MyBatis是3.5.6还是3.5.9它都能正常跑。不过有一点要提醒starter版本和插件版本不要混为一谈如果你需要通过Maven排除传递依赖来升级MyBatis版本要留意不要破坏了pagehelper对MyBatis的内部依赖关系。建议先看一下你引入的starter的pom.xml里到底传递了哪些依赖再动手排除。3. Pagehelper分页的核心机制与原理3.1 拦截器如何拦截SQL并改写Pagehelper本质上是一个MyBatis插件Interceptor。MyBatis允许你在四个核心组件上挂拦截器Executor、StatementHandler、ParameterHandler、ResultSetHandler。Pagehelper拦截的是Executor的query方法。当你调用PageHelper.startPage(1, 20)时Pagehelper做了什么说简单点就是在当前线程的ThreadLocal里存了一个分页参数对象包含页码、每页条数、是否count等。接下来你调用的Mapper方法在进入Executor查询前拦截器会检查ThreadLocal里有没有分页参数。如果有它先取出参数并清空ThreadLocal然后开始两个动作第一个动作执行一次count查询。它会根据当前要执行的原SQL生成对应的SELECT COUNT(*)语句比如把原SQL里的SELECT * FROM orders WHERE status 1 ORDER BY create_time DESC改写为SELECT COUNT(*) FROM orders WHERE status 1ORDER BY会被自动去除JOIN查询会尽量简化但具体简化的程度取决于SQL的复杂程度。第二个动作改写原SQL并执行分页查询。比如MySQL方言下会拼上LIMIT 0, 20。这里的0是根据你传入的pageNum1和pageSize20计算出来的offset(pageNum - 1) * pageSize 0。这两个动作发生在同一次查询的上下文中所以从调用者的视角看仿佛是一个Mapper方法同时返回了分页数据和总条数实际上内部执行了两次查询。3.2 PageInfo中的元数据到底是怎么算的执行完查询之后拿到的是当前页的数据列表。如果你想获取总记录数、总页数这些信息需要把结果封装为PageInfoPageInfoOrder pageInfo new PageInfo(list);PageInfo里面包含的字段大概有这么些pageNum、pageSize、size当前页实际返回条数、startRow、endRow、total总记录数、pages总页数、list当前页数据以及isFirstPage、isLastPage、hasPreviousPage、hasNextPage等辅助状态位。关键一点这里的total是从count查询里拿到的pages是用total和pageSize算出来的。计算方式pages total % pageSize 0 ? total / pageSize : total / pageSize 1这行逻辑本身没问题但有一个容易忽略的点PageInfo的构造函数会重新计算startRow和endRow而startRow是(pageNum - 1) * pageSize 1的语义跟SQL里的offset从0开始差了1。如果你把startRow直接当成SQL里的offset用会始终错一位这个细节做报表导出或手动拼接分页SQL时很容易踩。3.3 ThreadLocal机制与安全隐患上面提到Pagehelper用ThreadLocal来传递分页参数这个设计极大简化了API调用方式但也带来两个隐患。第一个隐患是线程池复用时参数错乱。如果某个请求设置了分页参数后Mapper查询还没执行线程就被归还到线程池下一次从线程池取出这条线程执行另一个查询时ThreadLocal里还留着上一个请求的分页参数就会把不该分页的查询强制分页了。第二个隐患是忘记消费导致的内存残留。Pagehelper在拦截器里取到分页参数后会主动把ThreadLocal清掉正常流程下不会残留。但如果你的代码出现了异常比如PageHelper.startPage()之后第一个就抛异常了那ThreadLocal里就不会被清除。后续这个线程再被复用就可能出现问题。所以严格来说官方文档也建议PageHelper.startPage()之后紧接着调用你的Mapper方法中间不要穿插其他可能有异常的代码。我自己在项目里做过一次事故复盘就是这类问题。某天晚上一个列表接口突然返回了全表数据排查下来发现是前一个请求在设置分页后、查询前抛了异常ThreadLocal残留了分页参数下一个请求复用了同一个线程但那条查询本来就不应该分页结果插件把PageInfo对象强塞了进来。好在那次是内部接口影响范围有限但也足够让人长记性。提示凡是使用PageHelper.startPage()的代码块建议包在try-finally里finally中显式调用PageHelper.clearPage()确保线程安全这是最稳的写法没有之一。4. 实操过程从零实现一个带分页的用户列表接口4.1 项目准备与表结构设计为了把整个过程串起来我设计了一个非常常见的用户查询场景后台管理系统的用户列表页支持按状态筛选、按创建时间倒序、分页展示。表结构简化如下CREATE TABLE sys_user ( id bigint(20) NOT NULL AUTO_INCREMENT COMMENT 主键ID, username varchar(50) NOT NULL COMMENT 用户名, nickname varchar(50) DEFAULT NULL COMMENT 昵称, status tinyint(4) NOT NULL DEFAULT 1 COMMENT 状态 1-正常 2-禁用, create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, PRIMARY KEY (id), KEY idx_status_create_time (status, create_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这里有一个小设计点idx_status_create_time这个联合索引是为了让WHERE status ? ORDER BY create_time DESC这条查询能够走索引避免到大数据量时候产生文件排序。分页接口在大数据量下的性能一大半取决于SQL本身能不能走好索引分页插件只是帮你改了SQL尾巴索引设计还得自己上心。4.2 实体类、Mapper接口与XML编写实体类public class SysUser { private Long id; private String username; private String nickname; private Integer status; private Date createTime; // getter/setter 略 }Mapper接口public interface SysUserMapper { ListSysUser selectUserPage(Param(status) Integer status); }Mapper XMLselect idselectUserPage resultTypecom.example.entity.SysUser SELECT id, username, nickname, status, create_time FROM sys_user WHERE 1 1 if teststatus ! null AND status #{status} /if ORDER BY create_time DESC /select大家可能注意到这个Mapper接口里根本没有分页参数也没有LIMIT分页完全靠调用方来控制这是Pagehelper最核心的使用方式——查询逻辑保持纯净分页是外挂的横切行为。4.3 Service层调用与PageInfo封装Service层的代码如下public PageInfoSysUser getUserPage(Integer status, int pageNum, int pageSize) { try { PageHelper.startPage(pageNum, pageSize); ListSysUser users sysUserMapper.selectUserPage(status); return new PageInfo(users); } finally { PageHelper.clearPage(); } }这段代码虽然短但承载了很多细节拆开说PageHelper.startPage(pageNum, pageSize)把分页参数塞进ThreadLocal。注意pageSize一定得是合法正整数如果你传了0插件会视为不合法默认不再分页这一点会直接导致查询返回全表。sysUserMapper.selectUserPage(status)实际查询触发拦截器执行count和分页查询。new PageInfo(users)把分页数据连同元数据包装成PageInfo对象。finally块中执行PageHelper.clearPage()清除ThreadLocal残留防止异常情况下参数泄漏。Controller层GetMapping(/users) public ResultPageInfoSysUser list(RequestParam(required false) Integer status, RequestParam(defaultValue 1) int pageNum, RequestParam(defaultValue 20) int pageSize) { return Result.success(userService.getUserPage(status, pageNum, pageSize)); }前端拿到的响应就是一个标准的分页模型total、pages、list、pageNum、pageSize可以直接配合各类前端表格组件使用基本不需要再二次加工。4.4 执行日志与SQL输出分析为了验证插件真的生效建议在开发环境打开MyBatis的SQL日志。在application.yml里配置logging: level: com.example.mapper: debug当你请求/users?status1pageNum2pageSize10时日志里会输出两条SQL。第一条count查询大约长这样SELECT COUNT(*) FROM sys_user WHERE status 1第二条数据查询SELECT id, username, nickname, status, create_time FROM sys_user WHERE status 1 ORDER BY create_time DESC LIMIT 10, 10注意LIMIT的计算(pageNum - 1) * pageSize (2 - 1) * 10 10所以是从第11条开始取10条。这两条SQL的日志顺序是先count再数据因为插件要先知道总条数才能计算total然后执行真正的数据查询。有一点比较隐蔽如果想禁用某次分页查询的count可以通过PageHelper.startPage(pageNum, pageSize, false)来关闭。第三个参数count为false时插件不会再发count查询只改写SQL加分页但PageInfo里的total会是0。这个适用于那些根本不需要总条数的场景比如无限滚动加载、前端手动触底加载更多省掉一次count查询能减轻数据库压力尤其大数据量时收益明显。5. 常见问题与排查技巧实录5.1 分页不生效的典型原因与处理这是群里被问得最多的一个问题明明调了PageHelper.startPage(1, 10)查询结果还是全表返回。排查思路可以按顺序过一遍原因一startPage之后紧跟着的查询不是Mapper查询。PageHelper.startPage(1, 10); // 中间做了一些其他操作比如调用了一个不带分页的查询或者业务判断 someService.doSomething(); ListSysUser users userMapper.selectUserPage(status);Pagehelper的拦截器取ThreadLocal时分页参数是有讲究的它只对紧跟startPage之后的第一个Executor查询生效。如果你在中间穿插了其他查询分页参数被那个查询消费掉了后面的真正要分页的查询自然就没分页效果了。而且这个参数是一次性的第一个查询会用掉它用完即焚。原因二查询方法内部自己又调用了其他Mapper查询。比如在一个Service方法里先查了角色表再查用户表PageHelper.startPage(1, 10); roleMapper.selectAllRoles(); // 这个查询消费了分页参数 userMapper.selectUserPage(status); // 这个查询不再分页这个坑特别隐蔽尤其是Mapper方法里嵌套调用另一条查询比如resultMap里有collection子查询或者代码中调了另一个Mapper查询来补充数据都会提前消费掉分页参数。原因三SQL里本身带了大偏移量的LIMIT。select idselectUserPage resultTypecom.example.entity.SysUser SELECT ... FROM sys_user LIMIT 5 /select如果原始SQL里已经有了LIMIT关键字插件改写时可能会冲突最终结果无法保证。所以使用Pagehelper的分页查询SQL里不要自己写LIMIT这也算一条铁律。原因四插件没有被正确注册。如果你用的是原生MyBatis而不带starter依赖忘了在mybatis-config.xml里注册插件或者属性配置名称写错比如把helperDialect拼写成dialect你会发现startPage完全无效。这种情况从日志里看不出来只能检查配置是否确实加载了。5.2 count查询结果不准的场景复盘count查询结果不对是另一个高频痛点。我遇到过的最典型情况是关联查询比如SELECT u.id, u.username, r.role_name FROM sys_user u LEFT JOIN sys_role r ON u.role_id r.id这里如果sys_user和sys_role是一对多关系一个用户有多个角色那么count出来的是连表后的行数而不是用户数。比如100个用户其中20个用户各绑定了3个角色原来count的结果会是100 20*2 140。Pagehelper的count优化会尝试把外面的SELECT部分替换成SELECT COUNT(*)但它无法判断你的业务语义到底是想要用户数还是连表行数。这种场景我不建议依赖插件的自动count更靠谱的做法是关闭自动count自己另外写一条精确的count查询把总数通过PageInfo或者自己封装的结果集里带回去。另外有个参数countSuffix可以指定count查询的Mapper方法后缀比如默认的count查询方法名带_COUNT后缀你会发现某些复杂查询下插件生成不了count SQL就会尝试去查一个名字类似的方法。如果实在搞不定就是把count参数设为false手动控制。5.3 嵌套查询导致的数据重复问题这个问题的场景一般是分页查询的是主表主表的resultMap里用collection嵌套了子表集合。举个例子分页查订单每条订单需要带出它的商品明细resultMap idOrderWithItems typecom.example.entity.Order id propertyid columnid/ result propertyorderNo columnorder_no/ collection propertyitems ofTypecom.example.entity.OrderItem id propertyid columnitem_id/ result propertyitemName columnitem_name/ /collection /resultMap select idselectOrdersPage resultMapOrderWithItems SELECT o.id, o.order_no, i.id AS item_id, i.item_name FROM orders o LEFT JOIN order_item i ON o.id i.order_id /select问题来了一条订单有多条商品明细关联查询得出来的行数会膨胀比如10条订单、平均每单3条明细查询结果SET就有30行。Pagehelper在改了SQL尾巴之后数据查询出来的物理行数是30行但映射到OrderWithItems后Pagehelper只能感知到物理行数它的count逻辑也容易把行数算成30而不是10。这种场景比较恶心因为Pagehelper拿到的list在Mapper执行后已经是MyBatis映射完成的对象了实际上如果你用PageInfo去拿totalcount SQL已经帮你执行完了它统计的也是膨胀后的30。想要拿到真实的10条订单数就得让count查询做一个子查询去重统计比如SELECT COUNT(*) FROM ( SELECT o.id FROM orders o LEFT JOIN order_item i ON o.id i.order_id GROUP BY o.id ) tmp这种情况下插件生成的count无法满足你的要求同样是手动处理count或者尽量把嵌套查询做成两条独立查询在Service层做内存组装。这也是我后来常用的做法能拆就拆不要让分页SQL和嵌套映射混在一起否则性能和准确性水平都很难控制。5.4 多数据源环境下Pagehelper失效怎么办多数据源项目里如果配置了多个SqlSessionFactoryPagehelper插件默认只会注册到Spring Boot自动装配的那个SqlSessionFactory上另一个数据源没有拦截器自然就不生效。解决办法有两种。第一种是每个数据源单独配置插件Bean(name primarySqlSessionFactory) public SqlSessionFactory primarySqlSessionFactory(Qualifier(primaryDataSource) DataSource dataSource) throws Exception { SqlSessionFactoryBean factoryBean new SqlSessionFactoryBean(); factoryBean.setDataSource(dataSource); // 手动设置插件 factoryBean.setPlugins(new Interceptor[]{pageInterceptor()}); return factoryBean.getObject(); }每个SqlSessionFactory都要注册同一个PageInterceptor实例。第二种更省事但也更需要注意如果多数据源里只有一个是真正需要分页的只需要在那个数据源的SqlSessionFactory里注册插件即可别重复注册到所有数据源。因为分页插件会拦截所有通过该工厂执行的查询如果你在某些只读库也不小心注册了那里如果出现一个大家都不希望被分页的方法误触发了startPage反而容易引入问题。5.5 PageHelper与MyBatis 3.5.x版本冲突排查之前提过Pagehelper的不同版本对MyBatis的版本有要求。如果你把MyBatis从3.4.x升到3.5.x之后分页突然失效或者直接报ClassNotFound大概率是依赖冲突或者插件版本太老。常见的报错有java.lang.NoSuchMethodError: org.apache.ibatis.session.Configuration.newMetaObject(Ljava/lang/Object;)Lorg/apache/ibatis/reflection/MetaObject;这种就是Pagehelper版本跟MyBatis版本不匹配。升级方案建议是MyBatis 3.5.x对应Pagehelper 5.3.x及以上Spring Boot 2.x对应pagehelper-spring-boot-starter 1.4.xSpring Boot 3.x对应2.1.x。升级后记得清一下本地Maven仓库缓存避免命中了旧的依赖坐标。另外如果项目里还有其他分页插件比如某些框架自带的分页逻辑会造成重复分页比如SQL后面被拼了两次LIMIT。这种情况多见于老项目改造过程中检查pom.xml里是否同时存在多个拦截器实现逐一定位排除。6. 实战中的性能优化与避坑心得6.1 大偏移量分页性能瓶颈与优化思路Pagehelper的默认分页在数据量小的时候毫无压力但一旦偏移量大起来比如上万页之后LIMIT 200000, 20会让MySQL扫描掉前面20万行再丢弃性能骤降。优化思路不能指望分页插件本身解决得从SQL侧入手。常见的方案是延迟关联Late Row Lookups即先走覆盖索引查出当前页的主键再用主键回表取完整数据SELECT u.* FROM sys_user u INNER JOIN ( SELECT id FROM sys_user WHERE status 1 ORDER BY create_time DESC LIMIT 200000, 20 ) tmp ON u.id tmp.id ORDER BY u.create_time DESC不过这种优化在Pagehelper模式下不太好直接套用因为分页SQL是插件拼的。如果你遇到了真正的大偏移量性能问题我的建议是干脆不要用Pagehelper了直接在SQL里自己写分页条件配合延迟关联或**基于游标Keyset Pagination**的方式也就是查询条件里带WHERE create_time ? ORDER BY create_time DESC LIMIT 20性能才能质变。Pagehelper适合的体量是百万级以内再大就要考虑换分页策略了。这不是插件不行而是LIMIT offset模式本身的物理限制谁也没办法。6.2 分页查询常见的IO开销与count优化COUNT(*)在InnoDB里不像MyISAM那样有现成的行数记录每次都要扫描统计。即便走了索引当表很大时count也是不小开销。虽然Pagehelper会尽量把count SQL简化为SELECT COUNT(*)但对于复杂查询它也无能为力。生产环境里做过分页接口压测的同学应该深有体会一个接口遇到了慢SQL十有八九问题出在count查询上。优化count的基本思路可以总结为几个方向尽量让count查询走上覆盖索引避免回表。如果业务上可以接受近似总条数对超大表可以关掉count只在UI上显示共xx条。对于运营后台这类低频分页也可以考虑在Redis里缓存一段时间的总记录数减少count对数据库的冲击。6.3 分页参数校验与前端传参规范最后聊一个偏工程实践的细节。后端接口一定要对pageNum和pageSize做参数校验同时注意设置合理的上限。比如if (pageSize 200) { pageSize 200; }200的硬顶是保护性措施防止有人传一个pageSize100000把整张表拉出去直接把数据库打满。配合reasonabletrue页码的边界问题能自动兜底但pageSize太大时要手动限制。这个看起来简单但真的很多线上事故就是漏了这一行。另外如果前端分页组件传的字段名不是pageNum和pageSize比如是current和limit就需要在Controller层做一次映射转换别把锅甩给Pagehelper。保持接口层的分页参数命名规则统一对多项目协作时很重要。7. 多场景使用实例与扩展思路7.1 批量查询中的分页处理有些内部批处理任务也需要分批拉取数据比如定时任务要扫描一张大表里待处理的数据。这种场景不涉及用户交互但同样需要分页控制内存占用。int pageNum 1; int pageSize 500; while (true) { try { PageHelper.startPage(pageNum, pageSize); ListTask tasks taskMapper.selectPendingTasks(); if (tasks.isEmpty()) { break; } processTasks(tasks); // 处理当前批次 pageNum; } finally { PageHelper.clearPage(); } }这里要注意的是当最后一页数据不足pageSize时怎么判断是否继续循环最简单的方式是通过tasks.size()是否小于pageSize来判断但更稳妥的还是要靠PageInfo里的total或者当前页是否isLastPage。如果直接判断isEmpty()万一某页正好过滤后没有数据但后面还有数据循环就会提前终止数据就丢了。7.2 在MyBatis-Plus项目中是否能复用很多用MyBatis-Plus的项目也有分页需求MyBatis-Plus自带了一个分页插件PaginationInnerInterceptor用法跟Pagehelper很像。如果你在MyBatis-Plus项目里想用Pagehelper理论上可以加依赖并注册插件但我个人不推荐两套分页插件混用因为它们都会拦截Executor执行顺序不同会导致SQL被改写多次。选择哪套分页方案建议看你的项目已有依赖原生MyBatis或者升级旧项目优先用Pagehelper新项目本身就是MyBatis-Plus生态直接用内置的分页插件更顺滑没必要额外引Pagehelper。7.3 分页结果在不同场景下的返回形式不同场景对分页结果的返回形式要求不一样面向管理后台的REST API通常返回PageInfo全量字段前端表格组件需要这些元数据。面向C端App的列表页如果是触底加载更多模式建议只返回list和hasNext两个字段减轻网络传输。如果是下拉加载实现无限滚动很多时候连total都不需要直接countfalse更合适。由于Pagehelper的PageInfo字段比较多直接暴露给前端显得冗余可以自己封装一个统一返回VOpublic class PageResultT { private ListT list; private long total; private int pages; private int pageNum; private int pageSize; private boolean hasNext; // 构造函数接收PageInfo拷贝需要的字段 }一方面避免前端误用了不该用的字段另一方面也是让接口契约更稳定以后就算底层换了分页插件对外返回结构也不变。8. 写在最后我的一些体会从刚接触MyBatis时手写LIMIT到后来为了应付各种需求变化改用Pagehelper到现在中间踩过的坑确实不少。坦白说Pagehelper并不是一个完美的分页方案它最让人省心的是侵入性极低写业务查询时不需要关心分页让代码回归干净。但它也不是银弹ThreadLocal机制需要使用者理解背后的消费顺序问题count和SQL的改写逻辑在复杂查询下可能会给你惊喜也可能会给你惊吓。我个人的实战建议一共三条第一分页代码块务必放try-finally并手动clearPage()这是成本最低、收益最稳的优化习惯。第二如果一个查询逻辑里包含嵌套集合映射、关联统计、子查询等复杂SQL同时要分页时提前评估插件的count和SQL改写是否符合预期实在不行就放弃插件自动分页自己控制分页SQL。第三参数校验一定不要偷懒尤其是pageSize上限。用了这么多年实战项目的稳定运行证明只要理解了它的核心机制避开了那几个经典陷阱Pagehelper依然是我在原生MyBatis项目里分页的首选。希望这篇文章里写到的那些经验和排查过程能帮你少走一些弯路。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询