若依框架分页失效深度解析从原理到自定义分页实战最近在基于若依框架开发管理系统时遇到了一个典型的分页问题对查询结果进行二次封装后分页功能突然失效。这个问题看似简单却涉及框架底层机制、MyBatis拦截器原理和分页算法等多个技术点。本文将完整呈现我的排查思路和解决方案希望能帮助遇到类似问题的开发者少走弯路。1. 问题现象与初步分析那天我正在开发一个机构管理模块按照若依框架的标准写法我在Controller中使用了startPage()和getDataTable()这对黄金组合GetMapping(/list) public TableDataInfo list(TbInstitution tbInstitution) { startPage(); ListTbInstitution list tbInstitutionService.selectTbInstitutionList(tbInstitution); // ...业务处理逻辑 return getDataTable(institutionDTOS); }表面上看代码没有任何问题但前端返回的数据却出现了异常分页控件显示总记录数只有10条实际数据库有90多条无论点击第几页显示的都是相同的数据前端分页控件无法正常工作关键现象当直接返回list时分页正常但经过DTO转换后的institutionDTOS分页就失效了。这提示我们问题出在数据二次处理环节。2. 深入探究分页失效根源2.1 若依分页机制解析通过阅读若依源码我发现其分页实现主要依赖两个核心组件PageHelperMyBatis分页插件通过拦截Executor的query方法实现基于ThreadLocal保存分页参数自动修改SQL语句添加LIMIT子句startPage()工作原理public static E PageE startPage() { return startPage(DEFAULT_PAGE_NUM, DEFAULT_PAGE_SIZE); }这个方法会在当前线程设置分页参数影响紧接着的第一个查询语句。2.2 问题本质剖析我们的代码执行流程如下startPage()设置分页参数selectTbInstitutionList()执行时被PageHelper拦截SQL被修改为带有LIMIT的分页查询获取到的list已经是分页后的结果集对list进行遍历转换生成新的institutionDTOS集合getDataTable()尝试从ThreadLocal获取分页信息但原始总数已丢失根本原因分页信息特别是totalCount绑定在原始查询上而我们对结果集进行了二次处理导致新的集合不再携带分页元数据框架无法获取正确的记录总数分页上下文信息在业务处理过程中丢失3. 解决方案设计与实现3.1 方案选型对比方案优点缺点适用场景保持原始查询结构简单直接无法处理复杂DTO转换简单业务场景使用MyBatis结果拦截器非侵入式实现复杂维护成本高需要全局处理的场景手动分页计算灵活可控需要额外编码复杂业务转换场景内存分页实现简单性能差数据量大时不可行小数据量临时方案基于我们的业务需求需要复杂DTO转换手动分页计算是最合适的解决方案。3.2 自定义分页实现步骤3.2.1 前端改造首先调整前端调用方式显式传递分页参数getList() { this.loading true; listInstitution(this.queryParams.pageNum, this.queryParams.pageSize) .then(response { this.institutionList response.data; this.total response.total; this.loading false; }); }3.2.2 后端分页逻辑自定义Page类Data public class PageT { private ListT data; private long total; private int pageNum; private int pageSize; private int pages; public Page(ListT data, long total, int pageNum, int pageSize) { this.data data; this.total total; this.pageNum pageNum; this.pageSize pageSize; this.pages (int) Math.ceil((double) total / pageSize); } }Service层实现Override public PageInstitutionDTO selectAll(Integer pageNum, Integer pageSize) { // 获取总记录数 int total tbInstitutionMapper.selectDtoCount(); // 计算分页参数 int offset (pageNum - 1) * pageSize; // 获取分页数据 ListTbInstitution list tbInstitutionMapper.selectDto(offset, pageSize); // DTO转换 ListInstitutionDTO dtos list.stream().map(institution - { InstitutionDTO dto BeanUtil.copyProperties(institution, InstitutionDTO.class); User user userService.selectUserByUserId(institution.getUserId()); dto.setUserName(user.getUserName()); dto.setEmail(user.getEmail()); return dto; }).collect(Collectors.toList()); return new Page(dtos, total, pageNum, pageSize); }Mapper层SQLselect idselectDto resultTypecom.ruoyi.system.domain.TbInstitution SELECT * FROM tb_institution ORDER BY create_time DESC LIMIT #{offset}, #{pageSize} /select select idselectDtoCount resultTypeint SELECT COUNT(0) FROM tb_institution /select4. 方案优化与注意事项4.1 性能优化建议分页查询优化避免使用SELECT *只查询必要字段确保排序字段有索引大数据量考虑使用延迟关联优化缓存策略Cacheable(value institutionPage, key #pageNum - #pageSize) public PageInstitutionDTO selectAll(Integer pageNum, Integer pageSize) { // ... }批量查询优化// 批量查询用户信息避免N1问题 ListLong userIds list.stream() .map(TbInstitution::getUserId) .collect(Collectors.toList()); MapLong, User userMap userService.selectUsersByIds(userIds) .stream() .collect(Collectors.toMap(User::getUserId, Function.identity()));4.2 常见问题排查分页参数计算错误检查offset计算公式(pageNum - 1) * pageSize确保pageNum从1开始总数不一致确认COUNT查询与数据查询条件一致检查是否有过滤条件被遗漏性能问题使用EXPLAIN分析SQL执行计划检查慢查询日志重要提示在实现自定义分页时务必保持前端分页参数与后端计算逻辑的一致性特别是当实现服务端排序或过滤时需要将相关参数传递到COUNT查询中。5. 扩展思考分页设计的演进在实际项目中我们可能会遇到更复杂的分页需求多表联合分页使用JOIN查询时注意性能影响考虑使用子查询先分页再关联滚动分页(游标分页)SELECT * FROM table WHERE id #{lastId} ORDER BY id ASC LIMIT #{pageSize}分布式环境分页考虑使用Elasticsearch等专业搜索工具实现一致性哈希分片查询在若依框架中虽然startPage()提供了便捷的分页方式但理解其底层原理能帮助我们在遇到特殊需求时灵活应对。自定义分页方案虽然需要更多编码但带来了更大的灵活性和可控性。
若依框架踩坑记:二次封装List后分页失效,我是如何一步步排查并手写分页解决的
若依框架分页失效深度解析从原理到自定义分页实战最近在基于若依框架开发管理系统时遇到了一个典型的分页问题对查询结果进行二次封装后分页功能突然失效。这个问题看似简单却涉及框架底层机制、MyBatis拦截器原理和分页算法等多个技术点。本文将完整呈现我的排查思路和解决方案希望能帮助遇到类似问题的开发者少走弯路。1. 问题现象与初步分析那天我正在开发一个机构管理模块按照若依框架的标准写法我在Controller中使用了startPage()和getDataTable()这对黄金组合GetMapping(/list) public TableDataInfo list(TbInstitution tbInstitution) { startPage(); ListTbInstitution list tbInstitutionService.selectTbInstitutionList(tbInstitution); // ...业务处理逻辑 return getDataTable(institutionDTOS); }表面上看代码没有任何问题但前端返回的数据却出现了异常分页控件显示总记录数只有10条实际数据库有90多条无论点击第几页显示的都是相同的数据前端分页控件无法正常工作关键现象当直接返回list时分页正常但经过DTO转换后的institutionDTOS分页就失效了。这提示我们问题出在数据二次处理环节。2. 深入探究分页失效根源2.1 若依分页机制解析通过阅读若依源码我发现其分页实现主要依赖两个核心组件PageHelperMyBatis分页插件通过拦截Executor的query方法实现基于ThreadLocal保存分页参数自动修改SQL语句添加LIMIT子句startPage()工作原理public static E PageE startPage() { return startPage(DEFAULT_PAGE_NUM, DEFAULT_PAGE_SIZE); }这个方法会在当前线程设置分页参数影响紧接着的第一个查询语句。2.2 问题本质剖析我们的代码执行流程如下startPage()设置分页参数selectTbInstitutionList()执行时被PageHelper拦截SQL被修改为带有LIMIT的分页查询获取到的list已经是分页后的结果集对list进行遍历转换生成新的institutionDTOS集合getDataTable()尝试从ThreadLocal获取分页信息但原始总数已丢失根本原因分页信息特别是totalCount绑定在原始查询上而我们对结果集进行了二次处理导致新的集合不再携带分页元数据框架无法获取正确的记录总数分页上下文信息在业务处理过程中丢失3. 解决方案设计与实现3.1 方案选型对比方案优点缺点适用场景保持原始查询结构简单直接无法处理复杂DTO转换简单业务场景使用MyBatis结果拦截器非侵入式实现复杂维护成本高需要全局处理的场景手动分页计算灵活可控需要额外编码复杂业务转换场景内存分页实现简单性能差数据量大时不可行小数据量临时方案基于我们的业务需求需要复杂DTO转换手动分页计算是最合适的解决方案。3.2 自定义分页实现步骤3.2.1 前端改造首先调整前端调用方式显式传递分页参数getList() { this.loading true; listInstitution(this.queryParams.pageNum, this.queryParams.pageSize) .then(response { this.institutionList response.data; this.total response.total; this.loading false; }); }3.2.2 后端分页逻辑自定义Page类Data public class PageT { private ListT data; private long total; private int pageNum; private int pageSize; private int pages; public Page(ListT data, long total, int pageNum, int pageSize) { this.data data; this.total total; this.pageNum pageNum; this.pageSize pageSize; this.pages (int) Math.ceil((double) total / pageSize); } }Service层实现Override public PageInstitutionDTO selectAll(Integer pageNum, Integer pageSize) { // 获取总记录数 int total tbInstitutionMapper.selectDtoCount(); // 计算分页参数 int offset (pageNum - 1) * pageSize; // 获取分页数据 ListTbInstitution list tbInstitutionMapper.selectDto(offset, pageSize); // DTO转换 ListInstitutionDTO dtos list.stream().map(institution - { InstitutionDTO dto BeanUtil.copyProperties(institution, InstitutionDTO.class); User user userService.selectUserByUserId(institution.getUserId()); dto.setUserName(user.getUserName()); dto.setEmail(user.getEmail()); return dto; }).collect(Collectors.toList()); return new Page(dtos, total, pageNum, pageSize); }Mapper层SQLselect idselectDto resultTypecom.ruoyi.system.domain.TbInstitution SELECT * FROM tb_institution ORDER BY create_time DESC LIMIT #{offset}, #{pageSize} /select select idselectDtoCount resultTypeint SELECT COUNT(0) FROM tb_institution /select4. 方案优化与注意事项4.1 性能优化建议分页查询优化避免使用SELECT *只查询必要字段确保排序字段有索引大数据量考虑使用延迟关联优化缓存策略Cacheable(value institutionPage, key #pageNum - #pageSize) public PageInstitutionDTO selectAll(Integer pageNum, Integer pageSize) { // ... }批量查询优化// 批量查询用户信息避免N1问题 ListLong userIds list.stream() .map(TbInstitution::getUserId) .collect(Collectors.toList()); MapLong, User userMap userService.selectUsersByIds(userIds) .stream() .collect(Collectors.toMap(User::getUserId, Function.identity()));4.2 常见问题排查分页参数计算错误检查offset计算公式(pageNum - 1) * pageSize确保pageNum从1开始总数不一致确认COUNT查询与数据查询条件一致检查是否有过滤条件被遗漏性能问题使用EXPLAIN分析SQL执行计划检查慢查询日志重要提示在实现自定义分页时务必保持前端分页参数与后端计算逻辑的一致性特别是当实现服务端排序或过滤时需要将相关参数传递到COUNT查询中。5. 扩展思考分页设计的演进在实际项目中我们可能会遇到更复杂的分页需求多表联合分页使用JOIN查询时注意性能影响考虑使用子查询先分页再关联滚动分页(游标分页)SELECT * FROM table WHERE id #{lastId} ORDER BY id ASC LIMIT #{pageSize}分布式环境分页考虑使用Elasticsearch等专业搜索工具实现一致性哈希分片查询在若依框架中虽然startPage()提供了便捷的分页方式但理解其底层原理能帮助我们在遇到特殊需求时灵活应对。自定义分页方案虽然需要更多编码但带来了更大的灵活性和可控性。