Spring Data Commons 中的 `Page` 接口是为存储库查询方法提供分页支持的基础。它允许开发人员使用简单的方法签名请求数据块,包括页码、页面大小和排序选项。通过使用 Page 接口,开发人员可以轻松实现可分页的数据访问,而无需手动管理查询和分页逻辑。此外,Page 接口还提供了有关分页查询结果的元数据,例如总页数和总记录数,这对于创建用户友好的分页界面非常有用。

Defining Query Methods

  1. 直接从方法名派生查询

  2. 使用手动定义的查询

策略的选择取决于实际存储器。存储库的基础结构提供了策略选项,并允许你配置要使用的策略。查询构建器允许你在实体上创建约束查询。它支持嵌套属性遍历、条件连接、投影和限制,从而为你的查询提供灵活性。此外,基础结构还提供分页、排序和限制选项,以便于管理大型数据集。

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

  • 直接从方法名称导出查询。

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

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

Query Lookup Strategies

以下策略可用于存储库基础结构来解析查询。使用 XML 配置时,你可以通过 query-lookup-strategy 属性在命名空间中配置该策略。对于 Java 配置,可以使用 Enable{store}Repositories 注释的 queryLookupStrategy 属性。某些策略可能不受特定数据存储支持。

  • CREATE 尝试从查询方法名称构建特定于商店的查询。一般方法是从方法名称中删除一组已知的指定前缀,并解析方法的其余部分。你可以在 “Query Creation” 中阅读有关查询构造的更多信息。

  • USE_DECLARED_QUERY 尝试查找已声明的查询,如果它找不到,则抛出异常。该查询可以通过注解在某处定义,也可以通过其他方式声明。请参阅特定仓库的文档以查找该仓库的可用选项。如果在启动时存储库基础结构未为该方法找到已声明的查询,则它将失败。

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

Query Creation

Spring Data 存储库基础设施中内置的查询构建器机制可用于构建对存储库实体的约束查询。

以下示例展示了如何创建多个查询:

Query creation from method names
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…Byexists…By)定义查询的主题,第二部分形成谓词。引言子句(主题)可以包含更多表达式。find(或其他引言关键字)和 By 之间的所有文本都视为描述性文本,除非使用某个结果限制关键字,例如 Distinct 为要创建的查询设置一个不同标志或 <<`Top`/First 限制查询结果,repositories.limit-query-result>>。

附录包含 full list of query method subject keywordsquery method predicate keywords including sorting and letter-casing modifiers。但是,第一个 `By`用作分隔符来指示实际条件谓词的开始。在非常基本的层面上,您可以在实体属性上定义条件,并使用 `And`和 `Or`连接它们。

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

  • 表达式通常是属性遍历与可以连接的运算符的组合。您可以将属性表达式与 ANDOR 相结合。您还可以在属性表达式中获得对 BetweenLessThanGreaterThanLike 等运算符的支持。支持的运算符因数据存储而异,因此请查阅参考文档的相应部分以获取特定存储区的查询方法。

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

  • 你可以通过将 OrderBy 子句附加到引用属性的查询方法以及提供排序方向 (AscDesc) 来应用静态排序。要创建支持动态排序的查询方法,请参阅 “Paging, Iterating Large Results, Sorting &amp; Limiting”。

Property Expressions

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

List<Person> findByAddressZipCode(ZipCode zipCode);

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

虽然这在大多数情况下应该有效,但算法有可能选择错误的属性。假设 Person 类也具有 addressZip 属性。该算法将在第一轮拆分中匹配,选择错误的属性并失败(因为 addressZip 的类型可能没有 code 属性)。

要解决此歧义,你可以在方法名中使用 _ 手动定义遍历点。因此,我们的方法名称如下:

List<Person> findByAddress_ZipCode(ZipCode zipCode);

因为我们把下划线(_)当作保留字符,我们强烈建议遵循标准的 Java 命名惯例(即,不要在属性名中使用下划线,而采用驼峰式命名)。

Field Names starting with underscore:

字段名可以以下划线开头,比如 String name 。务必保留 ,比如在 name 中保留,并使用双 分隔嵌套路径,比如 user__name 。 .Upper Case Field Names: 完全大写的字段名可以按原样使用。如果适用,嵌套路径需要通过 _ 分隔,比如在 USER_name 中。 .Field Names with 2nd uppercase letter: 字段名由一个起始小写字母后跟一个大写字母组成,比如 String qCode,可以通过以两个大写字母开头来解析,比如 QCode。请注意潜在路径歧义。 .Path Ambiguities: 在以下示例中,属性 qCodeq 的排列,以及包含名为 code 的属性的 q,会为路径 QCode 创建歧义。

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

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

Repository Methods Returning Collections or Iterables

返回多个结果的查询方法可以使用标准 Java IterableList`和`Set。除此之外,我们还支持返回 Spring Data 的 Streamable,这是 `Iterable`的自定义扩展,以及由 Vavr提供的集合类型。请参阅解释所有可能的 query method return types的附录。

Using Streamable as Query Method Return Type

你可以使用 Streamable 作为 Iterable 或任何集合类型的替代。它提供便利方法来访问非并行 StreamIterable 中没有)以及直接 ….filter(…)….map(…) 元素并连接到其他 Streamable 的能力:

Using Streamable to combine query method results
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"));

Returning Custom Streamable Wrapper Types

