Spring JPA 定义查询方法

时间:2022-07-24
本文章向大家介绍Spring JPA 定义查询方法,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

Spring JPA 定义查询方法

翻译:Defining Query Methods

​ 存储库代理有两种方式基于方法名派生特定域的查询方式:

  • 直接从方法名派生查询
  • 自定义查询方式

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

1、查询查找策略

​ 以下策略可用于存储库基础结构来解决查询。使用XML配置,可以通过querylookup strategy属性在名称空间配置策略。对于Java配置,可以使用Enable${store}Repositories注释的queryLookupStrategy属性。但某些策略可能不支持特定的数据存储。

  • create查询方式尝试从查询方法名称构造特定于存储的查询。一般是删除从方法中删除不用的部分,然后细化用到的部分。你可以从Query-Creation了解更多关于查询创建的内容。
  • USE_DECLARED_QUERY尝试查找已声明的查询,如果找不到则引发异常。查询可以通过某个地方的注释进行定义,或通过其他方式进行声明。请参阅特定存储库方法的文档,以找到该存储库内的可用方法。如果存储库基础结构在引导时未找到方法的声明查询,则导致失败。
  • CREATE_IF_NOT_FOUND(默认)结合CREATEUSE_DECLARED_QUERY的查询。它首先查找已声明的查询,如果没有找到声明的查询,它将创建一个基于自定义方法名的查询。这是默认的查找策略,因此,如果未显式配置任何内容,则使用此策略。它允许通过方法名快速定义查询,还可以根据需要引入声明的查询来定制这些查询。

2、查询创建

​ Spring数据存储库基础方法中内置的查询生成器机制对于在存储库的实体上构建的约束查询非常有用。该机制从方法中剥离前缀find…By、read…By、query…By、count…Byget…By,并开始解析其余部分。引入子句可以包含更多的表达式,例如在要创建的查询上设置Distinct标志的Distinct。第一个By充当分隔符,指示实际条件的开始。您可以定义实体属性的条件,并将它们使用andOr连接起来。以下示例演示如何创建多个查询:

例13:从方法名创建查询

interface PersonRepository extends Repository<Person, Long> {

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

  // 允许去重查询
  List<Person> findDistinctPeopleByLastnameOrFirstname(String lastname, String firstname);
  List<Person> findPeopleDistinctByLastnameOrFirstname(String lastname, String firstname);

  // 允许忽略大小写查询
  List<Person> findByLastnameIgnoreCase(String lastname);
  // Enabling ignoring case for all suitable properties
  List<Person> findByLastnameAndFirstnameAllIgnoreCase(String lastname, String firstname);

  // 允许查询结果进行排序
  List<Person> findByLastnameOrderByFirstnameAsc(String lastname);
  List<Person> findByLastnameOrderByFirstnameDesc(String lastname);
}

解析方法的实际结果取决于是基于哪个持久类实体进行的查询创建,但是,也有一些一般性问题需要注意:

  • 表达式通常是属性字段和运算符组合在一起进行遍历,你可以使用AND或者OR组合属性表达式,同时也支持Between, LessThan, GreaterThan, 和Like等运算符,支持的运算符可能因数据存储而异,具体请参考文档的相应部分。
  • 方法解析器支持为单个属性(例如findByLastnameIgnoreCase(…))或支持忽略大小写的类型的所有属性设置IgnoreCase标志(通常是字符串实例  ,例如findByLastnameAndFirstnameAllIgnoreCase(…))。是否支持忽略大小写可能因存储而异,因此请参阅参考文档中的相关部分以了解特定于存储的查询方法。
  • 通过向引用属性的查询方法追加OrderBy子句并提供排序方向(AscDesc),可以应用静态排序。要创建支持动态排序的查询方法,请参阅“特殊参数处理”。

3、属性表达式

​ 属性表达式只能引用实体类定义的直接属性,如上例所示,在创建查询时,你已经确定属性是实体类对应域中的属性,除此之外,还可以通过嵌套属性定义约束。

List<Person> findByAddressZipCode(ZipCode zipCode);

​ 假定一个人拥有一个带邮政编码的地址,在这种情况下,该方法遍历创建属性x.address.zipCode. 解析算法首先将整个部分(AddressZipCode)解释为属性,然后在域类中检查具有该名称(未大写)的属性。如果算法成功,则使用该属性。如果不是这样,算法会把驼峰命名部分的源代码拆分,并尝试在我们的示例中找到相应的属性 AddressZipCode。如果算法找到一个带有该头部的属性,它将获取尾部并继续从那里构建树,并按照刚才描述的方式将尾部拆分。如果第一个拆分不匹配,则算法将拆分点向左移动(Address、ZipCode)并继续。

