定义查询方法

仓库代理有两种方法从方法名称中派生出特定于存储的查询

  • 直接从方法名称派生查询。

  • 使用手动定义的查询。

可用的选项取决于实际的存储。但是,必须有一个策略来决定创建什么样的实际查询。下一节将介绍可用的选项。

查询查找策略

仓库基础架构可以使用以下策略来解析查询。对于 XML 配置,您可以通过命名空间中的 query-lookup-strategy 属性配置策略。对于 Java 配置,您可以使用 EnableRedisRepositories 注解的 queryLookupStrategy 属性。某些策略可能不支持特定的数据存储。

  • CREATE 尝试从查询方法名称构造特定于存储的查询。一般方法是从方法名称中移除一组已知的特定前缀,然后解析方法的其余部分。您可以在“查询创建”中阅读更多关于查询构造的信息。

  • USE_DECLARED_QUERY 尝试查找一个已声明的查询,如果找不到则抛出异常。查询可以通过注解或其他方式在某处定义或声明。请查阅特定存储的文档以了解该存储可用的选项。如果在引导时仓库基础架构找不到该方法的已声明查询,则会失败。

  • CREATE_IF_NOT_FOUND (默认) 结合了 CREATEUSE_DECLARED_QUERY。它首先查找一个已声明的查询,如果找不到已声明的查询,则创建一个基于方法名称的自定义查询。这是默认的查找策略,因此,如果您未显式配置任何内容,将使用此策略。它允许通过方法名称快速定义查询,但也允许通过根据需要引入已声明的查询来自定义调整这些查询。

查询创建

Spring Data 仓库基础架构内置的查询构建机制对于构建针对仓库实体进行约束的查询非常有用。

下面的例子展示了如何创建一些查询

从方法名称创建查询
interface PersonRepository extends Repository<Person, Long> {

  List<Person> findByEmailAddressAndLastname(EmailAddress emailAddress, String lastname);

  // Enables the distinct flag for the query
  List<Person> findDistinctPeopleByLastnameOrFirstname(String lastname, String firstname);
  List<Person> findPeopleDistinctByLastnameOrFirstname(String lastname, String firstname);

  // Enabling ignoring case for an individual property
  List<Person> findByLastnameIgnoreCase(String lastname);
  // Enabling ignoring case for all suitable properties
  List<Person> findByLastnameAndFirstnameAllIgnoreCase(String lastname, String firstname);

  // Enabling static ORDER BY for a query
  List<Person> findByLastnameOrderByFirstnameAsc(String lastname);
  List<Person> findByLastnameOrderByFirstnameDesc(String lastname);
}

解析查询方法名称分为主体 (subject) 和谓词 (predicate)。第一部分 (find…By, exists…By) 定义了查询的主体,第二部分构成了谓词。引入子句 (主体) 可以包含进一步的表达式。find (或其他引入关键字) 和 By 之间的任何文本都被认为是描述性的,除非使用了限制结果的关键字,例如 Distinct 用于设置创建的查询的 distinct 标志,或者 Top/First 用于限制查询结果

附录包含了查询方法主体关键字的完整列表以及查询方法谓词关键字,包括排序和大小写修饰符。然而,第一个 By 充当分隔符,表示实际条件谓词的开始。在最基本的层面上,您可以在实体属性上定义条件,并使用 AndOr 将它们连接起来。

解析方法的实际结果取决于您创建查询所用的持久化存储。但是,有一些一般性的注意事项

  • 表达式通常是属性遍历与可以连接的运算符的组合。您可以将属性表达式与 ANDOR 结合使用。您还可以获得对属性表达式的 Between, LessThan, GreaterThan, 和 Like 等运算符的支持。支持的运算符可能因数据存储而异,因此请查阅您的参考文档中的相应部分。

  • 方法解析器支持为单个属性设置 IgnoreCase 标志 (例如,findByLastnameIgnoreCase(…)),或为支持忽略大小写的所有类型属性设置该标志 (通常是 String 实例 — 例如,findByLastnameAndFirstnameAllIgnoreCase(…))。是否支持忽略大小写可能因存储而异,因此请查阅参考文档中特定于存储的查询方法的相关章节。

  • 您可以通过在引用属性的查询方法后附加 OrderBy 子句并提供排序方向 (AscDesc) 来应用静态排序。要创建支持动态排序的查询方法,请参见“分页、迭代大型结果、排序和限制”。

