从一个简单的分页需求出发,逐步演进到一个支持复杂组合排序的通用解决方案,这本身就是一个极佳的技术分享案例。
我将以您提供的 toPageableWithMultiSort 方法为核心,撰写一篇深入浅出的技术博客。
Spring Data JPA 分页排序进化论:从单字段到 toPageableWithMultiSort 的优雅之道
在构建任何数据驱动的应用时,分页和排序都是不可或缺的基础功能。Spring Data JPA 以其强大的 Pageable 和 Sort 接口,为我们提供了极大的便利。然而,当业务需求从简单的“按创建时间倒序”,演变为“先按序号降序,如果序号为空则排在最后,再按创建时间降序”时,我们最初编写的分页工具类可能就显得力不从心了。
本文将带你走过一次分页排序功能的“进化之旅”,从一个只支持单字段排序的 toPageable 方法开始,逐步重构,最终打造出一个支持任意多字段组合排序的、高度可复用的 toPageableWithMultiSort 方法。
V1.0:最初的起点 - 简单的单字段排序
在项目初期,我们的分页需求很简单。我们创建了一个 PageWithSearch 类来接收前端的分页和排序参数,并提供了一个 toPageableWithDefault 方法来构建 Pageable 对象。
PageWithSearch.java (初始版本):
public class PageWithSearch extends BasePage {
// ... page, size, direction, properties[] ...
public Pageable toPageableWithDefault(Integer page, Integer size, Sort.Direction direction, String orderBy) {
this.page = this.page == null ? page : this.page;
this.size = this.size == null ? size : this.size;
Sort.Direction dir = Sort.Direction.fromOptionalString(this.direction).orElse(direction);
// 关键:只支持单个排序字段
Sort sort = (properties == null || properties.length == 0)
? Sort.by(dir, orderBy)
: Sort.by(dir, properties); // properties 也只取了第一个
return PageRequest.of(this.page, this.size, sort);
}
}
Service 层调用:
// 只能按单个字段排序
Pageable pageable = query.toPageableWithDefault(0, 15, Sort.Direction.DESC, "createdDate");
这个版本在处理简单的单字段排序时工作得很好。但很快,我们就遇到了它的天花板。
V2.0:新的挑战 - 复杂的多字段组合排序
产品经理提出了新的排序需求:“方案列表需要优先按 ranks(序号)降序显示,以方便运营人员调整顺序。如果 ranks 相同,或者有些方案没有设置序号(ranks 为 NULL),那么这些方案需要再按照 createdDate(创建时间)降序排列,并且没有序号的要永远排在最后。”
这个需求包含了三个核心点:
- 多字段排序:
ranks+createdDate - 方向不同: 都是
DESC - NULL 值处理:
ranks为NULL的要排在最后 (NULLS LAST)
我们现有的 toPageableWithDefault 方法显然无法满足这个需求,因为它一次只能接收一个 String 类型的 orderBy 字段。
V3.0:进化!toPageableWithMultiSort 的诞生
为了解决这个问题,我们需要一个更强大的方法,它应该能够接收一组而不是单个排序规则。Spring Data JPA 的 Sort.Order 类正是为此而生。
我们对 PageWithSearch 类进行了扩展,添加了一个全新的方法:toPageableWithMultiSort。
PageWithSearch.java (进化后):
import org.springframework.data.domain.Sort;
import java.util.Arrays;
public class PageWithSearch extends BasePage {
// ... 保留原有方法 ...
/**
* 创建一个支持多字段组合排序的 Pageable 对象。
* 如果前端没有传递排序字段,则使用默认的多字段排序规则。
*
* @param defaultOrders 默认的排序规则,一个或多个 Sort.Order 对象
* @return 一个构建好的 Pageable 对象
*/
public Pageable toPageableWithMultiSort(Sort.Order... defaultOrders) {
Integer finalPage = this.page == null ? 0 : this.page;
Integer finalSize = this.size == null ? 15 : this.size;
Sort sort;
// 判断前端是否传递了自定义排序参数
if (this.properties != null && this.properties.length > 0 && !StringUtils.isEmpty(this.properties[0])) {
// 如果有,则优先使用前端的排序
Sort.Direction dir = Sort.Direction.fromOptionalString(this.direction).orElse(Sort.Direction.DESC);
sort = Sort.by(dir, this.properties);
} else {
// 如果没有,则使用我们传入的默认组合排序规则
sort = Sort.by(defaultOrders);
}
return PageRequest.of(finalPage, finalSize, sort);
}
}
这个新方法为什么强大?
- 参数的进化: 它的参数
Sort.Order... defaultOrders是一个可变参数,这意味着我们可以向它传递任意数量的Sort.Order对象,每个对象都封装了一个独立的、完整的排序规则(字段、方向、NULL处理)。 - 能力的进化:
Sort.by(defaultOrders)这行代码是 Spring Data JPA 的标准用法,它能将一个Sort.Order对象数组,完美地转换成一个包含多重排序逻辑的Sort实例。
V4.0:在 Service 层优雅地调用
有了这个强大的新工具,我们的 Service 层代码变得既清晰又富有表现力。
SolutionService.java (最终实现):
import org.springframework.data.domain.Sort;
@Transactional(readOnly = true)
public Page<SolutionCreateVO> listSolutionsByPage(Integer adminId, PageWithSearch query) {
Specification<Solution> spec = SolutionSpecification.getSpecification(adminId, query);
// 1. 像搭积木一样,定义每一个独立的排序规则
// 规则一:按 ranks 降序,并将 NULL 值排在最后
Sort.Order ranksOrder = Sort.Order.desc("ranks").nullsLast();
// 规则二:按 createdDate 降序
Sort.Order createdDateOrder = Sort.Order.desc("createdDate");
// 2. 将这些规则“喂”给我们的新方法
Pageable pageable = query.toPageableWithMultiSort(ranksOrder, createdDateOrder);
// 3. 执行查询
Page<Solution> entityPage = solutionRepository.findAll(spec, pageable);
// ... 后续的业务逻辑 ...
}
代码解读:
Sort.Order.desc("ranks").nullsLast(): 我们用链式调用的方式,轻松地创建了一个复杂的排序规则,语义一目了然。query.toPageableWithMultiSort(...): 调用变得非常直观,我们将定义好的规则作为参数传入即可。
最终,Hibernate 生成的 SQL ORDER BY 子句也如我们所愿:
ORDER BY solution.ranks DESC NULLS LAST, solution.created_date DESC
结语
从一个只能处理 String 的 toPageableWithDefault,进化到一个能够处理 Sort.Order 数组的 toPageableWithMultiSort,这不仅仅是一次功能的升级,更是一次设计思想的跃迁。
这个重构过程告诉我们:
- 拥抱领域对象: 相比于使用原始的字符串和布尔值,使用像
Sort.Order这样封装了完整业务含义的领域对象,能让我们的代码更具表现力和类型安全性。 - 分离关注点:
toPageableWithMultiSort方法很好地分离了“处理前端自定义排序”和“应用后端默认排序”这两种不同的关注点。 - 为扩展而设计: 新的方法具有极高的可扩展性。未来如果需要三字段、四字段排序,我们无需再修改
PageWithSearch类,只需在 Service 层多创建几个Sort.Order对象即可。
通过这次进化,我们的分页工具类变得更加健壮和灵活,能够从容应对未来更多变的排序需求。
✨ 总结与图表回顾 📊
📝 分页方法进化总结表
| 版本 🚀 | 方法签名 📝 | 支持的排序能力 🎯 | 优点 ✅ | 缺点 ❌ |
|---|---|---|---|---|
| V1.0 | toPageableWithDefault(..., String orderBy) | 单字段排序 | 简单直接 | 功能有限,不灵活 |
| V2.0 | toPageableWithMultiSort(Sort.Order... orders) | 任意多字段组合排序,支持NULLS处理 | 灵活、强大、可扩展、类型安全 | 代码量稍多 |
🗺️ 决策与重构流程图 (Flowchart)
🔄 Service层调用时序图 (Sequence Diagram)
🚦 Sort 对象状态图 (State Diagram)
🏗️ 关键类与方法类图 (Class Diagram)
🔗 实体关系图 (Entity Relationship Diagram)
🧠 思维导图 (Markdown Format)

155

被折叠的 条评论
为什么被折叠?