举例说明拆分:AaBbCc 第一次拆分 AaBb / Cc 获取属性方式 AaBb.Cc 第二次拆分 Aa / BbCc 获取属性方式 Aa.BbCc

​ 尽管这在大多数情况下都是可行的,但算法仍然可能会选择错误的属性。假设Person类也有一个addressZip属性。该算法已经在第一轮分割中匹配,选择了错误的属性,然后就会失败(因为addressZip的类型可能没有代码属性)。

​ 要解决这种歧义,可以在方法名内部手动定义遍历点(以 - 定义遍历点)。

List<Person> findByAddress_ZipCode(ZipCode zipCode);

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

4、特殊参数处理

​ 要处理查询中的参数,请像前面示例中所看到的那样定义方法参数。除此之外,基础结构还识别某些特定类型,如分页和排序,动态地对查询应用分页和排序。下面的示例演示了这些特性。

例14:在查询中使用Pageable, Slice, 和 Sort

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, Pageable pageable);

采用排序和可分页的api希望将非空值传递给方法。如果不想应用任何排序或分页,可以使用Sort.unsorted()和Pageable.unpaged()。

​ 第一个方法允许您传递一个org.springframework.data.domain查询方法的分页实例,以动态地向静态定义的查询添加分页。Page获取到了可用元素和页面的总数。它是通过基础结构触发计数查询来计算总数量来实现的。因为这可能会废算力(取决于所使用的存储),所以可以返回一个Slice。一个片只知道下一个片是否可用,这在遍历更大的结果集时可能就足够了。

TIPS:出于性能优化考虑,建议使用Slice

​ 排序同样通过Pageable实例进行处理,如果你只需要进行排序,只需要在你的方法中添加一个org.springframework.data.domain.Sort参数。如您所见,返回列表也是可能的。 在这种情况下,将不会创建构建Page实例所需的其他元数据(这意味着没有发出必要的附加计数查询)。相反,它将查询限制为仅查找给定范围的实体。

要查明整个查询得到了多少页,必须触发一个额外的count查询。默认情况下,该查询派生自您实际触发的查询。

可以使用属性名定义简单的排序表达式。可以将表达式连接起来,将多个表达式整合到一个表达式中。

例15:定义查询表达式

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

要以更类型安全的方式定义排序表达式,请从定义用于的排序表达式的类型开始,并使用方法引用定义要排序的属性

例16:使用类型安全的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等工具时,CGlib可能会干扰本机映像的编译。

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

例17: 使用Querydsl API定义排序表达式

QSort sort = QSort.by(QPerson.firstname.asc())
  .and(QSort.by(QPerson.lastname.desc()));

5、查询结果限制

查询结果可以使用互换使用的top或者first关键字来进行限制,可以将一个可变的数字值附加到topfirst,以指定返回的最大结果大小。如果遗漏了这个数字,则使用默认值1。下面的示例显示如何限制查询大小。

例18:使用topfirst限制查询返回结果的大小

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参数来限制结果与动态排序的组合,可以表达最小和最大元素的查询方法。

6、返回集合或迭代的存储库方法

​ 返回多个结果的查询方法可以使用标准的Java Iterable, List, Set。除此之外,我们还支持返回Spring数据的Streamable, Iterable的自定义扩展,以及Vavr提供的集合类型。

例19:使用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 { 
    //产品实体公开访问价格的API
  MonetaryAmount getPrice() { … }
}

@RequiredArgConstructor(staticName = "of")
class Products implements Streamable<Product> { 
//可通过产品构造的Streamable<Product>的包装器类型。of()(通过Lombok注释创建的工厂方法)。
  private Streamable<Product> streamable;

  public MonetaryAmount getTotal() { 
    return streamable.stream() //包装器类型公开了在Streamable<Product>上计算新值的附加API。
      .map(Priced::getPrice)
      .reduce(Money.of(0), MonetaryAmount::add);
  }
}

interface ProductRepository implements Repository<Product, Long> {
    //包装器类型可以直接用作查询方法返回类型。不需要返回Stremable<Product>并手动将其封装到存储库客户机中。
  Products findAllByDescriptionContaining(String text); 
}