保留方法名称

虽然派生的仓库方法按名称绑定到属性,但在涉及到从基础仓库继承的某些方法名称时,这些方法名称以标识符属性为目标,而有一些例外。这些保留方法,如 CrudRepository#findById (或仅 findById),无论声明的方法中使用的实际属性名称是什么,都以标识符属性为目标。

考虑以下领域类型,它包含一个通过 @Id 标记为标识符的属性 pk,以及一个名为 id 的属性。在这种情况下,您需要密切注意查找方法的命名,因为它们可能与预定义的签名冲突

class User {
  @Id Long pk;                          (1)

  Long id;                              (2)

  // …
}

interface UserRepository extends Repository<User, Long> {

  Optional<User> findById(Long id);     (3)

  Optional<User> findByPk(Long pk);     (4)

  Optional<User> findUserById(Long id); (5)
}
1 标识符属性 (主键)。
2 一个名为 id 的属性,但不是标识符。
3 pk 属性 (标记有 @Id 并被视为标识符的属性) 为目标,因为它引用了一个 CrudRepository 基础仓库方法。因此,它不是一个使用 id 作为属性名称所暗示的派生查询,因为它是保留方法之一。
4 通过名称以 pk 属性为目标,因为它是一个派生查询。
5 通过使用 findby 之间的描述性标记来避免与保留方法冲突,从而以 id 属性为目标。

这种特殊行为不仅适用于查找方法,也适用于 exitsdelete 方法。请参阅“仓库查询关键字”以获取方法列表。

属性表达式

属性表达式只能引用受管实体的直接属性,如前面的示例所示。在查询创建时,您已经确保解析的属性是受管领域类的属性。但是,您也可以通过遍历嵌套属性来定义约束。考虑以下方法签名

List<Person> findByAddressZipCode(ZipCode zipCode);

假设 Person 有一个带有 ZipCodeAddress。在这种情况下,方法创建 x.address.zipCode 属性遍历。解析算法首先将整个部分 (AddressZipCode) 解释为属性,并在领域类中检查是否存在该名称 (首字母小写) 的属性。如果算法成功,则使用该属性。否则,算法从右侧按驼峰命名部分将源字符串拆分为头部和尾部,并尝试查找相应的属性——在我们的示例中,是 AddressZipCode。如果算法找到具有该头部的属性,它将取尾部并从那里继续构建树,以刚刚描述的方式拆分尾部。如果第一次拆分不匹配,算法将拆分点向左移动 (Address, ZipCode) 并继续。

尽管这应该适用于大多数情况,但算法有可能选择了错误的属性。假设 Person 类还有一个 addressZip 属性。算法会在第一次拆分时就匹配,选择错误的属性,然后失败 (因为 addressZip 的类型可能没有 code 属性)。

为了解决这种歧义,您可以在方法名称中使用 _ 手动定义遍历点。因此,我们的方法名称如下

List<Person> findByAddress_ZipCode(ZipCode zipCode);

因为我们将下划线 (_) 视为保留字符,所以强烈建议遵循标准的 Java 命名约定 (即,不在属性名称中使用下划线,而是应用驼峰命名法)。

以下划线开头的字段名称

字段名称可以以下划线开头,如 String _name。请确保保留 _,如在 _name 中所示,并使用双下划线 __ 来分隔嵌套路径,如 user__name

全大写字段名称

全大写的字段名称可以照原样使用。如果适用,嵌套路径需要通过 _ 进行拆分,如在 USER_name 中所示。

第二个字母为大写的字段名称

由小写字母开头后跟一个大写字母组成的字段名称,如 String qCode,可以通过以两个大写字母开头来解析,如 QCode。请注意潜在的路径歧义。

路径歧义

在下面的示例中,属性 qCodeq (其中 q 包含一个名为 code 的属性) 的排列方式为路径 QCode 创建了歧义。

