Spring Data JPA 分页排序进化论:从单字段到 toPageableWithMultiSort 的优雅之道

从一个简单的分页需求出发,逐步演进到一个支持复杂组合排序的通用解决方案,这本身就是一个极佳的技术分享案例。

我将以您提供的 toPageableWithMultiSort 方法为核心,撰写一篇深入浅出的技术博客。


Spring Data JPA 分页排序进化论:从单字段到 toPageableWithMultiSort 的优雅之道

在构建任何数据驱动的应用时,分页和排序都是不可或缺的基础功能。Spring Data JPA 以其强大的 PageableSort 接口,为我们提供了极大的便利。然而,当业务需求从简单的“按创建时间倒序”,演变为“先按序号降序,如果序号为空则排在最后,再按创建时间降序”时,我们最初编写的分页工具类可能就显得力不从心了。

本文将带你走过一次分页排序功能的“进化之旅”,从一个只支持单字段排序的 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 相同,或者有些方案没有设置序号(ranksNULL),那么这些方案需要再按照 createdDate(创建时间)降序排列,并且没有序号的要永远排在最后。”

这个需求包含了三个核心点:

  1. 多字段排序: ranks + createdDate
  2. 方向不同: 都是 DESC
  3. NULL 值处理: ranksNULL 的要排在最后 (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

结语

从一个只能处理 StringtoPageableWithDefault,进化到一个能够处理 Sort.Order 数组的 toPageableWithMultiSort,这不仅仅是一次功能的升级,更是一次设计思想的跃迁

这个重构过程告诉我们:

  1. 拥抱领域对象: 相比于使用原始的字符串和布尔值,使用像 Sort.Order 这样封装了完整业务含义的领域对象,能让我们的代码更具表现力和类型安全性。
  2. 分离关注点: toPageableWithMultiSort 方法很好地分离了“处理前端自定义排序”和“应用后端默认排序”这两种不同的关注点。
  3. 为扩展而设计: 新的方法具有极高的可扩展性。未来如果需要三字段、四字段排序,我们无需再修改 PageWithSearch 类,只需在 Service 层多创建几个 Sort.Order 对象即可。

通过这次进化,我们的分页工具类变得更加健壮和灵活,能够从容应对未来更多变的排序需求。


✨ 总结与图表回顾 📊

📝 分页方法进化总结表
版本 🚀方法签名 📝支持的排序能力 🎯优点 ✅缺点 ❌
V1.0toPageableWithDefault(..., String orderBy)单字段排序简单直接功能有限,不灵活
V2.0toPageableWithMultiSort(Sort.Order... orders)任意多字段组合排序,支持NULLS处理灵活、强大、可扩展、类型安全代码量稍多
🗺️ 决策与重构流程图 (Flowchart)
否,只支持单字段String
新需求:
按 ranks(NULLS LAST) + createdDate 排序
现有的 toPageableWithDefault
能实现吗?
重构 PageWithSearch 类
新增 toPageableWithMultiSort 方法
参数类型改为 Sort.Order... (可变参数)
内部使用 Sort.by(orders) 构建组合排序
在 Service 层定义 Sort.Order 对象
调用新方法生成 Pageable
🎉 成功实现复杂组合排序
🔄 Service层调用时序图 (Sequence Diagram)
ControllerService"query 对象""Sort.Order"Repository1. listSolutionsByPage(query)构建排序规则2. new Sort.Order.desc("ranks").nullsLast()3. new Sort.Order.desc("createdDate")4. 调用 query.toPageableWithMultiSort(order1, order2)5. 返回构建好的 Pageable 对象6. findAll(spec, pageable)7. 返回 Page<Solution>ControllerService"query 对象""Sort.Order"Repository
🚦 Sort 对象状态图 (State Diagram)
"Sort.by(dir, property)"
"通过 Sort.and(anotherSort) 组合"
"Sort.by(order1, order2, ...)"
SingleField
多字段组合排序
通过Sort.Order数组创建
🏗️ 关键类与方法类图 (Class Diagram)
"使用"
"使用"
PageWithSearch
-String[] properties
+toPageableWithDefault(String orderBy) : Pageable
+toPageableWithMultiSort(Sort.Order... orders) : Pageable
«static»
Sort
+by(Sort.Order... orders) : Sort
Sort_Order
-String property
-Sort.Direction direction
-Sort.NullHandling nullHandling
+desc(String property)
+nullsLast()
🔗 实体关系图 (Entity Relationship Diagram)
PAGEABLEstringname分页请求intpage页码intsize页大小SORTstringname排序信息SORT_ORDERstringpropertyPK排序字段stringdirection排序方向stringnullHandlingNULL值处理包含由...组成
🧠 思维导图 (Markdown Format)

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值