原标题:Spring认证中国教育管理中心-Spring Data R2DBC框架教程三(Spring中国教育管理中心

14.2.查询方法

您通常在存储库上触发的大多数数据访问操作都会导致对数据库运行查询。定义这样的查询就是在存储库接口上声明一个方法,如以下示例所示:

示例 61.带有查询方法的 PersonRepository

interface ReactivePersonRepository extends ReactiveSortingRepository<Person, Long> {    Flux<Person> findByFirstname(String firstname);                                       Flux<Person> findByFirstname(Publisher<String> firstname);                            Flux<Person> findByFirstnameOrderByLastname(String firstname, Pageable pageable);     Mono<Person> findByFirstnameAndLastname(String firstname, String lastname);           Mono<Person> findFirstByLastname(String lastname);                                    @Query("SELECT * FROM person WHERE lastname = :lastname")   Flux<Person> findByLastname(String lastname);                                         @Query("SELECT firstname, lastname FROM person WHERE lastname = $1")   Mono<Person> findFirstByLastname(String lastname);                                 }

该方法显示了对所有具有给定 的人的查询firstname。该查询是通过解析可以与And和连接的约束的方法名称来派生的Or。因此,方法名称导致查询表达式为SELECT … FROM person WHERE firstname = :firstname。

firstname一旦给定的firstname发出 ,该方法就会显示对所有具有给定的人的查询Publisher。

使用Pageable来抵消和排序参数传递到数据库。

查找给定条件的单个实体。它以

IncorrectResultSizeDataAccessException非唯一结果结束。

除非 <4>,否则即使查询产生更多结果行,也总是发出第一个实体。

该findByLastname方法显示了对所有具有给定姓氏的人的查询。

对Person仅投影firstname和lastname列的单个实体的查询。带注释的查询使用本机绑定标记,在本例中是 Postgres 绑定标记。

Spring认证中国教育管理中心-Spring Data R2DBC框架教程三_2d

请注意,@Query注释中使用的 select 语句的列必须与NamingStrategy为相应属性生成的名称匹配。如果 select 语句不包含匹配的列,则不会设置该属性。如果持久性构造函数需要该属性,则提供 null 或(对于原始类型)默认值。

下表显示了查询方法支持的关键字:

Spring认证中国教育管理中心-Spring Data R2DBC框架教程三_字段_02

Spring认证中国教育管理中心-Spring Data R2DBC框架教程三_spring_03

14.2.1.修改查询

前面的部分描述了如何声明查询以访问给定的实体或实体集合。使用上表中的关键字可以与删除匹配行的派生查询结合使用delete…By或remove…By创建派生查询。

示例 62.Delete…By查询

interface ReactivePersonRepository extends ReactiveSortingRepository<Person, String> {    Mono<Integer> deleteByLastname(String lastname);                Mono<Void> deletePersonByLastname(String lastname);             Mono<Boolean> deletePersonByLastname(String lastname);       }

使用返回类型Mono<Integer>返回受影响的行数。

使用Voidjust 报告行是否已成功删除而不发出结果值。

使用Boolean报告是否至少删除了一行。

由于这种方法对于全面的自定义功能是可行的,您可以通过使用 注释查询方法来修改只需要参数绑定的查询@Modifying,如以下示例所示:

@Modifying @Query("UPDATE person SET firstname = :firstname where lastname = :lastname") Mono<Integer> setFixedFirstnameFor(String firstname, String lastname);

修改查询的结果可以是:

  • Void(或 Kotlin Unit)丢弃更新计数并等待完成。
  • Integer 或其他数字类型发出受影响的行数。
  • Boolean 发出是否至少更新了一行。

该@Modifying注释是唯一与组合相关的@Query注释。派生的自定义方法不需要此注释。

或者,您可以使用Spring Data Repositories 的自定义实现中描述的工具添加自定义修改行为。

14.2.2.使用 SpEL 表达式的查询

查询字符串定义可与 SpEL 表达式一起使用以在运行时创建动态查询。SpEL 表达式可以提供在运行查询之前计算的谓词值。

表达式通过包含所有参数的数组公开方法参数。以下查询用于[0] 声明 for lastname(相当于:lastname参数绑定)的谓词值:

@Query("SELECT * FROM person WHERE lastname = :#{[0]}") Flux<Person> findByQueryWithExpression(String lastname);

查询字符串中的 SpEL 可以成为增强查询的强大方法。但是,它们也可以接受范围广泛的不需要的参数。您应该确保在将字符串传递给查询之前对其进行清理,以避免对查询进行不必要的更改。

表达式的支持是可扩展的通过查询SPI:

org.springframework.data.spel.spi.EvaluationContextExtension。Query SPI 可以提供属性和函数,并且可以自定义根对象。构建查询时,在 SpEL 评估时从应用程序上下文中检索扩展。

将 SpEL 表达式与普通参数结合使用时,请使用命名参数表示法而不是本机绑定标记,以确保正确的绑定顺序。

14.2.3.按示例查询

Spring Data R2DBC 还允许您使用 Query By Example 来设计查询。此技术允许您使用“探针”对象。本质上,任何不为空或null将用于匹配的字段。

下面是一个例子:

Employee employee = new Employee();  employee.setName("Frodo");  Example<Employee> example = Example.of(employee);   Flux<Employee> employees = repository.findAll(example);   // do whatever with the flux

使用条件创建域对象(null字段将被忽略)。

使用域对象,创建一个Example.

通过R2dbcRepository, 执行查询(findOne用于 a Mono)。

这说明了如何使用域对象制作简单的探针。在这种情况下,它将根据Employee对象的name字段等于 进行查询Frodo。 null字段被忽略。

Employee employee = new Employee(); employee.setName("Baggins"); employee.setRole("ring bearer");  ExampleMatcher matcher = matching()      .withMatcher("name", endsWith())      .withIncludeNullValues()      .withIgnorePaths("role");  Example<Employee> example = Example.of(employee, matcher);   Flux<Employee> employees = repository.findAll(example);  // do whatever with the flux

创建一个ExampleMatcher匹配所有字段的自定义(用于matchingAny()匹配任何字段)

对于name字段,使用与字段末尾匹配的通配符

匹配列null(不要忘记在关系数据库NULL中不相等NULL)。

role形成查询时忽略该字段。

将自定义ExampleMatcher插入探头。

Spring认证中国教育管理中心-Spring Data R2DBC框架教程三_spring_04

也可以withTransform()对任何属性应用 a ,允许您在形成查询之前转换属性。例如,您可以在创建查询之前将 atoUpperCase()应用于String-based 属性。

当您事先不知道查询中所需的所有字段时,Query By Example 真的很有用。如果您在网页上构建过滤器,用户可以在其中选择字段,按示例查询是灵活地将其捕获到有效查询中的好方法。

14.2.4.实体状态检测策略

下表描述了 Spring Data 提供的用于检测实体是否为新实体的策略:

Spring认证中国教育管理中心-Spring Data R2DBC框架教程三_字段_05

14.2.5.身份证生成

Spring Data R2DBC 使用 ID 来标识实体。实体的 ID 必须使用 Spring Data 的@Id注解进行注解。

当您的数据库具有用于 ID 列的自动增量列时,生成的值在将其插入数据库后设置在实体中。

当实体是新的并且标识符值默认为其初始值时,Spring Data R2DBC 不会尝试插入标识符列的值。这适用0于原始类型,并且null如果标识符属性使用数字包装类型,例如Long.

一个重要的限制是,在保存实体后,该实体不能再是新的。请注意,实体是否是新实体是实体状态的一部分。对于自动增量列,这会自动发生,因为 ID 由 Spring Data 使用 ID 列中的值设置。

14.2.6.乐观锁定

该@Version注释在 R2DBC 的上下文中提供类似于 JPA 的语法,并确保更新仅应用于具有匹配版本的行。因此,version 属性的实际值被添加到更新查询中,如果另一个操作同时更改了该行,则更新不会产生任何影响。在这种情况下,
OptimisticLockingFailureException会抛出an 。以下示例显示了这些功能:

@Table class Person {    @Id Long id;   String firstname;   String lastname;   @Version Long version; }  R2dbcEntityTemplate template = …;  Mono<Person> daenerys = template.insert(new Person("Daenerys"));                        Person other = template.select(Person.class)                  .matching(query(where("id").is(daenerys.getId())))                  .first().block();                                                      daenerys.setLastname("Targaryen"); template.update(daenerys);                                                              template.update(other).subscribe(); // emits OptimisticLockingFailureException        

最初插入行。version设置为0。

加载刚刚插入的行。version还在0。

更新与行version = 0.SET的lastname和凹凸version来1。

尝试更新仍具有 的先前加载的行version = 0。操作失败并显示

OptimisticLockingFailureException,因为当前version是1。

Spring认证中国教育管理中心-Spring Data R2DBC框架教程三_2d_06

14.2.7.预测

Spring Data 查询方法通常返回存储库管理的聚合根的一个或多个实例。但是,有时可能需要根据这些类型的某些属性创建投影。Spring Data 允许对专用返回类型进行建模,以更有选择地检索托管聚合的部分视图。

想象一个存储库和聚合根类型,例如以下示例:

示例 63. 示例聚合和存储库

class Person {    @Id UUID id;   String firstname, lastname;   Address address;    static class Address {     String zipCode, city, street;   } }  interface PersonRepository extends Repository<Person, UUID> {    Flux<Person> findByLastname(String lastname); }

现在假设我们只想检索此人的姓名属性。Spring Data 提供什么方法​来实现这一目标?本章的其余部分回答了这个问题。

基于界面的投影

将查询结果限制为仅名称属性的最简单方法是声明一个接口,该接口公开要读取的属性的访问器方法,如以下示例所示:

示例 64. 用于检索属性子集的投影接口

interface NamesOnly {    String getFirstname();   String getLastname(); }

这里的重要一点是这里定义的属性与聚合根中的属性完全匹配。这样做可以添加一个查询方法,如下所示:

示例 65. 使用基于接口的投影和查询方法的存储库

interface PersonRepository extends Repository<Person, UUID> {    Flux<NamesOnly> findByLastname(String lastname); }

查询执行引擎在运行时为每个返回的元素创建该接口的代理实例,并将对公开方法的调用转发到目标对象。

在您Repository中声明一个覆盖基本方法的方法(例如,在 中声明CrudRepository,特定于商店的存储库接口或Simple…Repository)会导致对基本方法的调用,而不管声明的返回类型如何。确保使用兼容的返回类型,因为基本方法不能用于投影。一些商店模块支持@Query注释将覆盖的基本方法转换为查询方法,然后可用于返回投影。

可以递归地使用投影。如果您还想包含一些Address信息,请为此创建一个投影接口并从 的声明中返回该接口getAddress(),如下例所示:

示例 66. 用于检索属性子集的投影接口

interface PersonSummary {    String getFirstname();   String getLastname();   AddressSummary getAddress();    interface AddressSummary {     String getCity();   } }

在方法调用时,address获取目标实例的属性并依次包装到投影代理中。

封闭式投影

其访问器方法都与目标聚合的属性匹配的投影接口被认为是封闭投影。以下示例(我们在本章前面也使用过)是一个封闭投影:

例 67. 一个封闭的投影

interface NamesOnly {    String getFirstname();   String getLastname(); }

如果使用封闭投影,Spring Data 可以优化查询执行,因为我们知道支持投影代理所需的所有属性。有关更多详细信息,请参阅参考文档中特定于模块的部分。

打开投影

投影接口中的访问器方法也可用于通过使用@Value注释计算新值,如以下示例所示:

例 68. 一个开放的投影

interface NamesOnly {    @Value("#{target.firstname + ' ' + target.lastname}")   String getFullName();   … }

支持投影的聚合根在target变量中可用。使用的投影界面@Value是开放式投影。在这种情况下,Spring Data 无法应用查询执行优化,因为 SpEL 表达式可以使用聚合根的任何属性。

中使用的表达式@Value不应太复杂——您希望避免在String变量中编程。对于非常简单的表达式,一种选择可能是采用默认方法(在 Java 8 中引入),如以下示例所示:

示例 69. 使用自定义逻辑的默认方法的投影界面

interface NamesOnly {    String getFirstname();   String getLastname();    default String getFullName() {     return getFirstname().concat(" ").concat(getLastname());   } }

这种方法要求您能够纯粹基于投影接口上公开的其他访问器方法来实现逻辑。第二个更灵活的选择是在 Spring bean 中实现自定义逻辑,然后从 SpEL 表达式调用它,如以下示例所示:

示例 70. 示例 Person 对象

@Component class MyBean {    String getFullName(Person person) {     …   } }  interface NamesOnly {    @Value("#{@myBean.getFullName(target)}")   String getFullName();   … }

请注意 SpEL 表达式如何引用myBean和调用该getFullName(…)方法并将投影目标作为方法参数转发。由 SpEL 表达式评估支持的方法也可以使用方法参数,然后可以从表达式中引用这些参数。方法参数可通过Object名为的数组获得args。以下示例显示了如何从args数组中获取方法参数:

示例 71. 示例 Person 对象

interface NamesOnly {    @Value("#{args[0] + ' ' + target.firstname + '!'}")   String getSalutation(String prefix); }

同样,对于更复杂的表达式,您应该使用 Spring bean 并让表达式调用一个方法,如前所述。

可空包装器

投影接口中的 getter 可以使用可为空的包装器来提高空安全性。当前支持的包装器类型有:

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

示例 72. 使用可为空包装器的投影接口

interface NamesOnly {    Optional<String> getFirstname(); }

如果基础投影值不是null,则使用包装器类型的当前表示返回值。如果支持值是null,则 getter 方法返回所用包装器类型的空表示。

基于类的预测 (DTO)

定义投影的另一种方法是使用值类型 DTO(数据传输对象),这些 DTO 包含应该检索的字段的属性。这些 DTO 类型的使用方式与使用投影接口的方式完全相同,只是不发生代理并且不可以应用嵌套投影。

如果存储通过限制要加载的字段来优化查询执行,则要加载的字段由公开的构造函数的参数名称确定。

以下示例显示了一个投影 DTO:

例 73. 一个投影 DTO

class NamesOnly {    private final String firstname, lastname;    NamesOnly(String firstname, String lastname) {      this.firstname = firstname;     this.lastname = lastname;   }    String getFirstname() {     return this.firstname;   }    String getLastname() {     return this.lastname;   }    // equals(…) and hashCode() implementations }

避免投影 DTO 的样板代码

您可以使用Project Lombok显着简化 DTO 的代码,它提供了一个@Value注解(不要与@Value前面的接口示例中显示的Spring 的注解混淆)。如果使用 Project Lombok 的@Value注释,之前显示的示例 DTO 将变为以下内容:

@Value

class NamesOnly {

String firstname, lastname;

}

字段是private final默认的,并且该类公开了一个构造函数,该构造函数接受所有字段并自动获取equals(…)和hashCode()实现方法。

动态投影

到目前为止,我们已经使用投影类型作为集合的返回类型或元素类型。但是,您可能希望选择在调用时使用的类型(这使其成为动态的)。要应用动态投影,请使用如下例所示的查询方法:

示例 74. 使用动态投影参数的存储库

interface PersonRepository extends Repository<Person, UUID> {    <T> Flux<T> findByLastname(String lastname, Class<T> type); }

这样,该方法可用于按原样或应用投影获取聚合,如以下示例所示:

示例 75.使用具有动态投影的存储库

void someMethod(PersonRepository people) {    Flux<Person> aggregates =     people.findByLastname("Matthews", Person.class);    Flux<NamesOnly> aggregates =     people.findByLastname("Matthews", NamesOnly.class); }

结果映射

返回接口或 DTO 投影的查询方法由实际查询生成的结果支持。接口投影通常首先依赖于将结果映射到域类型来考虑潜在的@Column类型映射,而实际的投影代理使用潜在的部分物化实体来公开投影数据。

DTO 投影的结果映射取决于实际查询类型。派生查询使用域类型来映射结果,Spring Data 仅从域类型上可用的属性创建 DTO 实例。不支持在 DTO 中声明域类型上不可用的属性。

基于字符串的查询使用不同的方法,因为实际查询,特别是字段投影和结果类型声明是紧密结合在一起的。与查询方法一起使用的 DTO 投影,将@Query映射查询结果直接注释为 DTO 类型。不考虑域类型的字段映射。直接使用 DTO 类型,您的查询方法可以从不限于域模型的更动态的投影中受益。