record Container(String qCode, Code q) {}
record Code(String code) {}

由于首先考虑属性的直接匹配,任何潜在的嵌套路径都不会被考虑,算法将选择 qCode 字段。为了选择 q 中的 code 字段,需要使用下划线表示法 Q_Code

返回集合或可迭代对象的仓库方法

返回多个结果的查询方法可以使用标准的 Java IterableListSet。除此之外,我们还支持返回 Spring Data 的 Streamable (这是 Iterable 的一个自定义扩展),以及 Vavr 提供的集合类型。请参阅附录,其中解释了所有可能的查询方法返回类型

使用 Streamable 作为查询方法返回类型

您可以使用 Streamable 作为 Iterable 或任何集合类型的替代方案。它提供了方便的方法来访问非并行 Stream (Iterable 中没有此功能),以及直接对元素进行 ….filter(…)….map(…) 操作,并将 Streamable 与其他 Streamable 连接起来的功能

使用 Streamable 组合查询方法结果
interface PersonRepository extends Repository<Person, Long> {
  Streamable<Person> findByFirstnameContaining(String firstname);
  Streamable<Person> findByLastnameContaining(String lastname);
}

Streamable<Person> result = repository.findByFirstnameContaining("av")
  .and(repository.findByLastnameContaining("ea"));

返回自定义 Streamable 包装器类型

为集合提供专门的包装器类型是提供返回多个元素的查询结果 API 的常用模式。通常,这些类型是通过调用返回集合类型数据的仓库方法,然后手动创建包装器类型的实例来使用的。您可以避免额外的步骤,因为 Spring Data 允许您将这些包装器类型用作查询方法的返回类型,只要它们满足以下条件

  1. 该类型实现了 Streamable 接口。

  2. 该类型暴露了一个构造函数或一个名为 of(…)valueOf(…) 的静态工厂方法,该方法接受 Streamable 作为参数。

以下清单展示了一个示例

class Product {                                         (1)
  MonetaryAmount getPrice() { … }
}

@RequiredArgsConstructor(staticName = "of")
class Products implements Streamable<Product> {         (2)

  private final Streamable<Product> streamable;

  public MonetaryAmount getTotal() {                    (3)
    return streamable.stream()
      .map(Product::getPrice)
      .reduce(Money.of(0), MonetaryAmount::add);
  }


  @Override
  public Iterator<Product> iterator() {                 (4)
    return streamable.iterator();
  }
}

interface ProductRepository implements Repository<Product, Long> {
  Products findAllByDescriptionContaining(String text); (5)
}
1 一个 Product 实体,暴露了访问产品价格的 API。
2 一个 Streamable<Product> 的包装器类型,可以使用 Products.of(…) (使用 Lombok 注解创建的工厂方法) 构建。一个接受 Streamable<Product> 的标准构造函数也同样有效。
3 该包装器类型暴露了一个额外的 API,可以在 Streamable<Product> 上计算新值。
4 实现 Streamable 接口并委托给实际结果。
5 这个包装器类型 Products 可以直接用作查询方法的返回类型。您不需要返回 Streamable<Product> 并在仓库客户端中查询后手动包装它。

支持 Vavr 集合

Vavr 是一个在 Java 中拥抱函数式编程概念的库。它带有一组自定义的集合类型,您可以将其用作查询方法的返回类型,如下表所示

Vavr 集合类型 使用的 Vavr 实现类型 有效的 Java 源类型

io.vavr.collection.Seq

io.vavr.collection.List

java.util.Iterable

io.vavr.collection.Set

io.vavr.collection.LinkedHashSet

java.util.Iterable

io.vavr.collection.Map

io.vavr.collection.LinkedHashMap

java.util.Map

您可以使用第一列中的类型 (或其子类型) 作为查询方法的返回类型,并根据实际查询结果的 Java 类型 (第三列) 获取第二列中用作实现类型的类型。或者,您可以声明 Traversable (Vavr 的 Iterable 等效类型),然后我们将从实际返回值派生出实现类。也就是说,java.util.List 将转换为 Vavr ListSeqjava.util.Set 将变为 Vavr LinkedHashSet Set,依此类推。

