当 Spring Data JPA 仓库需要一个可选、可组合并在运行时决定的 WHERE 子句时,通常使用的 API 是 org.springframework.data.jpa.repository.JpaSpecificationExecutor<T>。
它连接了以下组件:
- Spring Data 仓库方法,例如
findAll(...)、count(...)和exists(...) - Spring 的
org.springframework.data.jpa.domain.Specification<T> - 以
jakarta.persistence.criteria.CriteriaBuilder为核心的 Jakarta Persistence Criteria API
JpaSpecificationExecutor<T>
#
假设存在一个实体:
@Entity
@Table(name = "orders")
public class PurchaseOrder {
@Id
private Long id;
@Column(nullable = false)
private String status;
@Column(nullable = false)
private String customerEmail;
@Column(nullable = false)
private Instant createdAt;
@ManyToOne(fetch = FetchType.LAZY)
private Customer customer;
}以及一个仓库:
public interface PurchaseOrderRepository
extends JpaRepository<PurchaseOrder, Long>,
JpaSpecificationExecutor<PurchaseOrder> {
}JpaRepository 提供 CRUD 和分页 API。JpaSpecificationExecutor 添加了一组方法,其过滤逻辑由 Specification<PurchaseOrder> 描述。
典型方法如下:
List<PurchaseOrder> findAll(Specification<PurchaseOrder> spec);
Page<PurchaseOrder> findAll(Specification<PurchaseOrder> spec, Pageable pageable);
List<PurchaseOrder> findAll(Specification<PurchaseOrder> spec, Sort sort);
Optional<PurchaseOrder> findOne(Specification<PurchaseOrder> spec);
long count(Specification<PurchaseOrder> spec);
boolean exists(Specification<PurchaseOrder> spec);核心思想很简单:不需要手动拼接 JPQL 字符串。将 Specification<T> 交给 Spring Data 后,Spring Data 会为你构建 Criteria 查询。
Specification<T> 的本质
#
org.springframework.data.jpa.domain.Specification<T> 是一个函数式接口:
public interface Specification<T> extends Serializable {
Predicate toPredicate(
Root<T> root,
CriteriaQuery<?> query,
CriteriaBuilder criteriaBuilder
);
}也就是说,Specification<T> 只是一个可复用的函数,它知道如何为实体类型 T 贡献一个 Predicate。
三个参数都很重要:
Root<T>表示被查询的根表或根实体CriteriaQuery<?>表示正在构建的完整查询CriteriaBuilder是用于创建表达式和谓词的工厂,例如equal、like、between、and、or、not、lower、count等
实际上,大多数应用代码只关心 root 和 criteriaBuilder。当需要 distinct、groupBy、自定义排序或 fetch join 时,query 才会变得重要。
实际获取 Specification 的方式
#
最常见的方式是从小型静态工厂方法中返回它:
public final class PurchaseOrderSpecs {
private PurchaseOrderSpecs() {}
public static Specification<PurchaseOrder> hasStatus(String status) {
return (root, query, cb) -> cb.equal(root.get("status"), status);
}
public static Specification<PurchaseOrder> customerEmailContains(String token) {
return (root, query, cb) ->
cb.like(
cb.lower(root.get("customerEmail")),
"%" + token.toLowerCase(Locale.ROOT) + "%"
);
}
public static Specification<PurchaseOrder> createdAtGte(Instant lowerBound) {
return (root, query, cb) -> cb.greaterThanOrEqualTo(root.get("createdAt"), lowerBound);
}
public static Specification<PurchaseOrder> customerIdEquals(Long customerId) {
return (root, query, cb) -> cb.equal(root.get("customer").get("id"), customerId);
}
}然后将它们组合起来:
Specification<PurchaseOrder> spec = Specification.where(null);
if (status != null) {
spec = spec.and(PurchaseOrderSpecs.hasStatus(status));
}
if (emailToken != null && !emailToken.isBlank()) {
spec = spec.and(PurchaseOrderSpecs.customerEmailContains(emailToken));
}
if (createdAfter != null) {
spec = spec.and(PurchaseOrderSpecs.createdAtGte(createdAfter));
}
if (customerId != null) {
spec = spec.and(PurchaseOrderSpecs.customerIdEquals(customerId));
}
Page<PurchaseOrder> page =
purchaseOrderRepository.findAll(spec, PageRequest.of(0, 20, Sort.by("createdAt").descending()));在大多数代码库中,这个 API 的实际形式是:将请求参数转换为一组可选的 Specification,然后将它们串联起来。
另一种常见风格是保留请求对象,并由一个方法构建完整的 specification:
public record PurchaseOrderFilter(
String status,
String emailToken,
Instant createdAfter,
Long customerId
) {}
public static Specification<PurchaseOrder> fromFilter(PurchaseOrderFilter filter) {
Specification<PurchaseOrder> spec = Specification.where(null);
if (filter.status() != null) {
spec = spec.and(hasStatus(filter.status()));
}
if (filter.emailToken() != null && !filter.emailToken().isBlank()) {
spec = spec.and(customerEmailContains(filter.emailToken()));
}
if (filter.createdAfter() != null) {
spec = spec.and(createdAtGte(filter.createdAfter()));
}
if (filter.customerId() != null) {
spec = spec.and(customerIdEquals(filter.customerId()));
}
return spec;
}CriteriaBuilder 的工作方式
#
jakarta.persistence.criteria.CriteriaBuilder 是一个工厂 API。它本身不会执行 SQL,而是创建一个表达式对象树,之后由 JPA 提供程序(例如 Hibernate)将其转换为 SQL。
其方法通常可分为以下类别:
- 比较谓词:
equal、notEqual、greaterThan、lessThan、between - 字符串操作:
like、lower、upper、trim、concat - null/boolean 检查:
isNull、isNotNull、isTrue、isFalse - 布尔组合:
and、or、not、conjunction、disjunction - 聚合或查询表达式:
count、avg、sum、exists
示例:
cb.equal(root.get("status"), "PAID");
cb.like(cb.lower(root.get("customerEmail")), "%gmail.com");
cb.between(root.get("createdAt"), start, end);
cb.and(
cb.equal(root.get("status"), "PAID"),
cb.isNotNull(root.get("customerEmail"))
);因此,Criteria API 本质上是一个用于构建类似 SQL 查询的类型化 AST 构建器。
相关的重要 Criteria API 类型 #
要理解 Specification,以下接口值得了解:
Root<T>
#
Root<T> 是 FROM 子句中的根实体。如果仓库查询的是 PurchaseOrder,那么 root 就对应于该表或实体。
示例:
root.get("status");
root.get("createdAt");
root.get("customer").get("id");Path<X>
#
每个 root.get(...) 都会返回一个 Path<?>,表示对属性的导航。这正是属性访问可以链式调用的原因。
Path<String> email = root.get("customerEmail");
Path<Long> customerId = root.get("customer").get("id");Predicate
#
Predicate 是 toPredicate(...) 返回的布尔表达式,并会成为生成的 WHERE 子句的一部分。
CriteriaQuery<?>
#
CriteriaQuery<?> 是完整的查询定义。必要时,specification 可以修改它:
return (root, query, cb) -> {
query.distinct(true);
return cb.equal(root.get("status"), "PAID");
};对此需要谨慎:一旦 specification 开始改变查询形状,它就不再只是一个小型过滤片段。它虽然功能强大,但也会使组合行为变得不那么可预测。
Join<Z, X>
#
按关联实体过滤时,显式 join 往往比链式调用 get(...) 更清晰:
public static Specification<PurchaseOrder> customerRegionEquals(String region) {
return (root, query, cb) -> {
Join<PurchaseOrder, Customer> customer = root.join("customer");
return cb.equal(customer.get("region"), region);
};
}Order
#
排序通常应该交给 Spring Data 的 Sort 或 Pageable 处理,而不是硬编码在 specification 中。但是,如果 specification 必须通过 query.orderBy(...) 影响排序,可以使用 CriteriaBuilder.asc(...) / desc(...)。
Specification 为什么存在
#
没有 Specification 时,通常会遇到以下两个问题之一:
- 出现大量类似
findByStatusAndCustomerEmailContainingAndCreatedAtAfter(...)的仓库方法 - 使用包含大量可为 null 条件的手写 JPQL 字符串
Specification 通过将每个过滤器表示为可以使用 .and(...) 和 .or(...) 组合的对象或函数,解决了“动态 WHERE 子句”问题。
它尤其适用于以下场景:
- 包含许多可选字段的搜索界面
- 支持分页过滤的管理 API
- 可复用的业务过滤器,例如“对租户可见”、“未被软删除”或“由当前用户创建”
类型安全的访问与字符串属性名 #
示例通常使用:
root.get("status")这种方式很方便,但属于字符串类型化。拼写错误只有在运行时才会失败。
如果构建过程生成了 JPA 静态元模型,可以使用更安全的形式:
root.get(PurchaseOrder_.status)使用 Specification 并不要求采用这种方式,但如果希望 Criteria 代码能够安全地配合重构,它就是一个重要的配套 API。
Spring Data 在幕后为你做了什么 #
在普通应用代码中,不需要自己创建 CriteriaBuilder。Spring Data 的 JPA 仓库实现会获取 EntityManager,向其请求 CriteriaBuilder,创建 CriteriaQuery,调用你的 Specification#toPredicate(...),然后通过 JPA 提供程序执行最终查询。
因此,控制流程大致如下:
- 应用程序创建或组合
Specification<T> - 应用程序调用
repository.findAll(spec, pageable) - Spring Data 获取
EntityManager基础设施 - Spring Data 构建 Criteria 查询
Specification贡献谓词树- Hibernate 将 Criteria 树转换为 SQL
- 数据库执行 SQL
这解释了为什么 Specification 看起来很小:它只是过滤片段,而不是完整的仓库实现。
一个实用的判断标准 #
当查询仍然属于“一个实体根加一个动态谓词树”的范围时,使用 JpaSpecificationExecutor。一旦查询变成包含投影、聚合、复杂分组、供应商特定 SQL 或复杂 fetch 计划的报表,Specification API 就会变得不太适用,此时自定义仓库实现或其他查询方式通常更清晰。
不过,对于常规的搜索和过滤用例,这套结构很简洁:
JpaSpecificationExecutor<T>是仓库入口Specification<T>是可复用的过滤器抽象CriteriaBuilder是创建谓词/表达式树的工厂Root<T>、Path<?>、Join<?, ?>、Predicate和CriteriaQuery<?>是主要的辅助类型
理解这个心智模型后,这个 API 就不再神秘。
评论
在 GitHub 上阅读或评论(发表评论需要登录)