规范

JPA 的 Criteria API 允许你以编程方式构建查询。 Spring Data JPA 的 Specification 提供了一个小巧而专注的 API,用于表达针对实体的谓词,并可在多个仓库之间复用。 该设计基于 Eric Evans 在《领域驱动设计》一书中提出的“规约”(specification)概念,遵循相同的语义,提供了一个使用 JPA 定义查询条件的 API。 为了支持规约(Specifications),你可以让你的仓库接口继承 JpaSpecificationExecutor 接口,如下所示:spring-doc.cadn.net.cn

public interface CustomerRepository extends CrudRepository<Customer, Long>, JpaSpecificationExecutor<Customer> {
}

规范(Specification)是使用 Criteria API 表达的对实体的一种谓词。 Spring Data JPA 提供了两个入口点:spring-doc.cadn.net.cn

谓词规范

PredicateSpecification 接口以最少的依赖项进行定义,从而支持广泛的功能组合:spring-doc.cadn.net.cn

public interface PredicateSpecification<T> {
  Predicate toPredicate(From<?, T> from, CriteriaBuilder builder);
}

规格(Specifications)可以轻松用于构建一组可扩展的谓词,并与 JpaRepository 一起使用,从而无需为每种所需的组合都声明一个查询(方法),如下例所示:spring-doc.cadn.net.cn

示例 1. 客户的规格说明
class CustomerSpecs {

  static PredicateSpecification<Customer> isLongTermCustomer() {
    return (from, builder) -> {
      LocalDate date = LocalDate.now().minusYears(2);
      return builder.lessThan(from.get(Customer_.createdAt), date);
    };
  }

  static PredicateSpecification<Customer> hasSalesOfMoreThan(MonetaryAmount value) {
    return (from, builder) -> {
      // build predicate for sales > value
    };
  }
}

Customer_ 类型是使用 JPA 元模型生成器生成的元模型类型(参见Hibernate 实现文档中的示例)。 因此,表达式 Customer_.createdAt 假定 Customer 具有一个类型为 createdAtDate 属性。 除此之外,我们还在业务需求的抽象层面上表达了一些条件,并创建了可执行的 Specificationsspring-doc.cadn.net.cn

直接在仓库中使用一个规格(Specification):spring-doc.cadn.net.cn

List<Customer> customers = customerRepository.findAll(isLongTermCustomer());

规格在组合使用时最为有价值:spring-doc.cadn.net.cn

MonetaryAmount amount = new MonetaryAmount(200.0, Currencies.DOLLAR);
List<Customer> customers = customerRepository.findAll(
  isLongTermCustomer().or(hasSalesOfMoreThan(amount))
);

Specification, UpdateSpecification, DeleteSpecification

Specification 接口已存在更长时间,并且根据 Criteria API 的限制,它与特定查询类型(select、update、delete)相关联。 三个规范接口定义如下:spring-doc.cadn.net.cn

public interface Specification<T> {
  Predicate toPredicate(Root<T> root, CriteriaQuery<?> query,
            CriteriaBuilder builder);
}
public interface UpdateSpecification<T> {
  Predicate toPredicate(Root<T> root, CriteriaUpdate<T> update,
            CriteriaBuilder builder);
}
public interface DeleteSpecification<T> {
  Predicate toPredicate(Root<T> root, CriteriaDelete<T> delete,
            CriteriaBuilder builder);
}

Specification 对象可以直接构造,也可以通过复用 PredicateSpecification 实例来构造,如下例所示:spring-doc.cadn.net.cn

示例2:客户规格说明
public class CustomerSpecs {

  public static UpdateSpecification<Customer> updateLastnameByFirstnameAndLastname(String newLastName, String currentFirstname, String currentLastname) {
    return UpdateSpecification<User> updateLastname = UpdateSpecification.<User> update((root, update, criteriaBuilder) -> {
      update.set("lastname", newLastName);
    }).where(hasFirstname(currentFirstname).and(hasLastname(currentLastname)));
  }

  public static PredicateSpecification<Customer> hasFirstname(String firstname) {
    return (root, builder) -> {
      return builder.equal(from.get("firstname"), value);
    };
  }

  public static PredicateSpecification<Customer> hasLastname(String lastname) {
    return (root, builder) -> {
      // build query here
    };
  }
}

流畅 API

JpaSpecificationExecutor 定义了流畅的查询方法,用于基于 Specification 实例灵活地执行查询:spring-doc.cadn.net.cn

  1. 对于 PredicateSpecificationfindBy(PredicateSpecification<T> spec, Function<? super SpecificationFluentQuery<S>, R> queryFunction)spring-doc.cadn.net.cn

  2. 对于 SpecificationfindBy(Specification<T> spec, Function<? super SpecificationFluentQuery<S>, R> queryFunction)spring-doc.cadn.net.cn

与其他方法一样,它会执行一个从 Specification 派生的查询。 然而,query 方法允许你控制那些通常无法动态控制的查询执行细节。 你可以通过调用 SpecificationFluentQuery 的各种中间方法和终止方法来实现这一点。spring-doc.cadn.net.cn

  • firstfirstValue:返回第一个值。first 返回一个 Optional<T>,如果查询未产生任何结果,则返回 Optional.empty()firstValue 是其可空变体,无需使用 Optionalspring-doc.cadn.net.cn

  • oneoneValue:返回单个值。one 返回一个 Optional<T>,如果查询未产生任何结果,则返回 Optional.empty()oneValue 是其可为空的变体,无需使用 Optional。 如果找到多个匹配项,则抛出 IncorrectResultSizeDataAccessExceptionspring-doc.cadn.net.cn

  • all:将所有结果以 List<T> 的形式返回。spring-doc.cadn.net.cn

  • page(Pageable):将所有结果以 Page<T> 的形式返回。spring-doc.cadn.net.cn

  • slice(Pageable):将所有结果以 Slice<T> 的形式返回。spring-doc.cadn.net.cn

  • scroll(ScrollPosition):使用滚动(偏移量、键集)以 Window<T> 的形式检索结果。spring-doc.cadn.net.cn

  • stream():返回一个 Stream<T> 以惰性方式处理结果。 该流是有状态的,使用后必须关闭。spring-doc.cadn.net.cn

  • countexists:返回匹配实体的数量,或判断是否存在任何匹配项。spring-doc.cadn.net.cn

中间方法和终端方法必须在查询函数内调用。
示例 3. 使用流式 API 获取一个按 Page 排序的投影 lastname
Page<CustomerProjection> page = repository.findBy(spec,
    q -> q.as(CustomerProjection.class)
          .page(PageRequest.of(0, 20, Sort.by("lastname")))
);
示例 4. 使用流式 API 获取按 lastname 排序后可能存在的多个结果中的最后一个
Optional<Customer> match = repository.findBy(spec,
    q -> q.sortBy(Sort.by("lastname").descending())
          .first()
);