流式查询结果

您可以使用 Java 8 的 Stream<T> 作为返回类型来增量处理查询方法的结果。与将查询结果包装到 Stream 中不同,这里使用数据存储特定的方法来执行流式处理,如以下示例所示

使用 Java 8 Stream<T> 对查询结果进行流式处理
@Query("select u from User u")
Stream<User> findAllByCustomQueryAndStream();

Stream<User> readAllByFirstnameNotNull();

@Query("select u from User u")
Stream<User> streamAllPaged(Pageable pageable);
Stream 可能包装了底层数据存储特定的资源,因此在使用后必须关闭。您可以通过使用 close() 方法手动关闭 Stream,或者使用 Java 7 的 try-with-resources 块,如以下示例所示
try-with-resources 块中处理 Stream<T> 结果
try (Stream<User> stream = repository.findAllByCustomQueryAndStream()) {
  stream.forEach(…);
}
目前并非所有 Spring Data 模块都支持将 Stream<T> 作为返回类型。

异步查询结果

您可以通过使用 Spring 的异步方法运行能力来异步运行仓库查询。这意味着方法在调用后立即返回,而实际查询则在提交给 Spring TaskExecutor 的任务中进行。异步查询与响应式查询不同,不应混用。有关响应式支持的更多详细信息,请参阅特定于存储的文档。以下示例展示了一些异步查询

@Async
Future<User> findByFirstname(String firstname);               (1)

@Async
CompletableFuture<User> findOneByFirstname(String firstname); (2)
1 使用 java.util.concurrent.Future 作为返回类型。
2 使用 Java 8 的 java.util.concurrent.CompletableFuture 作为返回类型。

分页、迭代大型结果、排序和限制

要在查询中处理参数,请定义方法参数,如前面的示例所示。除此之外,基础架构还识别一些特定类型,如 PageableSortLimit,以便动态地将分页、排序和限制应用于您的查询。以下示例演示了这些特性

在查询方法中使用 PageableSliceSortLimit
Page<User> findByLastname(String lastname, Pageable pageable);

Slice<User> findByLastname(String lastname, Pageable pageable);

List<User> findByLastname(String lastname, Sort sort);

List<User> findByLastname(String lastname, Sort sort, Limit limit);

List<User> findByLastname(String lastname, Pageable pageable);
接受 SortPageableLimit 的 API 期望将非 null 值传递给方法。如果您不想应用任何排序或分页,请使用 Sort.unsorted()Pageable.unpaged()Limit.unlimited()

第一个方法允许您将 org.springframework.data.domain.Pageable 实例传递给查询方法,以便动态地将分页添加到静态定义的查询中。Page 知道可用的元素总数和总页数。它通过基础架构触发一个计数查询来计算总数。由于这可能很昂贵 (取决于使用的存储),您可以选择返回一个 SliceSlice 只知道是否有下一个 Slice 可用,这在遍历大型结果集时可能已经足够。

排序选项也通过 Pageable 实例处理。如果您只需要排序,请向方法添加一个 org.springframework.data.domain.Sort 参数。如您所见,返回 List 也是可能的。在这种情况下,构建实际 Page 实例所需的附加元数据不会创建 (这反过来意味着不会发出原本必需的额外计数查询)。相反,它将查询限制为仅查找给定范围内的实体。

要了解整个查询有多少页,您必须触发一个额外的计数查询。默认情况下,此查询是从您实际触发的查询中派生的。

特殊参数在一个查询方法中只能使用一次。
上述一些特殊参数是互斥的。请考虑以下无效参数组合列表。

参数 示例 原因

PageableSort

findBy…​(Pageable page, Sort sort)

Pageable 已经定义了 Sort

PageableLimit

findBy…​(Pageable page, Limit limit)

Pageable 已经定义了一个限制。

用于限制结果的 Top 关键字可以与 Pageable 一起使用,其中 Top 定义了结果的总最大数量,而 Pageable 参数可能会减少此数量。

哪种方法是合适的?

