定义查询方法

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

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

  • 通过使用手动定义的查询。

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

查询查找策略

以下策略可用于存储库基础设施来解析查询。使用 XML 配置,您可以在命名空间中通过 `query-lookup-strategy` 属性配置策略。对于 Java 配置,您可以使用 `EnableJpaRepositories` 注解的 `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);
}

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

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

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

  • 表达式通常是属性遍历与可以连接的操作符的组合。您可以将属性表达式与ANDOR组合。您还可以为属性表达式获得对BetweenLessThanGreaterThanLike等操作符的支持。支持的操作符可能因数据存储而异,因此请查阅参考文档的相应部分。

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

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

属性表达式

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

List<Person> findByAddressZipCode(ZipCode zipCode);

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

虽然这应该适用于大多数情况,但算法可能会选择错误的属性。假设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 的 StreamableIterable 的自定义扩展),以及 Vavr 提供的集合类型。请参阅附录,其中解释了所有可能的 查询方法返回类型

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

您可以使用 Streamable 作为 Iterable 或任何集合类型的替代方案。它提供了一些便利方法来访问非并行 StreamIterable 中缺少的)以及直接 ….filter(…)….map(…) 遍历元素并将 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(Priced::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` 作为返回类型。

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

要处理查询中的参数,请定义方法参数,如前面的示例中所示。除此之外,基础设施识别某些特定类型,如 `Pageable`、`Sort` 和 `Limit`,以动态地将分页、排序和限制应用于您的查询。以下示例演示了这些功能

在查询方法中使用 `Pageable`、`Slice`、`Sort` 和 `Limit`
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);
采用 `Sort`、`Pageable` 和 `Limit` 的 API 期望将非 `null` 值传递给方法。如果您不想应用任何排序或分页,请使用 `Sort.unsorted()`、`Pageable.unpaged()` 和 `Limit.unlimited()`。

第一种方法允许您将一个 org.springframework.data.domain.Pageable 实例传递给查询方法,以动态地为您的静态定义的查询添加分页。一个 Page 知道可用的元素总数和页面数。它通过基础设施触发一个计数查询来计算总数。由于这可能很昂贵(取决于使用的存储),您可以改为返回一个 Slice。一个 Slice 只知道是否有一个下一个 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的消费方式,分块(逐个或批量)获取数据。

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

使用完流后必须关闭,以避免资源泄漏。

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(Limit limit);

User findFirstByOrderByLastnameAsc();

User findTopByOrderByAgeDesc();

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

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

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

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

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

如果将分页或切片应用于限制性查询分页(以及可用页面数量的计算),则它将应用于限制结果内。

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