为集合提供专用的包装器类型是一个常用模式,用于为返回多个元素的查询结果提供 API。通常,通过调用返回类似集合类型的存储库方法并手动创建包装器类型实例来使用这些类型。你可以避免此额外的步骤,因为 Spring Data 允许你在它们满足以下条件时将这些包装器类型用作查询方法返回类型:

  1. The type implements 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 可以公开 API 以访问产品价格的 Product 实体。
2 一个 Streamable&lt;Product&gt; 的包装类型,可以使用 Products.of(…)(使用 Lombok 注解创建的工厂方法)进行构造。采用 Streamable&lt;Product&gt; 的标准构造函数也可以。
3 包装器类型公开了一个附加的 API,用于在 Streamable&lt;Product&gt; 上计算新值。
4 实现 Streamable 接口,并将接口委托给实际结果。
5 包装类型 Products 可直接用作查询方法的返回类型。您不需要返回 Streamable&lt;Product&gt;,并在查询之后在存储库客户端中手动对其进行包装。

Support for Vavr Collections

Vavr 是一个在 Java 中包含函数式编程概念的库。它提供了可作为查询方法返回类型的自定义集合类型,如下表所示:

Vavr collection type Used Vavr implementation type Valid Java source types

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,依此类推。

Streaming Query Results

你可以通过使用 Java 8 Stream<T> 作为返回类型来增量处理查询方法的结果。数据存储器特定方法用于执行流式处理,而不是将查询结果包装在 Stream 中,如以下示例所示:

Stream the result of a query with 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);

A Stream 可能会包装潜在的数据存储特定资源,因此必须在使用后将其关闭。您可以使用 close() 方法或使用 Java 7 try-with-resources 块手动关闭 Stream,如下例所示:

Working with a Stream<T> result in a try-with-resources block
try (Stream<User> stream = repository.findAllByCustomQueryAndStream()) {
  stream.forEach(…);
}

目前并非所有 Spring Data 模块都支持 Stream<T> 作为返回类型。

Asynchronous Query Results

您可以使用 {spring-framework-docs}/integration/scheduling.html[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 作为返回类型。

Paging, Iterating Large Results, Sorting & Limiting

要在查询中处理参数,请将方法参数定义为前面示例中已经看到的那样。除此之外,基础设施识别某些特定类型(如 PageableSortLimit),以动态地对你的查询应用分页、排序和限制。以下示例演示了这些功能:

Using Pageable, Slice, Sort and Limit in query methods
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 实例所需的附加元数据(这反过来意味着不会发出必需的附加计数查询)。相反,它会将查询限制为仅查找给定范围内的实体。

要找出整个查询获得多少页,您必须触发附加的计数查询。默认情况下,此查询是从您实际触发的查询派生出来的。

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

Parameters Example Reason

Pageable and Sort

findBy…​(Pageable page, Sort sort)

Pageable already defines Sort

Pageable and Limit

findBy…​(Pageable page, Limit limit)

Pageable 已定义限制。

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

Which Method is Appropriate?

Spring Data 抽象提供的价值也许最能通过下表中概述的可能的查询方法返回类型来体现。该表显示了你可以从查询方法返回哪些类型

Table 1. Consuming Large Query Results
Method Amount of Data Fetched Query Structure Constraints

<<`List<T>`,repositories.collections-and-iterables>>

All results.

Single query.

查询结果可能会耗尽所有内存。获取所有数据可能需要大量时间。

<<`Streamable<T>`,repositories.collections-and-iterables.streamable>>

All results.

Single query.

查询结果可能会耗尽所有内存。获取所有数据可能需要大量时间。

<<`Stream<T>`,repositories.query-streaming>>

分块(逐个或批处理),具体取决于 Stream 消耗情况。

通常使用游标进行单一查询。

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

Flux<T>

根据 Flux 的使用情况分块(逐个或批量)。

通常使用游标进行单一查询。

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

Slice<T>

Pageable.getOffset() 中的 Pageable.getPageSize() + 1

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

一个 Slice 只能导航到下一个 Slice。* Slice 提供有关是否有更多数据需要提取的详细信息。* 基于偏移量的查询在偏移量过大时变得效率低下,因为数据库仍然必须将完整的查询结果具体化。* Window 提供有关是否有更多数据需要提取的详细信息。* 基于偏移量的查询在偏移量过大时变得效率低下,因为数据库仍然必须将完整的查询结果具体化。

Page<T>

Pageable.getPageSize() at Pageable.getOffset()

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

通常情况下,需要 COUNT(…) 查询,这些查询代价很高。* 基于偏移量的查询在偏移量过大时变得效率低下,因为数据库仍然必须将完整的查询结果具体化。

Paging and Sorting

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

Defining sort expressions
Sort sort = Sort.by("firstname").ascending()
  .and(Sort.by("lastname").descending());

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

Defining sort expressions by using the type-safe 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,您还可以使用生成的元模型类型来定义排序表达式:

Defining sort expressions by using the Querydsl API
QSort sort = QSort.by(QPerson.firstname.asc())
  .and(QSort.by(QPerson.lastname.desc()));

Limiting Query Results

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

Limiting the result size of a query with Top and First
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 关键字,适用于支持不同查询的数据存储。此外,对于将结果集限制为一个实例的查询,支持用 Optional 关键字包装结果。

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

对结果进行限制,同时使用 Sort 参数进行动态排序,使您能够表达“K”个最小的元素以及“K”个最大的元素的查询方法。