Spring Data 抽象提供的值也许最好通过下表中列出的可能的查询方法返回类型来展示。下表显示了您可以从查询方法中返回哪些类型

表 1. 消费大型查询结果
方法 获取的数据量 查询结构 约束

List<T>

所有结果。

单个查询。

查询结果可能耗尽所有内存。获取所有数据可能非常耗时。

Streamable<T>

所有结果。

单个查询。

查询结果可能耗尽所有内存。获取所有数据可能非常耗时。

Stream<T>

分块 (逐个或批量),取决于 Stream 的消费方式。

通常使用游标的单个查询。

使用后必须关闭 Stream 以避免资源泄漏。

Flux<T>

分块 (逐个或批量),取决于 Flux 的消费方式。

通常使用游标的单个查询。

存储模块必须提供响应式基础架构。

Slice<T>

Pageable.getOffset() 处获取 Pageable.getPageSize() + 1 条数据

一个或多个查询,从 Pageable.getOffset() 开始获取数据并应用限制。

Slice 只能导航到下一个 Slice

  • Slice 提供了是否有更多数据可获取的详细信息。

  • 基于偏移量的查询在偏移量太大时会变得低效,因为数据库仍然必须物化完整的结果。

  • Window 提供了是否有更多数据可获取的详细信息。

  • 基于偏移量的查询在偏移量太大时会变得低效,因为数据库仍然必须物化完整的结果。

Page<T>

Pageable.getOffset() 处获取 Pageable.getPageSize() 条数据

一个或多个查询,从 Pageable.getOffset() 开始应用限制。此外,可能需要 COUNT(…) 查询来确定元素总数。

通常需要执行昂贵的 COUNT(…) 查询。

  • 基于偏移量的查询在偏移量太大时会变得低效,因为数据库仍然必须物化完整的结果。

分页和排序

您可以使用属性名称定义简单的排序表达式。您可以连接表达式将多个条件收集到一个表达式中。

定义排序表达式
Sort sort = Sort.by("firstname").ascending()
  .and(Sort.by("lastname").descending());

为了更类型安全地定义排序表达式,请从要定义排序表达式的类型开始,并使用方法引用来定义要排序的属性。

使用类型安全 API 定义排序表达式
TypedSort<Person> person = Sort.sort(Person.class);

Sort sort = person.by(Person::getFirstname).ascending()
  .and(person.by(Person::getLastname).descending());
TypedSort.by(…) (通常) 通过使用 CGlib 利用运行时代理,这在使用 Graal VM Native 等工具进行原生镜像编译时可能会产生干扰。

如果您的存储实现支持 Querydsl,您也可以使用生成的元模型类型来定义排序表达式

使用 Querydsl API 定义排序表达式
QSort sort = QSort.by(QPerson.firstname.asc())
  .and(QSort.by(QPerson.lastname.desc()));

限制查询结果

除了分页之外,还可以使用专门的 Limit 参数限制结果大小。您还可以通过使用 FirstTop 关键字来限制查询方法的结果,这两个关键字可以互换使用,但不能与 Limit 参数混用。您可以在 TopFirst 后附加一个可选的数值,以指定要返回的最大结果大小。如果省略数值,则假定结果大小为 1。以下示例展示了如何限制查询大小

使用 TopFirst 限制查询结果大小
List<User> findByLastname(String lastname, Limit limit);

User findFirstByOrderByLastnameAsc();

User findTopByLastnameOrderByAgeDesc(String lastname);

Page<User> queryFirst10ByLastname(String lastname, Pageable pageable);

Slice<User> findTop3By(Pageable pageable);

List<User> findFirst10ByLastname(String lastname, Sort sort);

List<User> findTop10ByLastname(String lastname, Pageable pageable);

限制表达式还支持支持 Distinct 查询的数据存储的 Distinct 关键字。此外,对于将结果集限制为单个实例的查询,支持使用 Optional 关键字包装结果。

如果将分页或分片应用于限制查询的分页 (以及可用页数的计算),则会在限制结果范围内应用。

将限制结果与使用 Sort 参数的动态排序相结合,可以表达查询方法以获取“K”个最小或“K”个最大元素。