Vavr 集合的支持

Vavr是一个包含Java中函数式编程概念的库。它附带了一组可用作查询方法返回类型的自定义集合类型。

Vavr 集合类型

Vavr 实现的类型

Valid 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类型(第三列)获得作为实现类型的第二列中的类型。然后通过实现派生类的方法进行类型转化。

7、空值方法处理库

​ 在Spring Data 2.0中,返回单个聚合实例的存储库CRUD方法使用Java 8 s可选来指示可能缺少的值。除此之外,Spring Data还支持在查询方法上返回以下包装器类型:

  • com.google.common.base.Optional
  • scala.Option
  • io.vavr.control.Option

​ 或者,查询方法可以选择根本不使用包装器类型。如果没有查询结果,则返回null。返回集合、集合替代、包装器和流的存储库方法保证不会返回null,而是返回相应的空表示。有关详细信息,请参见存储库查询返回类型。

空值注解

您可以使用Spring Framework的可空性注释来表示存储库方法的可空性约束。它们提供了一种工具友好的方法,并在运行时选择空检查,如下所示:

  • @NonNullApi:在包级别上使用,用于声明参数和返回值的默认行为是不接受或生成空值。
  • @NonNull:用于不能为null的参数或返回值(在@NonNullApi应用的地方,参数和返回值不需要)。
  • @Nullable:用于可以为空的参数或返回值。

Spring注释使用JSR 305注释(一种停止维护但广泛传播的JSR)进行元注释。JSR 305元注释让工具供应商(如IDEA、Eclipse和Kotlin)以通用的方式提供空安全支持,而不必对Spring注释进行硬编码支持。要启用查询方法的nullability约束的运行时检查,您需要在package-info中使用Spring 的@NonNullApi来激活package-info.java上的非空配置,如下面的示例所示

例20:在包级别上声明非空

@org.springframework.lang.NonNullApi
package com.acme;

​ 一旦设置了非空默认值,存储库查询方法调用将在运行时验证是否存在可空性约束。如果查询执行结果违反定义的约束,则抛出异常。当方法将返回null,但声明为不可空时(存储库所在的包上定义的注释的默认值),就会发生这种情况。如果您希望再次选择可为空的结果,可以在单个方法上有选择地使用@Nullable。使用本节开始提到的结果包装器类型继续按预期工作:空结果被转换为表示缺席的值。下面的示例显示了刚才描述的许多技术:

例21:使用不同的空值配置

package com.acme;                                 // 存储库驻留在一个包(或子包)中,我们为其定义了非空行为。                     

import org.springframework.lang.Nullable;

interface UserRepository extends Repository<User, Long> {

  User getByEmailAddress(EmailAddress emailAddress);      //当执行的查询不产生结果时,抛出EmptyResultDataAccessException。当传递给方法的电子邮件地址为空时,抛出IllegalArgumentException异常。              

  @Nullable
  User findByEmailAddress(@Nullable EmailAddress emailAdress);       //当执行的查询没有产生结果时,返回null。还接受null作为emailAddress的值。   

  Optional<User> findOptionalByEmailAddress(EmailAddress emailAddress); //当执行的查询不产生结果时,返回Optional.empty()。当传递给方法的电子邮件地址为空时,抛出IllegalArgumentException异常。
}

8、Stream化查询结果

​ 通过使用Java 8 Stream作为返回类型,可以渐进地处理查询方法的结果。与将查询结果包装在流数据存储中不同,使用特定的方法执行流,如下面的示例所示

例23:用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);

流可能包装了底层数据存储特定的资源,因此在使用后必须关闭。您可以使用close()方法手动关闭流,也可以使用Java 7 try-with-resources块,如下面的示例所示

例24:try-catch形式使用Stream

try (Stream<User> stream = repository.findAllByCustomQueryAndStream()) {
  stream.forEach(…);
}

9 、异步查询结果

​ 通过使用Spring的异步方法执行能力,存储库查询可以异步运行。这意味着,当实际的查询执行发生在已提交给Spring TaskExecutor的任务中时,该方法在调用时立即返回。异步查询执行与反应性查询执行不同,不应该混合使用。有关响应性支持的更多细节,请参阅特定于存储的文档。下面的示例显示了许多异步查询

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

@Async
CompletableFuture<User> findOneByFirstname(String firstname); 

@Async
ListenableFuture<User> findOneByLastname(String lastname);