定义查询方法

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

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

  • 使用手动定义的查询。

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

查询查找策略

仓库基础设施可以使用以下策略来解析查询。使用 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…Byexists…By)定义查询的主题,第二部分构成谓词。介绍性子句(主题)可以包含更多表达式。find(或其他介绍性关键字)和By之间的任何文本都被认为是描述性的,除非使用结果限制关键字(例如Distinct来设置要创建的查询的 distinct 标志或Top/First来限制查询结果)。

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

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

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

  • 方法解析器支持为单个属性设置IgnoreCase标志(例如,findByLastnameIgnoreCase(…))或为支持忽略大小写的类型的所有属性设置IgnoreCase标志(通常是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。如果算法找到具有该头的属性,则它将获取尾部并继续从那里向下构建树,按照刚才描述的方式拆分尾部。如果第一次拆分不匹配,算法将拆分点向左移动 (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连接的能力。

使用 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的消耗情况,分块(逐个或批量)。

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

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

Flux<T>

根据Flux的消耗情况,分块(逐个或批量)。

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

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

Slice<T>

Pageable.getPageSize() + 1 at Pageable.getOffset()

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

Slice只能导航到下一个Slice

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

  • 基于偏移量的查询在偏移量过大时效率低下,因为数据库仍然需要物化完整的结果。

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

  • 基于偏移量的查询在偏移量过大时效率低下,因为数据库仍然需要物化完整的结果。

Page<T>

Pageable.getPageSize()Pageable.getOffset()

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”个最大元素的查询方法。