这个Spring Data JPA模式清除了三年的仓储代码债务
如果您从事企业级 Java 应用开发超过一年,很可能体验过这种特有的痛苦:产品经理要求添加一个新的搜索筛选条件,您打开仓库文件,发现里面已经有 18 个方法。您写了第 19 个、第 20 个,然后在写到第 25 个方法时开始怀疑是否有更好的方式。
确实有。它叫做 Spring Data JPA Specifications,而且它一直安静地存在于框架中。
硬编码查询方法的问题
Spring Data JPA 的派生查询方法对于简单的查找非常适用。findByEmail 清晰、可读,且无需任何 SQL。但企业搜索很少保持简单。您的 CRM 用户希望按名称 和 状态筛选客户。然后是按日期范围。再然后是城市。接着是一个可能匹配名称 或 邮箱的关键词。很快,您就需要维护一个像下面这样的仓库:
每增加一个新需求就意味着一个新方法。仓库变成了一个垃圾场。测试它变成了苦差事。引导新人上手变成了一场关于该使用 30 个方法中哪一个的对话。
Specifications 通过让您定义小型、可组合的查询谓词,并在运行时根据用户实际提供的筛选条件组合它们,从而解决了这个问题。
Specification 到底是什么
在底层,Specification 包装了 JPA Criteria API,这是一种通过编程方式、类型安全地构建查询的方法,无需编写原始的 SQL 或 JPQL。Criteria API 功能强大但冗长且难以阅读。Specifications 在更清晰的表面区域下提供了这种能力。
每个 Specification 只是一个生成谓词的 lambda 表达式:(root, query, criteriaBuilder) -> criteriaBuilder.equal(root.get("status"), "ACTIVE")
仅此而已。一个条件,一个方法,可以与任何其他条件组合。
构建它:一个客户搜索示例
让我们具体化。假设有一个 Customer 实体,包含 name、email、status 和 createdDate 字段。用户可以按这些字段的任意组合(或完全不组合)进行筛选。
实体
一个 Specifications 工具类
我倾向于将谓词放在一个专门的类中,而不是散落在服务中:
null 返回是有意为之的;Spring Data JPA 会忽略 null 谓词,这意味着您可以免费获得“如果未提供则跳过此筛选条件”的行为。
仓库
您的仓库需要扩展 JpaSpecificationExecutor:
在服务中组合起来
这一个 findAll 调用会根据调用者提供的筛选条件组合动态适配。没有分支逻辑,没有 20 个仓库方法。当产品经理在下个冲刺要求添加第五个筛选条件时,您只需在 CustomerSpecifications 中添加一个方法,并在服务中添加一行 .and()。搞定。
更进一步:OR 条件、连接和分页
OR 条件
.or() 组合器的工作方式与预期完全一致。一个同时检查名称或邮箱的全局搜索栏:
跨连接筛选
如果您的 Customer 有一个嵌套的 Address,您可以在不修改服务层的情况下触及它:
连接发生在了 Specification 内部。您的服务代码依然整洁。
分页
由于 JpaSpecificationExecutor 暴露了一个 findAll(Specification, Pageable) 的重载,添加分页只需一行代码:
我在实践中见过的错误
- 为 null 筛选条件返回非 null 谓词:这是最常见的陷阱。如果您忘记检查 null 并仍然返回了一个有效谓词,您会静默地过滤掉本应返回的数据。始终在 lambda 开头进行保护。
- 将业务逻辑混入 Specifications:一个 Specification 应该只做一件事:生成一个谓词。我见过记录日志、调用服务、检查权限的 Specifications。不要这样做。保持它们的纯粹性。
- 创建一个处理所有筛选条件的“God Specification”:这只不过是把臃肿的仓库问题变成了臃肿的 Specification 问题。小型、单一职责的 Specifications 保持可测试和可复用。一个
statusEquals的 Specification 可以服务于您的搜索界面、报表模块和管理仪表盘,而它们彼此之间无需了解。 - 字符串搜索跳过大小写规范:
cb.like(root.get("name"), "%dzone%")不会匹配 "DZone" 或 "DZONE"。始终进行规范化:使用cb.lower(root.get("name"))配合输入内容的小写化。
为什么这种方案会随着时间的推移带来回报
Specifications 真正的红利出现在引入它们六个月之后,当需求发生变化时——而需求总是会变化。添加一个筛选条件?一个新的静态方法,一行 .and()。删除一个筛选条件?删除方法和组合行。在两个功能间复用某个筛选条件?导入同一个 Specification 类。对筛选条件进行单元测试?实例化 Specification,传入一个 mock 的 CriteriaBuilder,断言谓词。无需 Spring 上下文。
在复杂的企业级代码库中——那种拥有多个开发团队、不断演化的产品需求和长期维护周期的代码库——这种模块化的价值远比初听起来要大得多。
最后的想法
Specifications 并不新奇。它们是 Spring Data JPA 标准库的一部分;它们与您已有的所有东西都能协同工作,并且它们解决了每个拥有搜索界面的团队最终都会遇到的问题。
如果您的仓库开始看起来像一个按字母顺序排列的、包含用户曾经请求过的所有筛选条件组合的索引,那么现在就是切换的好时机。
DZone 贡献者表达的观点仅代表其个人观点。