Java分页执行流程如何规整

wen java案例 29

本文目录导读:

Java分页执行流程如何规整

  1. 规整化的核心原则
  2. 标准分页执行流程(MyBatis-Plus为例)
  3. 规整化流程的“避坑”与进阶
  4. 最终效果对比

针对Java分页执行流程的规整化,核心在于解耦(将分页逻辑与业务逻辑分离)、统一(使用标准接口与DTO)和高效(避免内存分页,使用数据库原生分页)。

以下是经过业界验证的规整化方案,主要涵盖后端(Spring Boot + MyBatis/MyBatis-Plus)的典型场景。


规整化的核心原则

  1. 统一参数与返回:所有分页接口统一接收 PageParam(当前页、每页大小),返回 PageResult(分页数据、总条数、总页数等)。
  2. SQL层分页:坚决避免 List.stream().skip().limit() 这种方式(性能极差),必须使用数据库的 LIMIT/OFFSET 或游标分页。
  3. 使用现成ORM框架:优先使用 MyBatis-Plus / JPA 的分页机制,避免手动拼装 PageHelper 或手写 RowBounds(容易出问题)。
  4. DTO与VO分离:数据库实体(DO)不应直接返回前端,需要转换为视图对象(VO)。

标准分页执行流程(MyBatis-Plus为例)

分页入参出参定义(规整的基础)

// 1. 通用分页请求参数
@Data
public class PageParam {
    @Min(1) private Integer pageNo = 1;
    @Min(1) @Max(500) private Integer pageSize = 20; 
    // 可扩展排序字段:private String orderBy; private Boolean asc;
}
// 2. 通用分页返回结果
@Data
public class PageResult<T> {
    private List<T> records;
    private Integer pageNo;
    private Integer pageSize;
    private Long total;
    private Integer pages; // 总页数
    public PageResult(List<T> records, Long total, PageParam param) {
        this.records = records;
        this.total = total;
        this.pageNo = param.getPageNo();
        this.pageSize = param.getPageSize();
        this.pages = (int) Math.ceil((double) total / param.getPageSize());
    }
}

Controller层:接收参数并封装

@RestController
public class UserController {
    @GetMapping("/users")
    // 统一使用 @Validated 校验分页参数
    public Result<PageResult<UserVO>> listUsers(@Validated PageParam pageParam, 
                                                  @RequestParam(required = false) String name) {
        // 调用Service层,Controller只负责接收和返回,不做业务处理
        PageResult<UserVO> page = userService.queryUserPage(pageParam, name);
        return Result.success(page);
    }
}
/* 
优点:Controller 代码极短,不感知 MyBatis-Plus 的 Page 对象
*/

Service层:转换与装配

@Service
public class UserServiceImpl implements UserService {
    @Resource
    private UserMapper userMapper;
    @Override
    public PageResult<UserVO> queryUserPage(PageParam pageParam, String name) {
        // 1. 构建 MyBatis-Plus 的 Page 对象(这一步需要 ORM 框架的分页插件)
        //    Page<UserDO> 是 MyBatis-Plus 提供的,内部会自动处理 limit
        Page<UserDO> mybatisPage = new Page<>(pageParam.getPageNo(), pageParam.getPageSize());
        // 2. 执行分页查询(传入 Page 对象,无需手动计算 offset)
        //    Mapper 方法返回 Page<UserDO>,其中已包含 total 和 records
        Page<UserDO> resultPage = userMapper.selectUserPage(mybatisPage, name);
        // 3. 将 DO 转换为 VO(规整的关键步骤)
        List<UserVO> voList = resultPage.getRecords().stream()
                .map(this::convertToVO) // 可使用 BeanUtils.copyProperties 或 MapStruct
                .collect(Collectors.toList());
        // 4. 返回统一的 PageResult (解耦,不暴露 MyBatis 的 Page 给 Controller)
        return new PageResult<>(voList, resultPage.getTotal(), pageParam);
    }
}

Mapper层:纯 SQL 或 MyBatis-Plus 语法

MyBatis-Plus 方法(推荐,无需写XML)

// Mapper 接口
@Mapper
public interface UserMapper extends BaseMapper<UserDO> {
    // 使用 MyBatis-Plus 的 QueryWrapper 分页
    default Page<UserDO> selectUserPage(Page<UserDO> page, String name) {
        return this.selectPage(page, 
            new LambdaQueryWrapper<UserDO>()
                .like(StringUtils.isNotBlank(name), UserDO::getName, name)
                .orderByDesc(UserDO::getCreateTime));
    }
}
// 注意:需要在配置类中启用 MyBatis-Plus 分页插件
@Configuration
public class MyBatisPlusConfig {
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
        return interceptor;
    }
}

手写 XML(复杂查询时使用)

<!-- UserMapper.xml -->
<select id="selectUserPage" resultType="com.example.entity.UserDO">
    SELECT * FROM user
    <where>
        <if test="name != null and name != ''">
            name LIKE CONCAT('%', #{name}, '%')
        </if>
    </where>
    ORDER BY create_time DESC
    <!-- 这里不需要写 limit,MyBatis-Plus 分页插件会自动在尾部追加 LIMIT 和 COUNT 查询 -->
</select>

规整化流程的“避坑”与进阶

计算总条数的性能优化(count优化)

  • 问题:MyBatis-Plus 默认会执行 SELECT COUNT(*) 全表扫描。
  • 优化方案
    • 根据业务判断:如果pageSize很小(如固定20条),且用户极少翻到第1000页,可配置 page.setOptimizeCountSql(false)
    • 如果数据量极大(如千万级),弃用传统分页,改用 游标分页(Keyset Pagination):基于上一页最后一条记录的ID查询 WHERE id > ? LIMIT 20,这种方式无需count,且性能稳定O(1)。
    • 规整建议:在 PageResult 中加入 isOptimized 标识,前端判断如果总页数过大,不显示具体总条数。

统一处理排序

  • 不要让每个Mapper都写死排序字段。
  • 规整做法:在PageParam中增加 orderByasc 字段,Service层统一构建OrderItem后设置到Page对象。

批量操作与分页冲突

  • 问题PageHelperThreadLocal 机制在异步或批处理时易导致分页错乱。
  • 规整建议:全局统一使用 MyBatis-Plus 的 Page 对象作为方法参数(显式传递),彻底弃用 PageHelper.startPage()

最终效果对比

场景 混乱的写法 规整写法
Controller方法 PageInfo<User> 直接返回给前端 返回通用的 PageResult<UserVO>
参数校验 手动判断 pageNo > 0 使用 @Validated + PageParam统一校验
分页逻辑 在Service里自己算 offset + limit 传入 MyBatis-Plus 的 Page 对象,插件自动处理
返回数据 返回的list包含DO实体,暴露密码等敏感字段 VO转换,只返回前端需要的字段
耦合度 Service里new PageInfo()依赖了PageHelper Service只依赖自己的DTO和MyBatis-Plus的Page,容易替换

规整的Java分页流程 = PageParam作为入参ORM分页插件执行DO转VOPageResult作为出参

只要团队遵循这个流程,任何后端成员都能快速看懂、修改分页代码,且不会出现慢SQL或数据泄露问题。

抱歉,评论功能暂时关闭!