# 前言

Spring Data Commons 项目将核心 Spring 概念应用于使用许多关系和非关系数据存储的解决方案的开发。

# 1. 项目元数据

# 2. 依赖关系

由于每个 Spring 数据模块的启动日期不同,它们中的大多数都带有不同的主要版本号和次要版本号。找到兼容版本的最简单的方法是依赖 Spring 数据发布列 BOM,我们提供的是定义的兼容版本。在 Maven 项目中,你将在 POM 的<dependencyManagement />部分中声明此依赖项,如下所示:

例 1。使用 Spring 数据发布列表 BOM

<dependencyManagement>
  <dependencies>
    <dependency>
      <groupId>org.springframework.data</groupId>
      <artifactId>spring-data-bom</artifactId>
      <version>2021.1.2</version>
      <scope>import</scope>
      <type>pom</type>
    </dependency>
  </dependencies>
</dependencyManagement>

当前的发行版本为2021.1.2。列车版本使用calver (opens new window)的模式YYYY.MINOR.MICRO。对于 GA 版本和服务版本,版本名如下:${calver},对于所有其他版本,版本名如下:${calver}-${modifier},其中modifier可以是以下几种类型之一:

  • SNAPSHOT:当前快照

  • M1M2,以此类推:里程碑

  • RC1RC2,以此类推:释放候选项

你可以在Spring Data examples repository (opens new window)中找到使用 BOMS 的工作示例。有了这一点,你就可以在<dependencies />块中声明希望使用的 Spring 数据模块,而不使用版本,如下所示:

例 2。声明对 Spring 数据模块的依赖项

<dependencies>
  <dependency>
    <groupId>org.springframework.data</groupId>
    <artifactId>spring-data-jpa</artifactId>
  </dependency>
<dependencies>

# 2.1.具有 Spring 引导的依赖管理

Spring 引导为你选择 Spring 数据模块的最新版本。如果你仍然希望升级到较新的版本,请将spring-data-releasetrain.version属性设置为希望使用的训练版本和迭代

# 2.2. Spring 框架

当前版本的 Spring 数据模块需要 Spring 框架 5.3.16 或更好。这些模块还可以与该小版本的旧 Bugfix 版本一起工作。但是,强烈建议你在这一代中使用最新的版本。

# 3. 对象映射基础

本节介绍了 Spring 数据对象映射、对象创建、字段和属性访问、可变性和不可变性的基本原理。注意,本节仅适用于不使用底层数据存储的对象映射的 Spring 数据模块(如 JPA)。还要确保查阅存储特定的部分以获得存储特定的对象映射,例如索引、自定义列或字段名称等。

Spring 数据对象映射的核心职责是创建域对象的实例,并将存储本机数据结构映射到这些实例上。这意味着我们需要两个基本步骤:

  1. 通过使用公开的构造函数之一创建实例。

  2. 实例填充以实体化所有公开的属性。

# 3.1.对象创建

Spring 数据自动地尝试检测用于实现该类型对象的持久性实体的构造函数。分辨率算法的工作原理如下:

  1. 如果只有一个构造函数,就使用它。

  2. 如果有多个构造函数,并且正好有一个用@PersistenceConstructor注释,则使用它。

  3. 如果有一个无参数构造函数,就使用它。其他构造函数将被忽略。

值解析假定构造函数参数名称与实体的属性名称匹配,即解析将被执行,就像要填充属性一样,包括映射中的所有自定义(不同的数据存储栏或字段名称等)。这还需要类文件中可用的参数名称信息,或者构造函数中存在@ConstructorProperties注释。

值分辨率可以通过使用 Spring Framework 的@Value使用特定于存储的 SPEL 表达式的值注释来定制。请参阅有关商店特定映射的部分以获取更多详细信息。

对象创建内部

Spring 为了避免反射的开销,数据对象创建默认使用在运行时生成的工厂类,它将直接调用域类构造函数。例如,对于这个示例类型:

class Person {
  Person(String firstname, String lastname) { … }
}

我们将在运行时创建一个在语义上与这个类等价的工厂类:

class PersonObjectInstantiator implements ObjectInstantiator {

  Object newInstance(Object... args) {
    return new Person((String) args[0], (String) args[1]);
  }
}

这给了我们一个迂回的 10% 的性能提升超过反映。为了使域类有资格进行这种优化,它需要遵守一组约束:

  • 它一定不是一个私人班级。

  • 它不能是非静态的内部类

  • 它一定不是一个 CGlib 代理类

  • Spring 数据使用的构造函数不能是私有的

如果这些条件中的任何一个匹配, Spring 数据将通过反射返回到实体实例化。

# 3.2.财产人口

一旦创建了实体的实例, Spring 数据就会填充该类的所有剩余的持久性属性。除非已经由实体的构造函数填充(即通过其构造函数参数列表填充),否则将首先填充标识符属性,以允许解析循环对象引用。在此之后,构造函数尚未填充的所有非瞬态属性都将在实体实例上设置。为此,我们使用以下算法:

  1. 如果属性是不可变的,但是公开了with…方法(见下文),那么我们使用with…方法使用新的属性值创建一个新的实体实例。

  2. 如果定义了属性访问(即通过 getter 和 setter 进行访问),那么我们调用的是 setter 方法。

  3. 如果属性是可变的,我们直接设置字段。

  4. 如果该属性是不可变的,那么我们将使用持久化操作使用的构造函数(参见对象创建)来创建实例的副本。

  5. 默认情况下,我们直接设置字段的值。

财产人口内部

与我们的对象构造中的优化类似,我们也使用 Spring 数据运行时生成的访问器类与实体实例交互。

class Person {

  private final Long id;
  private String firstname;
  private @AccessType(Type.PROPERTY) String lastname;

  Person() {
    this.id = null;
  }

  Person(Long id, String firstname, String lastname) {
    // Field assignments
  }

  Person withId(Long id) {
    return new Person(id, this.firstname, this.lastame);
  }

  void setLastname(String lastname) {
    this.lastname = lastname;
  }
}

例 3。生成的属性访问器

class PersonPropertyAccessor implements PersistentPropertyAccessor {

  private static final MethodHandle firstname;              (2)

  private Person person;                                    (1)

  public void setProperty(PersistentProperty property, Object value) {

    String name = property.getName();

    if ("firstname".equals(name)) {
      firstname.invoke(person, (String) value);             (2)
    } else if ("id".equals(name)) {
      this.person = person.withId((Long) value);            (3)
    } else if ("lastname".equals(name)) {
      this.person.setLastname((String) value);              (4)
    }
  }
}
1 PropertyAccessor 持有底层对象的可变实例。这是为了使本来不可变的属性发生突变。
2 默认情况下, Spring Data 使用字段访问来读写属性值。根据private字段的可见性规则,MethodHandles用于与字段交互。
3 该类公开了一个withId(…)方法,该方法用于设置标识符,例如,当一个实例被插入到数据存储中并生成了一个标识符时。调用withId(…)将创建一个新的Person对象。所有后续的突变都将发生在新实例中,而前一个实例将保持不变。
4 使用属性访问允许直接调用方法,而不使用MethodHandles

这给了我们一个迂回 25% 的性能提升超过反映。为了使域类有资格进行这种优化,它需要遵守一组约束:

  • 类型不能驻留在默认值中或java包下。

  • 类型及其构造函数必须public

  • 内部类的类型必须是static

  • 使用的 Java 运行时必须允许在初始化ClassLoader中声明类。Java9 和更新版本施加了一定的限制。

默认情况下, Spring 数据尝试使用生成的属性访问器,如果检测到限制,则返回到基于反射的属性访问器。

让我们来看看下面这个实体:

例 4。一个样本实体

class Person {

  private final @Id Long id;                                                (1)
  private final String firstname, lastname;                                 (2)
  private final LocalDate birthday;
  private final int age;                                                    (3)

  private String comment;                                                   (4)
  private @AccessType(Type.PROPERTY) String remarks;                        (5)

  static Person of(String firstname, String lastname, LocalDate birthday) { (6)

    return new Person(null, firstname, lastname, birthday,
      Period.between(birthday, LocalDate.now()).getYears());
  }

  Person(Long id, String firstname, String lastname, LocalDate birthday, int age) { (6)

    this.id = id;
    this.firstname = firstname;
    this.lastname = lastname;
    this.birthday = birthday;
    this.age = age;
  }

  Person withId(Long id) {                                                  (1)
    return new Person(id, this.firstname, this.lastname, this.birthday, this.age);
  }

  void setRemarks(String remarks) {                                         (5)
    this.remarks = remarks;
  }
}
1 标识符属性是最终的,但在构造函数中设置为null
该类公开了一个withId(…)方法,该方法用于设置标识符,例如,当一个实例被插入到数据存储中并生成了一个标识符时。
当创建一个新的实例时,原始的Person实例保持不变。
相同的模式通常应用于其他属性 wither 方法是可选的,因为持久性构造函数(参见 6)实际上是一个复制构造函数,并且设置该属性将被转换为创建一个新的实例,并应用新的标识符。
2 firstnamelastname属性是普通的不可变属性,可能通过 getter 公开。
3 age属性是一个不可变的属性,但它是从birthday属性派生出来的,
根据所示的设计,数据库值将超过默认值,因为 Spring 数据使用的是唯一声明的构造函数,
即使这样做的目的是为了更好地进行计算,重要的是,这个构造函数还将age作为参数(可能会忽略它),否则属性填充步骤将尝试设置 Age 字段并失败,因为它是不可变的,并且不存在with…方法。
4 comment属性 is mutable 是通过直接设置其字段来填充的。
5 remarks属性是可变的,可以通过直接设置comment字段或调用 setter 方法来填充
6 该类公开了用于创建对象的工厂方法和构造函数。
这里的核心思想是使用工厂方法而不是附加的构造函数,以避免通过@PersistenceConstructor消除构造函数歧义的需要。
相反,默认属性将在工厂方法中处理。

# 3.3.一般性建议

  • 尽量坚持使用不变的对象—不可变对象很容易创建,因为物化一个对象只需要调用它的构造函数。此外,这也避免了你的域对象中充斥着允许客户端代码操作对象状态的 setter 方法。如果你需要这些,可以选择对它们进行包保护,以便它们只能被有限数量的合用类型调用。只有建造者的物化比财产人口快多达 30%。

  • 提供一个 All-Args 构造器——即使你不能或不想将实体建模为不可变的值,提供一个构造函数仍然有价值,该构造函数将实体的所有属性作为参数,包括可变的属性,因为这允许对象映射跳过属性总体以获得最佳性能。

    • 使用工厂方法而不是重载的构造函数来避免@PersistenceConstructor*——使用最佳性能所需的全参数构造函数,我们通常希望公开更多的应用程序用例特定的构造函数,这些构造函数省略了自动生成的标识符等内容。使用静态工厂方法来公开 All-Args 构造函数的这些变体是一种已有的模式。
  • 确保遵守允许使用生成的实例器和属性访问器类的约束

    • 对于要生成的标识符,仍然使用与全参数持久性构造函数(首选)或with…方法 * 相结合的最终字段—
  • 使用 Lombok 避免样板代码—因为持久性操作通常需要一个构造函数来接受所有参数,所以它们的声明变成了对字段分配的样板参数的繁琐重复,而使用 Lombok 的@AllArgsConstructor可以最好地避免这种重复。

# 3.3.1.覆盖属性

Java 允许域类的灵活设计,其中一个子类可以定义一个属性,该属性已经在其超类中以相同的名称声明了。考虑以下示例:

public class SuperType {

   private CharSequence field;

   public SuperType(CharSequence field) {
      this.field = field;
   }

   public CharSequence getField() {
      return this.field;
   }

   public void setField(CharSequence field) {
      this.field = field;
   }
}

public class SubType extends SuperType {

   private String field;

   public SubType(String field) {
      super(field);
      this.field = field;
   }

   @Override
   public String getField() {
      return this.field;
   }

   public void setField(String field) {
      this.field = field;

      // optional
      super.setField(field);
   }
}

这两个类都使用可分配类型定义fieldSubType然而阴影SuperType.field。根据类的设计,使用构造函数可能是设置SuperType.field的唯一默认方法。或者,在 setter 中调用super.setField(…)可以在SuperType中设置field。所有这些机制在某种程度上都会产生冲突,因为这些属性共享相同的名称,但可能表示两个不同的值。 Spring 如果类型是不可分配的,则数据跳过超类型属性。也就是说,重写的属性的类型必须可分配给它的超类型属性类型,以注册为重写,否则超类型属性被认为是瞬态的。我们通常建议使用不同的属性名称。

Spring 数据模块通常支持持有不同值的重写属性。从编程模型的角度来看,有几点需要考虑:

  1. 应该持久化哪个属性(默认为所有声明的属性)?你可以通过使用@Transient注释这些属性来排除这些属性。

  2. 如何表示你的数据存储中的属性?对不同的值使用相同的字段/列名称通常会导致数据损坏,因此你应该使用显式的字段/列名称对至少一个属性进行注释。

  3. 使用@AccessType(PROPERTY)不能作为超级属性,如果不对 setter 实现做任何进一步的假设,通常不能进行设置。

# 3.4. Kotlin 支持

Spring 数据适应 Kotlin 的细节以允许对象创建和突变。

# 3.4.1. Kotlin 对象创建

Kotlin 支持实例化类,所有类在默认情况下都是不可变的,并且需要显式的属性声明来定义可变属性。考虑以下dataPerson:

data class Person(val id: String, val name: String)

上面的类使用显式构造函数编译为一个典型的类。我们可以通过添加另一个构造函数来自定义这个类,并用@PersistenceConstructor对它进行注释,以表示构造函数的首选项:

data class Person(var id: String, val name: String) {

    @PersistenceConstructor
    constructor(id: String) : this(id, "unknown")
}

Kotlin 如果没有提供参数,则允许使用默认值,从而支持参数的可选性。当 Spring 数据检测到具有参数 default 的构造函数时,如果数据存储区不提供值(或简单地返回null),则不存在这些参数,因此 Kotlin 可以应用参数 default。考虑为name应用参数 default 的以下类

data class Person(var id: String, val name: String = "unknown")

每当name参数不是结果的一部分或其值null时,则name默认为unknown

# 3.4.2. Kotlin 数据类的属性总体

在 Kotlin 中,所有类在默认情况下都是不可变的,并且需要显式的属性声明来定义可变属性。考虑以下dataPerson:

data class Person(val id: String, val name: String)

这个类实际上是不可变的。它允许创建新实例,因为 Kotlin 生成了一个copy(…)方法,该方法创建新的对象实例,从现有对象复制所有属性值,并将作为参数提供的属性值应用到该方法。

# 3.4.3. Kotlin 最重要的属性

Kotlin 允许声明属性重写 (opens new window)来改变子类中的属性。

open class SuperType(open var field: Int)

class SubType(override var field: Int = 1) :
	SuperType(field) {
}

这样的安排呈现了两个名为field的属性。 Kotlin 为每个类中的每个属性生成属性访问器(getter 和 setter)。实际上,代码如下所示:

public class SuperType {

   private int field;

   public SuperType(int field) {
      this.field = field;
   }

   public int getField() {
      return this.field;
   }

   public void setField(int field) {
      this.field = field;
   }
}

public final class SubType extends SuperType {

   private int field;

   public SubType(int field) {
      super(field);
      this.field = field;
   }

   public int getField() {
      return this.field;
   }

   public void setField(int field) {
      this.field = field;
   }
}

SubType上的 getters 和 setters 只设置SubType.field,而不是SuperType.field。在这种安排中,使用构造函数是设置SuperType.field的唯一缺省方法。将方法添加到SubType以通过this.SuperType.field = …设置SuperType.field是可能的,但不属于受支持的约定。属性重写在一定程度上造成了冲突,因为这些属性共享相同的名称,但可能表示两个不同的值。我们通常建议使用不同的属性名称。

Spring 数据模块通常支持持有不同值的重写属性。从编程模型的角度来看,有几点需要考虑:

  1. 应该持久化哪个属性(默认为所有声明的属性)?你可以通过使用@Transient注释这些属性来排除这些属性。

  2. 如何表示你的数据存储中的属性?对不同的值使用相同的字段/列名称通常会导致数据损坏,因此你应该使用显式的字段/列名称对至少一个属性进行注释。

  3. 不能使用@AccessType(PROPERTY)作为不能设置的超级属性。

# 4. 使用 Spring 数据存储库

Spring 数据存储库抽象的目标是显著减少为各种持久性存储实现数据访问层所需的样板代码的数量。

Spring Data repository documentation and your module

本章解释了 Spring 数据存储库的核心概念和接口。
本章中的信息是从 Spring 数据共享模块中提取的。
它使用了配置以及 Java 持久性 API( JPA)模块的代码示例。
你应该调整 XML 名称空间声明和要扩展的类型,使其与你所使用的特定模块的等同物。“名称空间引用”涵盖了 XML 配置,该配置在支持存储库 API 的所有 Spring 数据模块中都受到支持。“存储库查询关键字”一般涵盖了存储库抽象支持的查询方法关键字。
有关模块特定功能的详细信息,请参见本文档有关该模块的章节。

# 4.1.核心概念

Spring 数据存储库抽象中的中心接口是Repository。它把要管理的域类以及域类的 ID 类型作为类型参数。这个接口主要充当一个标记接口,用于捕获要使用的类型,并帮助你发现扩展这个类型的接口。[CrudRepository](https://DOCS. Spring.io/ Spring-data/commons/DOCS/current/api/org/springframework/data/repository/crudrepository.html)接口为所管理的实体类提供了复杂的增删改查功能。

例 5。CrudRepository接口

public interface CrudRepository<T, ID> extends Repository<T, ID> {

  <S extends T> S save(S entity);      (1)

  Optional<T> findById(ID primaryKey); (2)

  Iterable<T> findAll();               (3)

  long count();                        (4)

  void delete(T entity);               (5)

  boolean existsById(ID primaryKey);   (6)

  // … more functionality omitted.
}
1 保存给定的实体。
2 返回由给定 ID 标识的实体。
3 返回所有实体。
4 返回实体的数量。
5 删除给定的实体。
6 指示是否存在具有给定 ID 的实体。
我们还提供了特定于持久性技术的抽象,例如JpaRepositoryMongoRepository
这些接口扩展了CrudRepository,并且除了比较通用的持久性技术之外,还公开了底层持久性技术的功能,这些接口与持久性技术无关,例如CrudRepository

CrudRepository之上,有一个[PagingAndSortingRepository](https://DOCS. Spring.io/ Spring-data/commons/DOCS/current/api/org/springframework/data/repository/pagingandsortingrepository.html)抽象,它添加了额外的方法,以简化对实体的分页访问:

例 6。PagingAndSortingRepository接口

public interface PagingAndSortingRepository<T, ID> extends CrudRepository<T, ID> {

  Iterable<T> findAll(Sort sort);

  Page<T> findAll(Pageable pageable);
}

要以 20 的页面大小访问User的第二页,可以执行以下操作:

PagingAndSortingRepository<User, Long> repository = // … get access to a bean
Page<User> users = repository.findAll(PageRequest.of(1, 20));

除了查询方法之外,还可以对 Count 和 Delete 查询进行查询派生。下面的列表显示了派生的 Count 查询的接口定义:

例 7。派生计数查询

interface UserRepository extends CrudRepository<User, Long> {

  long countByLastname(String lastname);
}

下面的清单显示了派生删除查询的接口定义:

例 8。派生删除查询

interface UserRepository extends CrudRepository<User, Long> {

  long deleteByLastname(String lastname);

  List<User> removeByLastname(String lastname);
}

# 4.2.查询方法

标准增删改查功能存储库通常对底层数据存储进行查询。对于 Spring 数据,声明这些查询变成了一个四步过程:

  1. 声明一个扩展存储库或其子接口之一的接口,并将其键入它应该处理的域类和 ID 类型,如以下示例所示:

    interface PersonRepository extends Repository<Person, Long> { … }
    
  2. 在接口上声明查询方法。

    interface PersonRepository extends Repository<Person, Long> {
      List<Person> findByLastname(String lastname);
    }
    
  3. 设置 Spring 来为这些接口创建代理实例,或者使用JavaConfig,或者使用XML 配置

    1. 要使用 Java 配置,请创建一个类似于以下内容的类:

      import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
      
      @EnableJpaRepositories
      class Config { … }
      
    2. 要使用 XML 配置,请定义类似于以下内容的 Bean:

      <?xml version="1.0" encoding="UTF-8"?>
      <beans xmlns="http://www.springframework.org/schema/beans"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xmlns:jpa="http://www.springframework.org/schema/data/jpa"
         xsi:schemaLocation="http://www.springframework.org/schema/beans
           https://www.springframework.org/schema/beans/spring-beans.xsd
           http://www.springframework.org/schema/data/jpa
           https://www.springframework.org/schema/data/jpa/spring-jpa.xsd">
      
         <jpa:repositories base-package="com.acme.repositories"/>
      
      </beans>
      

      JPA 名称空间在本例中使用。如果你对任何其他存储使用存储库抽象,则需要将其更改为存储模块的适当名称空间声明。换句话说,你应该使用jpa来交换,例如,mongodb

      另外,请注意,JavaConfig 变体不会显式地配置包,因为默认情况下使用的是带注释的类的包。要定制要扫描的包,请使用数据存储特定存储库的basePackage…-注释的@Enable${store}Repositories属性之一。

  4. 注入存储库实例并使用它,如以下示例所示:

    class SomeClient {
    
      private final PersonRepository repository;
    
      SomeClient(PersonRepository repository) {
        this.repository = repository;
      }
    
      void doSomething() {
        List<Person> persons = repository.findByLastname("Matthews");
      }
    }
    

下面的小节详细解释了每个步骤:

# 4.3.定义存储库接口

要定义存储库接口,首先需要定义一个特定于域类的存储库接口。接口必须扩展Repository,并键入到域类和 ID 类型。如果希望公开该域类型的增删改查方法,请扩展CrudRepository,而不是Repository

# 4.3.1.微调存储库定义

通常,存储库接口扩展RepositoryCrudRepositoryPagingAndSortingRepository。或者,如果不想扩展 Spring 数据接口,也可以用@RepositoryDefinition注释存储库接口。扩展CrudRepository公开了一组完整的方法来操作你的实体。如果你希望对要公开的方法有所选择,那么可以将要公开的方法从CrudRepository复制到你的域存储库中。

这样做可以让你在所提供的 Spring 数据存储库功能之上定义自己的抽象。

下面的示例显示了如何选择性地公开增删改查方法(在本例中,是findByIdsave):

例 9。有选择地暴露增删改查方法

@NoRepositoryBean
interface MyBaseRepository<T, ID> extends Repository<T, ID> {

  Optional<T> findById(ID id);

  <S extends T> S save(S entity);
}

interface UserRepository extends MyBaseRepository<User, Long> {
  User findByEmailAddress(EmailAddress emailAddress);
}

在前面的示例中,你为所有域存储库定义了一个公共的基本接口,并公开了findById(…)以及save(…),这些方法被路由到你选择的存储库的基础存储库实现中,该存储库由 Spring 数据提供(例如,如果你使用 JPA,实现是SimpleJpaRepository),因为它们匹配CrudRepository中的方法签名。因此UserRepository现在可以保存用户,通过 ID 查找单个用户,并触发查询以通过电子邮件地址查找Users

中间存储库接口用@NoRepositoryBean注释。
确保将该注释添加到所有存储库接口中,其中 Spring 数据不应在运行时为其创建实例。

# 4.3.2.使用具有多个 Spring 数据模块的存储库

在应用程序中使用唯一的 Spring 数据模块使事情变得简单,因为定义的作用域中的所有存储库接口都绑定到 Spring 数据模块。有时,应用程序需要使用多个 Spring 数据模块。在这种情况下,存储库定义必须区分持久性技术。 Spring 当检测到类路径上的多个存储库工厂时,数据进入严格的存储库配置模式。严格配置使用存储库或域类的详细信息来决定 Spring 存储库定义的数据模块绑定:

  1. 如果存储库定义扩展特定于模块的存储库,则它是特定 Spring 数据模块的有效候选者。

  2. 如果域类是使用特定于模块的类型注释,则它是特定 Spring 数据模块的有效候选者。 Spring 数据模块要么接受第三方注释(例如 JPA 的@Entity),要么提供自己的注释(例如用于 Spring Data MongoDB 和 Spring Data ElasticSearch 的@Document)。

下面的示例展示了一个使用特定于模块的接口的存储库(本例中为 JPA):

例 10。使用特定于模块的接口的存储库定义

interface MyRepository extends JpaRepository<User, Long> { }

@NoRepositoryBean
interface MyBaseRepository<T, ID> extends JpaRepository<T, ID> { … }

interface UserRepository extends MyBaseRepository<User, Long> { … }

MyRepositoryUserRepository在其类型层次结构中扩展JpaRepository。它们是 Spring 数据 JPA 模块的有效候选者。

下面的示例展示了一个使用通用接口的存储库:

例 11。使用通用接口的存储库定义

interface AmbiguousRepository extends Repository<User, Long> { … }

@NoRepositoryBean
interface MyBaseRepository<T, ID> extends CrudRepository<T, ID> { … }

interface AmbiguousUserRepository extends MyBaseRepository<User, Long> { … }

AmbiguousRepositoryAmbiguousUserRepository在其类型层次结构中仅扩展RepositoryCrudRepository。虽然在使用唯一的 Spring 数据模块时这是很好的,但多个模块无法区分这些存储库应该绑定到哪个特定的 Spring 数据。

下面的示例展示了一个使用带有注释的域类的存储库:

例 12。使用带有注释的域类的存储库定义

interface PersonRepository extends Repository<Person, Long> { … }

@Entity
class Person { … }

interface UserRepository extends Repository<User, Long> { … }

@Document
class User { … }

PersonRepository引用Person,这是用 JPA @Entity注释的,所以这个存储库显然属于 Spring 数据 JPA。UserRepository引用了User,这是用 Spring Data MongoDB 的@Document注释的。

下面的糟糕示例展示了一个存储库,它使用带有混合注释的域类:

例 13。使用带有混合注释的域类的存储库定义

interface JpaPersonRepository extends Repository<Person, Long> { … }

interface MongoDBPersonRepository extends Repository<Person, Long> { … }

@Entity
@Document
class Person { … }

这个示例展示了一个同时使用 JPA 和 Spring 数据 MongoDB 注释的域类。它定义了两个存储库,JpaPersonRepositoryMongoDBPersonRepository。一个用于 JPA,另一个用于 MongoDB 的使用。 Spring 数据不再能够区分存储库,这导致未定义的行为。

存储库类型详细信息区分域类注释用于严格的存储库配置,以识别特定 Spring 数据模块的存储库候选。在同一域类型上使用多个特定于持久化技术的注释是可能的,并且允许跨多个持久化技术重用域类型。然而, Spring 这样的数据就不能再确定与存储库绑定的唯一模块。

区分存储库的最后一种方法是对存储库基包进行范围界定。基包定义了扫描存储库接口定义的起点,这意味着存储库定义位于适当的包中。默认情况下,注释驱动的配置使用配置类的包。基于 XML 的配置中的基本包是强制性的。

下面的示例展示了基本包的注释驱动配置:

例 14。注解驱动的基包配置

@EnableJpaRepositories(basePackages = "com.acme.repositories.jpa")
@EnableMongoRepositories(basePackages = "com.acme.repositories.mongo")
class Configuration { … }

# 4.4.定义查询方法

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

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

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

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

# 4.4.1.查询查找策略

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

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

  • USE_DECLARED_QUERY尝试查找已声明的查询,如果找不到异常,则抛出异常。查询可以通过某个地方的注释来定义,也可以通过其他方式进行声明。请参阅特定存储的文档,以查找该存储的可用选项。如果存储库基础结构在引导阶段没有找到该方法的已声明查询,那么它将失败。

  • CREATE_IF_NOT_FOUND(默认值)结合了CREATEUSE_DECLARED_QUERY。它首先查找已声明的查询,如果没有找到已声明的查询,则创建一个基于名称的自定义方法查询。这是默认的查找策略,因此,如果你没有显式地配置任何内容,就会使用该策略。它允许通过方法名快速定义查询,也可以根据需要引入声明的查询,从而对这些查询进行自定义优化。

# 4.4.2.查询创建

Spring 数据存储库基础设施中内置的查询生成器机制对于在存储库的实体上构建约束查询非常有用。

下面的示例展示了如何创建许多查询:

例 15。从方法名创建查询

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限制查询结果]。

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

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

  • 表达式通常是属性遍历,与可以级联的运算符结合在一起。可以将属性表达式与ANDOR合并。对于属性表达式,还可以支持诸如BetweenLessThanGreaterThanLike之类的运算符。受支持的操作符可以因数据存储而异,因此请参阅参考文档的相应部分。

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

  • 你可以通过在引用属性的查询方法中附加OrderBy子句并提供排序方向(AscDesc)来应用静态排序。要创建支持动态排序的查询方法,请参见“特殊参数处理”。

# 4.4.3.属性表达式

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

List<Person> findByAddressZipCode(ZipCode zipCode);

假设 aPerson具有AddressZipCode。在这种情况下,该方法创建x.address.zipCode属性遍历。解析算法首先将整个部分(AddressZipCode)解释为属性,然后检查域类中是否有该名称的属性(未大写)。如果算法成功,它将使用该属性。如果不是这样,则算法将右侧驼峰部分的源拆分为头部和尾部,并尝试找到相应的属性——在我们的示例中,AddressZipCode。如果算法找到了一个带有头部的属性,它就会获取尾部,并继续从那里构建树,按照刚才描述的方式将尾部分割开来。如果第一次分割不匹配,则算法将分割点向左移动(AddressZipCode)并继续。

尽管这在大多数情况下都适用,但算法可能会选择错误的属性。假设Person类也有一个addressZip属性。该算法将在第一轮分割中匹配,选择错误的属性,并失败(因为addressZip的类型可能没有code属性)。

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

List<Person> findByAddress_ZipCode(ZipCode zipCode);

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

# 4.4.4.特殊参数处理

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

例 16。在查询方法中使用PageableSliceSort

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);
获取SortPageable的 API 期望将非null值传递到方法中。
如果不想应用任何排序或分页,请使用Sort.unsorted()Pageable.unpaged()

第一个方法允许你将org.springframework.data.domain.Pageable实例传递给查询方法,以动态地将分页添加到静态定义的查询中。aPage知道可用的元素和页面的总数。它通过触发 Count 查询来计算总数量的基础设施来实现这一点。因为这可能很昂贵(取决于使用的存储空间),所以你可以返回Slice。aSlice只知道 nextSlice是否可用,当遍历更大的结果集时,这可能就足够了。

排序选项也通过Pageable实例处理。如果只需要排序,请向方法中添加org.springframework.data.domain.Sort参数。正如你所看到的,返回List也是可能的。在这种情况下,不会创建构建实际Page实例所需的附加元数据(这反过来意味着不会发出本来需要的附加计数查询)。相反,它将查询限制为仅查找给定的实体范围。

要找出整个查询有多少页,你必须触发一个额外的计数查询。
默认情况下,该查询是从你实际触发的查询派生出来的。
# 分页和排序

你可以使用属性名称来定义简单的排序表达式。你可以将表达式串联起来,以便将多个条件收集到一个表达式中。

例 17。定义排序表达式

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

要获得一种更安全的类型定义排序表达式的方法,请从要为其定义排序表达式的类型开始,并使用方法引用来定义要对其进行排序的属性。

例 18。使用类型安全的 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,那么你也可以使用生成的元模型类型来定义排序表达式:

例 19。使用 QueryDSL API 定义排序表达式

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

# 4.4.5.限制查询结果

你可以使用firsttop关键字来限制查询方法的结果,这些关键字可以互换使用。可以在topfirst中添加一个可选的数值,以指定要返回的最大结果大小。如果省略了这个数字,则假定结果大小为 1。下面的示例展示了如何限制查询大小:

例 20。用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);

对于支持不同查询的数据存储,Limiting 表达式还支持Distinct关键字。此外,对于将结果集限制为一个实例的查询,支持用Optional关键字包装结果。

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

通过使用Sort参数将结果与动态排序结合起来进行限制,这样就可以表示最小的“k”元素和最大的“k”元素的查询方法。

# 4.4.6.返回集合或迭代的存储库方法

返回多个结果的查询方法可以使用标准的 JavaIterableListSet。除此之外,我们还支持返回 Spring 数据的StreamableIterable的自定义扩展以及Vavr (opens new window)提供的集合类型。参考附录解释所有可能的查询方法返回类型

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

你可以使用Streamable作为Iterable或任何集合类型的替代。它提供了方便的方法来访问非并行的Stream(从Iterable缺少)和直接….filter(…)….map(…)上的元素并将Streamable连接到其他元素:

例 21。使用 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"));
# 返回自定义的可刷新包装器类型

为集合提供专用的包装器类型是一种常用的模式,用于为返回多个元素的查询结果提供 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 允许 API 访问产品价格的Product实体。
2 一个Streamable<Product>的包装器类型,它可以通过使用Products.of(…)(使用 Lombok 注释创建的工厂方法)来构造。
一个使用Streamable<Product>的标准构造函数也可以这样做。
3 包装器类型公开了一个额外的 API,在Streamable<Product>上计算新值。
4 实现Streamable接口并将其委托给实际结果。
5 该包装器类型Products可以直接用作返回类型的查询方法。
你不需要返回Streamable<Product>并在存储库客户端中进行查询后手动包装它。
# 对 VAVR 收藏的支持

Vavr (opens new window)是一个包含 Java 函数式编程概念的库。它附带了一组自定义的集合类型,你可以将其用作查询方法返回类型,如下表所示:

Vavr collection type 使用的 VAVR 实现类型 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(VAVRIterable等价的),然后我们从实际的返回值派生实现类。即把 ajava.util.List变成 vAVRListSeq,ajava.util.Set变成 vAVRLinkedHashSet``Set,以此类推。

# 4.4.7.存储库方法的空处理

在 Spring Data2.0 中,返回单个聚合实例的存储库增删改查方法使用 Java8 的Optional来指示潜在的值缺失。此外, Spring Data 支持在查询方法上返回以下包装器类型:

  • com.google.common.base.Optional

  • scala.Option

  • io.vavr.control.Option

或者,查询方法可以选择完全不使用包装器类型。然后通过返回null来表示没有查询结果。返回集合、集合替代方案、包装器和流的存储库方法保证永远不返回null,而是返回相应的空表示。详见“存储库查询返回类型”。

# 可否定性注释

你可以通过使用Spring Framework’s nullability annotations (opens new window)来表示存储库方法的可否定性约束。它们提供了一种工具友好的方法和 OPT-在运行时进行null检查,如下所示:

  • [@NonNullApi](https://DOCS. Spring.io/ Spring/DOCS/5.3.16/javadoc-api/org/springframework/lang/nonnullapi.html):在包级别上用于声明参数和返回值的默认行为分别是既不接受也不产生null值。

  • [@NonNull](https://DOCS. Spring.io/ Spring/DOCS/5.3.16/javadoc-api/org/springframework/lang/nonnull.html):用于参数或返回值,该参数或返回值必须不是null(对于@NonNullApi适用的参数和返回值不需要)。

  • [@Nullable](https://DOCS. Spring.io/ Spring/DOCS/5.3.16/javadoc-api/org/springframework/lang/nullable.html):用于可以是null的参数或返回值。

Spring 注释是用JSR 305 (opens new window)注释(一种休眠但广泛使用的 JSR)进行元注释的。JSR305 元注释允许工具供应商(例如IDEA (opens new window)Eclipse (opens new window)Kotlin (opens new window))以通用方式提供空安全支持,而无需对 Spring 注释进行硬编码支持。要为查询方法启用可否定性约束的运行时检查,你需要在包级别上通过在package-info.java中使用 Spring 的@NonNullApi来激活非可否定性,如以下示例所示:

例 22。在package-info.java中声明不可无效

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

一旦非空默认值到位,存储库查询方法调用将在运行时针对无效约束进行验证。如果查询结果违反了定义的约束,将引发异常。当方法返回null但被声明为 non-nullable 时,就会发生这种情况(缺省情况,在存储库所在的包上定义了注释)。如果你希望再次 OPT 到无效的结果,可以在单个方法上选择性地使用@Nullable。使用本节开头提到的结果包装器类型将继续按预期工作:将空结果转换为表示缺省的值。

下面的示例展示了刚才描述的一些技术:

例 23。使用不同的零度约束

package com.acme;                                                       (1)

import org.springframework.lang.Nullable;

interface UserRepository extends Repository<User, Long> {

  User getByEmailAddress(EmailAddress emailAddress);                    (2)

  @Nullable
  User findByEmailAddress(@Nullable EmailAddress emailAdress);          (3)

  Optional<User> findOptionalByEmailAddress(EmailAddress emailAddress); (4)
}
1 存储库驻留在我们为其定义了非空行为的包(或子包)中。
2 当查询不产生结果时抛出EmptyResultDataAccessException。当将emailAddress传递给方法的null时抛出IllegalArgumentException
3 当查询不产生结果时,返回null
还接受null作为emailAddress的值。
4 当查询不产生结果时返回Optional.empty()。当将emailAddress传递给方法的null时,抛出一个IllegalArgumentException
# 基于 Kotlin 的存储库中的可否定性

Kotlin 已将可否定性约束 (opens new window)的定义烘焙到语言中。 Kotlin 代码编译成字节码,其不通过方法签名而是通过编译元数据来表示可否定性约束。确保在你的项目中包含kotlin-reflect jar,以便能够对 Kotlin 的无效约束进行内省。 Spring 数据存储库使用语言机制来定义那些约束,以应用相同的运行时检查,如下所示:

例 24。在 Kotlin 存储库上使用可否定性约束

interface UserRepository : Repository<User, String> {

  fun findByUsername(username: String): User     (1)

  fun findByFirstname(firstname: String?): User? (2)
}
1 该方法将参数和结果都定义为不可空( Kotlin 默认值)。
Kotlin 编译器拒绝将null传递给该方法的方法调用。
如果查询产生一个空结果,则抛出一个EmptyResultDataAccessException
2 对于firstname参数,此方法接受null,如果查询不产生结果,则返回null

# 4.4.8.流式查询结果

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

例 25。用 java8Stream<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()方法或通过使用 Java7try-with-resources块手动关闭Stream,如以下示例所示:

例 26。与Stream<T>一起工作会导致try-with-resources

try (Stream<User> stream = repository.findAllByCustomQueryAndStream()) {
  stream.forEach(…);
}
并非所有 Spring 数据模块当前都支持Stream<T>作为返回类型。

# 4.4.9.异步查询结果

你可以使用Spring’s asynchronous method running capability (opens new window)异步运行存储库查询。这意味着当实际查询发生在已提交给 Spring TaskExecutor的任务中时,方法在调用后立即返回。异步查询与反应式查询不同,不应混用。有关反应性支持的更多详细信息,请参见特定于商店的文档。下面的示例显示了一些异步查询:

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

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

@Async
ListenableFuture<User> findOneByLastname(String lastname);    (3)
1 使用java.util.concurrent.Future作为返回类型。
2 使用 Java8java.util.concurrent.CompletableFuture作为返回类型。
3 使用org.springframework.util.concurrent.ListenableFuture作为返回类型。

# 4.5.创建存储库实例

本节介绍如何为已定义的存储库接口创建实例和 Bean 定义。这样做的一种方法是使用每个支持存储库机制的 Spring 数据模块附带的 Spring 名称空间,尽管我们通常建议使用 Java 配置。

# 4.5.1.XML 配置

Spring 每个数据模块包括一个repositories元素,该元素允许你定义一个基包,由 Spring 为你扫描,如以下示例所示:

例 27。通过 XML 启用 Spring 数据存储库

<?xml version="1.0" encoding="UTF-8"?>
<beans:beans xmlns:beans="http://www.springframework.org/schema/beans"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xmlns="http://www.springframework.org/schema/data/jpa"
  xsi:schemaLocation="http://www.springframework.org/schema/beans
    https://www.springframework.org/schema/beans/spring-beans.xsd
    http://www.springframework.org/schema/data/jpa
    https://www.springframework.org/schema/data/jpa/spring-jpa.xsd">

  <repositories base-package="com.acme.repositories" />

</beans:beans>

在前面的示例中, Spring 被指示扫描及其所有子包以用于扩展的接口或其一个子接口。对于找到的每个接口,基础结构注册了与持久性技术相关的FactoryBean,以创建处理查询方法调用的适当代理。每个 Bean 都注册在一个 Bean 名称下,该名称来自接口名称,因此UserRepository的接口将注册在userRepository下。 Bean 嵌套式存储库接口的名称以其封闭类型名称作为前缀。base-package属性允许通配符,这样你就可以定义扫描包的模式。

# 使用过滤器

默认情况下,基础设施会获取扩展位于配置的基包下的特定于持久性技术的Repository子接口的每个接口,并为其创建一个 Bean 实例。然而,你可能希望对哪些接口为它们创建了 Bean 实例进行更细粒度的控制。为此,在<repositories />元素中使用<include-filter /><exclude-filter />元素。语义与 Spring 上下文名称空间中的元素完全等价。有关这些元素的详细信息,请参见Spring reference documentation (opens new window)

例如,为了将某些接口从作为存储库 bean 的实例化中排除,你可以使用以下配置:

例 28。使用排除过滤器元件

<repositories base-package="com.acme.repositories">
  <context:exclude-filter type="regex" expression=".*SomeRepository" />
</repositories>

前面的示例排除了所有以SomeRepository结尾的接口的实例化。

# 4.5.2.Java 配置

你还可以通过在 Java 配置类上使用特定于存储的@Enable${store}Repositories注释来触发存储库基础设施。有关 Spring 容器的基于 Java 的配置的介绍,请参见JavaConfig in the Spring reference documentation (opens new window)

启用 Spring 数据存储库的示例配置类似于以下内容:

例 29。基于注释的存储库配置示例

@Configuration
@EnableJpaRepositories("com.acme.repositories")
class ApplicationConfiguration {

  @Bean
  EntityManagerFactory entityManagerFactory() {
    // …
  }
}
前面的示例使用特定于 JPA 的注释,你将根据实际使用的存储模块对其进行更改。这同样适用于EntityManagerFactory Bean 的定义。请参阅涵盖特定于商店的配置的部分。

# 4.5.3.独立使用

你还可以在 Spring 容器之外使用存储库基础设施——例如,在 CDI 环境中。在你的 Classpath 中仍然需要一些 Spring 库,但是,通常情况下,你也可以通过编程的方式设置存储库。 Spring 提供存储库支持的数据模块附带你可以使用的持久性技术特有的RepositoryFactory,如下所示:

例 30。仓库工厂的独立使用

RepositoryFactorySupport factory = … // Instantiate factory here
UserRepository repository = factory.getRepository(UserRepository.class);

# 4.6. Spring 数据存储库的自定义实现

Spring 数据提供了各种选项,以用很少的编码来创建查询方法。但是当这些选项不适合你的需求时,你也可以为存储库方法提供自己的定制实现。这一节描述了如何做到这一点。

# 4.6.1.自定义各个存储库

要用自定义功能丰富存储库,你必须首先为自定义功能定义一个片段接口和一个实现,如下所示:

例 31。自定义存储库功能的接口

interface CustomizedUserRepository {
  void someCustomMethod(User user);
}

例 32。自定义存储库功能的实现

class CustomizedUserRepositoryImpl implements CustomizedUserRepository {

  public void someCustomMethod(User user) {
    // Your custom implementation
  }
}
与片段接口对应的类名中最重要的部分是Impl后缀。

该实现本身不依赖于 Spring 数据并且可以是常规的 Spring Bean。因此,可以使用标准的依赖注入行为来注入对其他 bean 的引用(例如)、参与方面,等等。

然后,你可以让你的存储库接口扩展片段接口,如下所示:

例 33。对存储库接口的更改

interface UserRepository extends CrudRepository<User, Long>, CustomizedUserRepository {

  // Declare query methods here
}

使用存储库接口扩展片段接口结合了增删改查和自定义功能,并使其对客户可用。

Spring 数据存储库是通过使用形成存储库组合的片段来实现的。片段是基本存储库、功能方面(如QueryDsl)和自定义接口及其实现。每次向存储库接口添加一个接口时,都会通过添加一个片段来增强组合。基础存储库和存储库方面的实现是由每个 Spring 数据模块提供的。

下面的示例展示了自定义接口及其实现:

例 34。片段及其实现

interface HumanRepository {
  void someHumanMethod(User user);
}

class HumanRepositoryImpl implements HumanRepository {

  public void someHumanMethod(User user) {
    // Your custom implementation
  }
}

interface ContactRepository {

  void someContactMethod(User user);

  User anotherContactMethod(User user);
}

class ContactRepositoryImpl implements ContactRepository {

  public void someContactMethod(User user) {
    // Your custom implementation
  }

  public User anotherContactMethod(User user) {
    // Your custom implementation
  }
}

下面的示例展示了扩展CrudRepository的定制存储库的接口:

例 35。对存储库接口的更改

interface UserRepository extends CrudRepository<User, Long>, HumanRepository, ContactRepository {

  // Declare query methods here
}

存储库可以由多个自定义实现组成,这些实现是按照其声明的顺序导入的。自定义实现比基本实现和存储库方面具有更高的优先级。这种排序使你可以重写基本存储库和方面方法,并且如果两个片段提供相同的方法签名,则可以解决歧义。存储库片段不限于在单个存储库接口中使用。多个存储库可能使用一个片段接口,允许你在不同的存储库之间重用定制。

下面的示例展示了一个存储库片段及其实现:

例 36。覆盖save(…)的片段

interface CustomizedSave<T> {
  <S extends T> S save(S entity);
}

class CustomizedSaveImpl<T> implements CustomizedSave<T> {

  public <S extends T> S save(S entity) {
    // Your custom implementation
  }
}

下面的示例展示了一个使用前面的存储库片段的存储库:

例 37。定制的存储库接口

interface UserRepository extends CrudRepository<User, Long>, CustomizedSave<User> {
}

interface PersonRepository extends CrudRepository<Person, Long>, CustomizedSave<Person> {
}
# 配置

如果你使用名称空间配置,存储库基础设施将通过扫描其发现存储库的包下面的类,尝试自动检测自定义实现片段。这些类需要遵循将命名空间元素的repository-impl-postfix属性追加到片段接口名称的命名惯例。此后缀缺省为Impl。下面的示例展示了一个使用默认后缀的存储库和一个为后缀设置自定义值的存储库:

例 38。配置示例

<repositories base-package="com.acme.repository" />

<repositories base-package="com.acme.repository" repository-impl-postfix="MyPostfix" />

前面示例中的第一个配置试图查找一个名为com.acme.repository.CustomizedUserRepositoryImpl的类,以充当自定义存储库实现。第二个示例尝试查找com.acme.repository.CustomizedUserRepositoryMyPostfix

# 歧义的解决

如果在不同的包中发现了具有匹配的类名的多个实现,则 Spring 数据使用 Bean 名称来标识要使用哪个。

给出了下面两个用于CustomizedUserRepository的自定义实现,使用了第一个实现。 Bean 它的名称是customizedUserRepositoryImpl,它与片段接口的名称(CustomizedUserRepository)加上后缀Impl相匹配。

例 39。解决模棱两可的实现

package com.acme.impl.one;

class CustomizedUserRepositoryImpl implements CustomizedUserRepository {

  // Your custom implementation
}
package com.acme.impl.two;

@Component("specialCustomImpl")
class CustomizedUserRepositoryImpl implements CustomizedUserRepository {

  // Your custom implementation
}

如果用@Component("specialCustom")注释UserRepository接口,则 Bean 名称加上Impl,然后与com.acme.impl.two中为存储库实现定义的名称匹配,并使用它来代替第一个。

# 手动接线

Spring Bean Bean 如果你的自定义实现仅使用基于注释的配置和自动布线,则前面所示的方法工作得很好,因为它被视为任何其他方法。如果你的实现片段 Bean 需要特殊的接线,则可以声明 Bean 并根据前一节中描述的约定对其进行命名。然后,基础设施通过名称引用手动定义的 Bean 定义,而不是创建本身。下面的示例展示了如何手动连接自定义实现:

例 40。自定义实现的手动接线

<repositories base-package="com.acme.repository" />

<beans:bean id="userRepositoryImpl" class="…">
  <!-- further configuration -->
</beans:bean>

# 4.6.2.自定义基本存储库

前一节中描述的方法需要定制每个存储库接口,当你希望定制基本存储库行为时,所有存储库都会受到影响。为了改变所有存储库的行为,你可以创建一个实现来扩展特定于持久性技术的存储库基类。然后,这个类充当存储库代理的自定义基类,如下例所示:

例 41。自定义存储库基类

class MyRepositoryImpl<T, ID>
  extends SimpleJpaRepository<T, ID> {

  private final EntityManager entityManager;

  MyRepositoryImpl(JpaEntityInformation entityInformation,
                          EntityManager entityManager) {
    super(entityInformation, entityManager);

    // Keep the EntityManager around to used from the newly introduced methods.
    this.entityManager = entityManager;
  }

  @Transactional
  public <S extends T> S save(S entity) {
    // implementation goes here
  }
}
该类需要具有存储特定的存储库工厂实现所使用的超类的构造函数。
如果存储库基类具有多个构造函数,则重写一个EntityInformation加上一个存储特定的基础设施对象(例如EntityManager或模板类)。

最后一步是使 Spring 数据基础设施了解定制的存储库基类。在 Java 配置中,你可以通过使用@Enable${store}Repositories注释的repositoryBaseClass属性来实现这一点,如下例所示:

例 42。使用 JavaConfig 配置自定义存储库基类

@Configuration
@EnableJpaRepositories(repositoryBaseClass = MyRepositoryImpl.class)
class ApplicationConfiguration { … }

相应的属性在 XML 命名空间中可用,如以下示例所示:

例 43。使用 XML 配置自定义存储库基类

<repositories base-package="com.acme.repository"
     base-class="….MyRepositoryImpl" />

# 4.7.从聚合根发布事件

由存储库管理的实体是聚合根。在域驱动的设计应用程序中,这些聚合根通常发布域事件。 Spring 数据提供了一种名为@DomainEvents的注释,你可以在你的聚合根的方法上使用该注释,以使该发布尽可能简单,如以下示例所示:

例 44。从聚合根公开域事件

class AnAggregateRoot {

    @DomainEvents (1)
    Collection<Object> domainEvents() {
        // … return events you want to get published here
    }

    @AfterDomainEventPublication (2)
    void callbackMethod() {
       // … potentially clean up domain events list
    }
}
1 使用@DomainEvents的方法可以返回单个事件实例或事件集合。
它不能接受任何参数。
2 在所有事件都已发布之后,我们有一个用@AfterDomainEventPublication注释的方法。
你可以使用它来清除要发布的事件列表(以及其他用途)。

每次调用 Spring 数据存储库的save(…)saveAll(…)delete(…)deleteAll(…)方法之一时,都会调用这些方法。

# 4.8. Spring 数据扩展

本节记录了一组 Spring 数据扩展,这些扩展允许 Spring 在各种上下文中使用数据。目前,大多数的集成是针对 Spring MVC 的。

# 4.8.1.QueryDSL 扩展

Querydsl (opens new window)是一个框架,该框架通过其 Fluent API 支持构建静态类型的类 SQL 查询。

Spring 若干数据模块通过QuerydslPredicateExecutor提供与 QueryDSL 的集成,如下例所示:

例 45。QueryDSLPredicateExecutor 接口

public interface QuerydslPredicateExecutor<T> {

  Optional<T> findById(Predicate predicate);  (1)

  Iterable<T> findAll(Predicate predicate);   (2)

  long count(Predicate predicate);            (3)

  boolean exists(Predicate predicate);        (4)

  // … more functionality omitted.
}
1 查找并返回与Predicate匹配的单个实体。
2 查找并返回所有匹配Predicate的实体。
3 返回匹配Predicate的实体的数量。
4 返回是否存在与Predicate匹配的实体。

要使用 QueryDSL 支持,请在存储库接口上扩展QuerydslPredicateExecutor,如下例所示:

例 46。库上的 QueryDSL 集成

interface UserRepository extends CrudRepository<User, Long>, QuerydslPredicateExecutor<User> {
}

前面的示例允许你通过使用 QueryDSLPredicate实例来编写类型安全查询,如下例所示:

Predicate predicate = user.firstname.equalsIgnoreCase("dave")
	.and(user.lastname.startsWithIgnoreCase("mathews"));

userRepository.findAll(predicate);

# 4.8.2.网络支持

Spring 支持存储库的数据模块编程模型提供了各种 Web 支持。与 Web 相关的组件需要 Spring MVC JAR 在 Classpath 上。其中一些甚至提供了Spring HATEOAS (opens new window)的集成。通常,通过在 JavaConfig 配置类中使用@EnableSpringDataWebSupport注释来启用集成支持,如下例所示:

例 47。启用 Spring 数据 Web 支持

@Configuration
@EnableWebMvc
@EnableSpringDataWebSupport
class WebConfiguration {}

@EnableSpringDataWebSupport注释注册了一些组件。我们将在本节稍后讨论这些问题。它还在 Classpath 上检测 Spring 仇恨,并为其注册集成组件(如果存在)。

或者,如果使用 XML 配置,则将SpringDataWebConfigurationHateoasAwareSpringDataWebConfiguration注册为 Spring bean,如下例所示(对于SpringDataWebConfiguration):

例 48。启用 Spring XML 中的数据 Web 支持

<bean class="org.springframework.data.web.config.SpringDataWebConfiguration" />

<!-- If you use Spring HATEOAS, register this one *instead* of the former -->
<bean class="org.springframework.data.web.config.HateoasAwareSpringDataWebConfiguration" />
# 基本网络支持

上一节中显示的配置记录了一些基本组件:

  • [使用DomainClassConverter类](#core.web.basic.domain-class-converter)让 Spring MVC 从请求参数或路径变量解析存储库管理的域类实例。

  • [HandlerMethodArgumentResolver](#core.web.basic.paging-and-sorting)实现让 Spring MVC 从请求参数解析PageableSort实例。

  • Jackson 模块以反-/序列化PointDistance之类的类型,或者存储特定的类型,这取决于所使用的 Spring 数据模块。

# 使用DomainClassConverter

DomainClassConverter类允许你在 Spring MVC 控制器方法签名中直接使用域类型,这样你就不需要通过存储库手动查找实例,如下例所示:

例 49。 Spring 在方法签名中使用域类型的 MVC 控制器

@Controller
@RequestMapping("/users")
class UserController {

  @RequestMapping("/{id}")
  String showUserForm(@PathVariable("id") User user, Model model) {

    model.addAttribute("user", user);
    return "userForm";
  }
}

该方法直接接收User实例,无需进一步查找。可以通过让 Spring MVC 首先将路径变量转换为域类的id类型来解析实例,并最终通过在为域类型注册的存储库实例上调用findById(…)来访问实例。

目前,存储库必须实现CrudRepository才有资格被发现以进行转换。
# 用于分页和排序的 HandlerMethodargumentResolver

上一节中显示的配置片段还注册了PageableHandlerMethodArgumentResolver以及SortHandlerMethodArgumentResolver的实例。注册允许PageableSort作为有效的控制器方法参数,如下例所示:

例 50。使用 Pageable 作为控制器方法参数

@Controller
@RequestMapping("/users")
class UserController {

  private final UserRepository repository;

  UserController(UserRepository repository) {
    this.repository = repository;
  }

  @RequestMapping
  String showUsers(Model model, Pageable pageable) {

    model.addAttribute("users", repository.findAll(pageable));
    return "users";
  }
}

前面的方法签名会导致 Spring MVC 尝试通过使用以下默认配置从请求参数派生Pageable实例:

page 要检索的页面。索引为 0,默认值为 0。
size 要检索的页面大小。默认值为 20。
sort Properties that should be sorted by in the format property,property(,ASC|desc). The default sort direction is case-sensitive ascending. Use multiple sort parameters if you want to switch direction or case sensitivity — for example, ?sort=firstname&sort=lastname,ASC&sort=city,ignorecase

要定制此行为,请注册一个 Bean,该 Bean 分别实现PageableHandlerMethodArgumentResolverCustomizer接口或SortHandlerMethodArgumentResolverCustomizer接口。调用它的customize()方法,允许你更改设置,如下例所示:

@Bean SortHandlerMethodArgumentResolverCustomizer sortCustomizer() {
    return s -> s.setPropertyDelimiter("<-->");
}

如果设置现有MethodArgumentResolver的属性对于你的目的来说是不够的,那么扩展SpringDataWebConfiguration或启用 Hateoas 的等效方法,覆盖pageableResolver()sortResolver()方法,并导入自定义的配置文件,而不是使用@Enable注释。

如果需要从请求中解析多个PageableSort实例(例如,对于多个表),则可以使用 Spring 的@Qualifier注释来区分一个实例和另一个实例。然后,请求参数必须以${qualifier}_作为前缀。下面的示例展示了生成的方法签名:

String showUsers(Model model,
      @Qualifier("thing1") Pageable first,
      @Qualifier("thing2") Pageable second) { … }

你必须填充thing1_pagething2_page,以此类推。

传递到该方法的默认Pageable相当于PageRequest.of(0, 20),但是你可以使用@PageableDefault参数上的@PageableDefault注释来定制它。

# 对页面的超媒体支持

Spring Hateoas 附带一个表示模型类(PagedResources),该表示模型类允许使用必要的Page元数据以及链接来丰富Page实例的内容,从而使客户端能够轻松地在页面中导航。将Page转换为PagedResources是通过 Spring HateoasResourceAssembler接口的实现完成的,该接口称为PagedResourcesAssembler。下面的示例展示了如何使用PagedResourcesAssembler作为控制器方法参数:

例 51。使用 PagedResourcesAssembler 作为控制器方法参数

@Controller
class PersonController {

  @Autowired PersonRepository repository;

  @RequestMapping(value = "/persons", method = RequestMethod.GET)
  HttpEntity<PagedResources<Person>> persons(Pageable pageable,
    PagedResourcesAssembler assembler) {

    Page<Person> persons = repository.findAll(pageable);
    return new ResponseEntity<>(assembler.toResources(persons), HttpStatus.OK);
  }
}

启用配置,如前面的示例所示,让PagedResourcesAssembler用作控制器方法参数。在它上调用toResources(…)具有以下效果:

  • Page的内容成为PagedResources实例的内容。

  • PagedResources对象获得一个PageMetadata实例,并且它被填充了来自Page和底层PageRequest的信息。

  • 根据页面的状态,PagedResources可能会附加prevnext链接。链接指向该方法映射到的 URI。添加到该方法中的分页参数与PageableHandlerMethodArgumentResolver的设置匹配,以确保以后可以解析链接。

假设我们在数据库中有 30 个Person实例。你现在可以触发一个请求(GET [http://localhost:8080/persons](http://localhost:8080/persons)),并看到类似于以下内容的输出:

{ "links" : [ { "rel" : "next",
                "href" : "http://localhost:8080/persons?page=1&size=20" }
  ],
  "content" : [
     … // 20 Person instances rendered here
  ],
  "pageMetadata" : {
    "size" : 20,
    "totalElements" : 30,
    "totalPages" : 2,
    "number" : 0
  }
}

汇编程序生成了正确的 URI,并且还选择了默认配置,以便为即将到来的请求将参数解析为Pageable。这意味着,如果你更改了该配置,那么链接将自动遵守更改。默认情况下,汇编器指向调用它的控制器方法,但是你可以通过传递一个自定义的Link作为构建分页链接的基础来定制该方法,这将重载PagedResourcesAssembler.toResource(…)方法。

# Spring 数据 Jackson 模块

核心模块和一些特定的存储模块附带一组用于类型的 Jackson 模块,如org.springframework.data.geo.Distanceorg.springframework.data.geo.Point,由 Spring 数据域使用。一旦启用网络支持并且com.fasterxml.jackson.databind.ObjectMapper可用,就会导入这些模块。

在初始化SpringDataJacksonModules期间,就像SpringDataJacksonConfiguration一样,被基础结构拾取,这样声明的com.fasterxml.jackson.databind.Modules 对 JacksonObjectMapper是可用的。

公共基础设施注册了用于下列域类型的数据绑定 mixin。

org.springframework.data.geo.Distance
org.springframework.data.geo.Point
org.springframework.data.geo.Box
org.springframework.data.geo.Circle
org.springframework.data.geo.Polygon
个别模块可以提供额外的SpringDataJacksonModules
有关更多详细信息,请参阅商店特定部分。
# 网络数据库支持

可以使用 Spring 数据投影(在预测中描述)通过使用JSONPath (opens new window)表达式(需要Jayway Jsonpath (opens new window)XPath (opens new window)表达式(需要XmlBeam (opens new window))来绑定传入的请求有效负载,如下例所示:

例 52。使用 JSONPath 或 XPath 表达式的 HTTP 有效负载绑定

@ProjectedPayload
public interface UserPayload {

  @XBRead("//firstname")
  @JsonPath("$..firstname")
  String getFirstname();

  @XBRead("/lastname")
  @JsonPath({ "$.lastname", "$.user.lastname" })
  String getLastname();
}

你可以使用前面示例中所示的类型作为 Spring MVC 处理程序方法参数,或者在RestTemplate的方法之一上使用ParameterizedTypeReference。前面的方法声明将尝试在给定的文档中的任何地方找到firstnamelastnameXML 查找是在传入文档的顶层执行的。它的 JSON 变体首先尝试一个顶级的lastname,但如果前者不返回一个值,则还会尝试在lastname子文档中嵌套的user。这样,在不需要客户端调用公开的方法(通常是基于类的有效负载绑定的一个缺点)的情况下,可以轻松地减轻源文档结构中的更改。

支持预测中所述的嵌套投影。如果方法返回复杂的非接口类型,则使用 JacksonObjectMapper映射最终值。

对于 Spring MVC,一旦处于活动状态并且所需的依赖关系在 Classpath 上可用,则所需的转换器被自动注册。要使用RestTemplate,请手动注册一个ProjectingJackson2HttpMessageConverterXmlBeamHttpMessageConverter

有关更多信息,请参见规范Spring Data Examples repository (opens new window)中的Web 投影示例 (opens new window)

# QueryDSL Web 支持

对于那些具有QueryDSL (opens new window)集成的存储,你可以从Request查询字符串中包含的属性派生查询。

考虑以下查询字符串:

?firstname=Dave&lastname=Matthews

给定来自前面示例的User对象,你可以使用QuerydslPredicateArgumentResolver将查询字符串解析为下列值,如下所示:

QUser.user.firstname.eq("Dave").and(QUser.user.lastname.eq("Matthews"))
当在 Classpath 上找到 QueryDSL 时,该功能将与@EnableSpringDataWebSupport一起自动启用。

@QuerydslPredicate添加到方法签名中,将提供一个现成的Predicate,你可以使用QuerydslPredicateExecutor来运行它。

类型信息通常是从方法的返回类型解析的。
由于该信息不一定与域类型匹配,因此使用rootQuerydslPredicate属性可能是个好主意。

下面的示例展示了如何在方法签名中使用@QuerydslPredicate:

@Controller
class UserController {

  @Autowired UserRepository repository;

  @RequestMapping(value = "/", method = RequestMethod.GET)
  String index(Model model, @QuerydslPredicate(root = User.class) Predicate predicate,    (1)
          Pageable pageable, @RequestParam MultiValueMap<String, String> parameters) {

    model.addAttribute("users", repository.findAll(predicate, pageable));

    return "index";
  }
}
1 将查询字符串参数解析为匹配PredicateUser

默认绑定如下:

  • Object在简单属性上为eq

  • Object在集合 like 属性上为contains

  • 在简单属性上Collectionin

你可以通过@QuerydslPredicatebindings属性或通过使用 Java8default methods并将QuerydslBinderCustomizer方法添加到存储库接口来定制这些绑定,如下所示:

interface UserRepository extends CrudRepository<User, String>,
                                 QuerydslPredicateExecutor<User>,                (1)
                                 QuerydslBinderCustomizer<QUser> {               (2)

  @Override
  default void customize(QuerydslBindings bindings, QUser user) {

    bindings.bind(user.username).first((path, value) -> path.contains(value))    (3)
    bindings.bind(String.class)
      .first((StringPath path, String value) -> path.containsIgnoreCase(value)); (4)
    bindings.excluding(user.password);                                           (5)
  }
}
1 QuerydslPredicateExecutor提供对Predicate的特定查找方法的访问。
2 在存储库接口上定义的QuerydslBinderCustomizer将自动拾取并执行快捷方式@QuerydslPredicate(bindings=…​)
3 username属性的绑定定义为简单的contains绑定。
4 String属性的默认绑定定义为不区分大小写的contains匹配。
5 password属性从Predicate分辨率中排除。
在从存储库应用特定绑定之前,可以注册一个持有默认 QueryDSL 绑定的QuerydslBinderCustomizerDefaults Bean 或@QuerydslPredicate

# 4.8.3.存储库填充器

如果你使用 Spring JDBC 模块,你可能熟悉使用 SQL 脚本填充DataSource的支持。类似的抽象可以在存储库级别上使用,尽管它不使用 SQL 作为数据定义语言,因为它必须与存储无关。因此,populators 支持 XML(通过 Spring 的 OXM 抽象)和 JSON(通过 Jackson)来定义用于填充存储库的数据。

假设你有一个名为data.json的文件,其内容如下:

例 53。在 JSON 中定义的数据

[ { "_class" : "com.acme.Person",
 "firstname" : "Dave",
  "lastname" : "Matthews" },
  { "_class" : "com.acme.Person",
 "firstname" : "Carter",
  "lastname" : "Beauford" } ]

你可以通过使用 Spring Data Commons 中提供的存储库名称空间的 populator 元素来填充存储库。要将前面的数据填充到你的PersonRepository中,请声明一个类似于以下内容的填充器:

例 54。声明 Jackson 存储库填充程序

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xmlns:repository="http://www.springframework.org/schema/data/repository"
  xsi:schemaLocation="http://www.springframework.org/schema/beans
    https://www.springframework.org/schema/beans/spring-beans.xsd
    http://www.springframework.org/schema/data/repository
    https://www.springframework.org/schema/data/repository/spring-repository.xsd">

  <repository:jackson2-populator locations="classpath:data.json" />

</beans>

前面的声明导致data.json文件被 JacksonObjectMapper读取和反序列化。

通过检查 JSON 文档的_class属性来确定解组 JSON 对象的类型。基础结构最终选择适当的存储库来处理反序列化的对象。

要使用 XML 来定义存储库应该使用的数据,可以使用unmarshaller-populator元素。你可以将其配置为使用 Spring OXM 中可用的 XML 编组器选项之一。有关详细信息,请参见Spring reference documentation (opens new window)。下面的示例展示了如何使用 JAXB 取消对存储库填充程序的约束:

例 55。声明一个解组存储库填充程序(使用 JAXB)

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xmlns:repository="http://www.springframework.org/schema/data/repository"
  xmlns:oxm="http://www.springframework.org/schema/oxm"
  xsi:schemaLocation="http://www.springframework.org/schema/beans
    https://www.springframework.org/schema/beans/spring-beans.xsd
    http://www.springframework.org/schema/data/repository
    https://www.springframework.org/schema/data/repository/spring-repository.xsd
    http://www.springframework.org/schema/oxm
    https://www.springframework.org/schema/oxm/spring-oxm.xsd">

  <repository:unmarshaller-populator locations="classpath:data.json"
    unmarshaller-ref="unmarshaller" />

  <oxm:jaxb2-marshaller contextPath="com.acme" />

</beans>

# 5. 预测

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

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

例 56。样本集合和存储库

class Person {

  @Id UUID id;
  String firstname, lastname;
  Address address;

  static class Address {
    String zipCode, city, street;
  }
}

interface PersonRepository extends Repository<Person, UUID> {

  Collection<Person> findByLastname(String lastname);
}

现在想象一下,我们只想检索这个人的姓名属性。 Spring 数据提供了什么手段来实现这一点?本章的其余部分回答了这个问题。

# 5.1.基于界面的投影

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

例 57。检索属性子集的投影接口

interface NamesOnly {

  String getFirstname();
  String getLastname();
}

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

例 58。使用基于接口的投影和查询方法的存储库

interface PersonRepository extends Repository<Person, UUID> {

  Collection<NamesOnly> findByLastname(String lastname);
}

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

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

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

例 59。检索属性子集的投影接口

interface PersonSummary {

  String getFirstname();
  String getLastname();
  AddressSummary getAddress();

  interface AddressSummary {
    String getCity();
  }
}

在方法调用中,将获得目标实例的address属性,并依次将其包装到一个投影代理中。

# 5.1.1.闭合投影

一个投影接口,其访问器的方法是所有匹配的属性的目标聚集被认为是一个封闭的投影。下面的示例(我们在本章前面也使用了它)是一个封闭投影:

例 60。闭合投影

interface NamesOnly {

  String getFirstname();
  String getLastname();
}

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

# 5.1.2.开放投影

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

例 61。一个开放的投影

interface NamesOnly {

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

支持该投影的聚合根在target变量中可用。使用@Value的投影接口是一个开放的投影。 Spring 在这种情况下,数据不能应用查询执行优化,因为 SPEL 表达式可以使用聚合根的任何属性。

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

例 62。使用自定义逻辑的默认方法的投影接口

interface NamesOnly {

  String getFirstname();
  String getLastname();

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

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

例 63。样本人物对象

@Component
class MyBean {

  String getFullName(Person person) {
    …
  }
}

interface NamesOnly {

  @Value("#{@myBean.getFullName(target)}")
  String getFullName();
  …
}

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

例 64。样本人物对象

interface NamesOnly {

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

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

# 5.1.3.可空包装纸

投影接口中的吸取器可以使用可空包装器来提高零安全性。当前支持的包装器类型如下:

  • java.util.Optional

  • com.google.common.base.Optional

  • scala.Option

  • io.vavr.control.Option

例 65。一种使用可空包装器的投影接口

interface NamesOnly {

  Optional<String> getFirstname();
}

如果底层的投影值不是null,那么值将使用包装器类型的当前表示返回。如果支持值是null,那么 getter 方法返回所使用的包装器类型的空表示。

# 5.2.基于类别的投影(DTO)

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

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

下面的示例显示了一个投影 DTO:

例 66。投射 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 的样板代码

使用龙目岛计划 (opens new window)可以极大地简化 DTO 的代码,它提供了@Value注释(不要与 Spring 的@Value前面的接口示例中显示的注释混淆)。
如果使用 Project Lombok 的@Value注释,前面显示的示例 DTO 将变成如下:

缺省情况下,该类公开了一个构造函数,该构造函数接受所有字段并自动实现equals(…)hashCode()方法。

# 5.3.动态投影

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

例 67。使用动态投影参数的存储库

interface PersonRepository extends Repository<Person, UUID> {

  <T> Collection<T> findByLastname(String lastname, Class<T> type);
}

通过这种方式,可以使用该方法获得所述聚集如当前所应用的或与所应用的投影,如以下示例所示:

例 68。使用具有动态投影的存储库

void someMethod(PersonRepository people) {

  Collection<Person> aggregates =
    people.findByLastname("Matthews", Person.class);

  Collection<NamesOnly> aggregates =
    people.findByLastname("Matthews", NamesOnly.class);
}
如果查询的实际返回类型等于Class参数的泛型参数类型,则检查类型
的查询参数是否符合动态投影参数的条件,然后匹配的Class参数在查询或 SPEL 表达式中不可用。
如果你想使用Class参数作为查询参数,那么请确保使用不同的泛型参数,例如Class<?>

# 6. 示例查询

# 6.1.导言

本章通过示例介绍了查询,并解释了如何使用它。

示例查询是一种用户友好的查询技术,具有简单的界面。它允许动态查询创建,并且不需要你编写包含字段名的查询。实际上,通过示例查询根本不需要你通过使用特定于商店的查询语言来编写查询。

# 6.2.用法

通过示例 API 进行的查询由三部分组成:

  • Probe:包含填充字段的域对象的实际示例。

  • ExampleMatcher:ExampleMatcher包含有关如何匹配特定字段的详细信息。它可以在多个示例中重用。

  • Example:一个Example由探针和ExampleMatcher组成。它用于创建查询。

示例查询非常适合于以下几种用例:

  • 使用一组静态或动态约束来查询你的数据存储。

  • 经常重构域对象,而不必担心破坏现有的查询。

  • 独立于底层数据存储 API 工作。

示例查询也有几个限制:

  • 不支持嵌套或分组的属性约束,例如firstname = ?0 or (firstname = ?1 and lastname = ?2)

  • 只支持字符串的开始/包含/结束/正则表达式匹配,以及其他属性类型的精确匹配。

在开始使用示例查询之前,你需要有一个域对象。要开始,请为存储库创建一个接口,如以下示例所示:

例 69。样本人物对象

public class Person {

  @Id
  private String id;
  private String firstname;
  private String lastname;
  private Address address;

  // … getters and setters omitted
}

前面的示例展示了一个简单的域对象。你可以使用它来创建Example。默认情况下,具有null值的字段将被忽略,字符串将通过使用特定于存储的默认值进行匹配。

根据示例条件将属性包含到查询中是基于可否定性的。使用原语类型(intdouble,…)的属性总是包括在内,除非忽略属性路径

可以使用of工厂方法或[ExampleMatcher](#query-by-example.matchers)构建示例。Example是不变的。下面的清单展示了一个简单的示例:

例 70。简单的例子

Person person = new Person();                         (1)
person.setFirstname("Dave");                          (2)

Example<Person> example = Example.of(person);         (3)
1 创建域对象的新实例。
2 将属性设置为“查询”。
3 创建Example

你可以通过使用存储库运行示例查询。为此,让你的存储库接口扩展QueryByExampleExecutor<T>。下面的清单显示了QueryByExampleExecutor接口的摘录:

例 71。theQueryByExampleExecutor

public interface QueryByExampleExecutor<T> {

  <S extends T> S findOne(Example<S> example);

  <S extends T> Iterable<S> findAll(Example<S> example);

  // … more functionality omitted.
}

# 6.3.示例匹配器

示例不限于默认设置。你可以使用ExampleMatcher为字符串匹配、空值处理和特定于属性的设置指定自己的默认值,如下例所示:

例 72。具有定制匹配的示例匹配器

Person person = new Person();                          (1)
person.setFirstname("Dave");                           (2)

ExampleMatcher matcher = ExampleMatcher.matching()     (3)
  .withIgnorePaths("lastname")                         (4)
  .withIncludeNullValues()                             (5)
  .withStringMatcher(StringMatcher.ENDING);            (6)

Example<Person> example = Example.of(person, matcher); (7)
1 创建域对象的新实例。
2 设置属性。
3 创建ExampleMatcher以期望所有值匹配。
在此阶段,即使没有进一步的配置,它也是可用的。
4 构造一个新的ExampleMatcher来忽略lastname属性路径。
5 构造一个新的ExampleMatcher以忽略lastname属性路径并包括空值。
6 构造一个新的ExampleMatcher以忽略lastname属性路径,包括空值,并执行后缀字符串匹配。
7 基于域对象和配置的ExampleMatcher创建一个新的Example

默认情况下,ExampleMatcher期望探针上设置的所有值都匹配。如果希望得到与隐式定义的任何谓词匹配的结果,请使用ExampleMatcher.matchingAny()

你可以为各个属性指定行为(例如“firstname”和“lastname”,或者,对于嵌套属性,“address.city”)。你可以使用匹配的选项和大小写敏感性来调整它,如以下示例所示:

例 73。配置匹配器选项

ExampleMatcher matcher = ExampleMatcher.matching()
  .withMatcher("firstname", endsWith())
  .withMatcher("lastname", startsWith().ignoreCase());
}

配置 Matcher 选项的另一种方法是使用 lambdas(在 Java8 中引入)。这种方法创建了一个回调,该回调要求实现器修改匹配器。你不需要返回 Matcher,因为配置选项保存在 Matcher 实例中。下面的示例展示了一个使用 lambdas 的匹配器:

例 74。使用 lambdas 配置匹配器选项

ExampleMatcher matcher = ExampleMatcher.matching()
  .withMatcher("firstname", match -> match.endsWith())
  .withMatcher("firstname", match -> match.startsWith());
}

Example创建的查询使用配置的合并视图。默认的匹配设置可以设置在ExampleMatcher级别,而单独的设置可以应用于特定的属性路径。设置在ExampleMatcher上的设置由属性路径设置继承,除非它们是显式定义的。属性修补程序上的设置比默认设置具有更高的优先级。下表描述了各种ExampleMatcher设置的作用域:

Setting 范围
Null-handling ExampleMatcher
String matching ExampleMatcher和属性路径
Ignoring properties 属性路径
Case sensitivity ExampleMatcher和属性路径
Value transformation 属性路径

# 7. 审计

# 7.1.基础知识

Spring 数据提供了复杂的支持,以透明地跟踪谁创建或更改了一个实体以及何时发生了更改。为了从该功能中受益,你必须为你的实体类配备审计元数据,这些元数据可以使用注释或通过实现接口来定义。此外,必须通过注释配置或 XML 配置来启用审计,以注册所需的基础设施组件。有关配置示例,请参阅特定于商店的部分。

只跟踪创建和修改日期的应用程序不需要指定[AuditorAware]。

# 7.1.1.基于注释的审计元数据

我们提供@CreatedBy@LastModifiedBy来捕获创建或修改实体的用户,以及@CreatedDate@LastModifiedDate来捕获更改发生的时间。

例 75。被审计实体

class Customer {

  @CreatedBy
  private User user;

  @CreatedDate
  private Instant createdDate;

  // … further properties omitted
}

正如你所看到的,根据你想要捕获的信息,可以有选择地应用这些注释。捕获何时进行了更改的注释可以用于类型 joda-time、DateTime、遗留 JavaDateCalendar、JDK8 日期和时间类型以及longLong的属性。

审核元数据不一定需要存在于根级实体中,但可以添加到嵌入式实体中(取决于实际使用的存储),如下面的剪辑所示。

例 76。嵌入式实体中的审计元数据

class Customer {

  private AuditMetadata auditingMetadata;

  // … further properties omitted
}

class AuditMetadata {

  @CreatedBy
  private User user;

  @CreatedDate
  private Instant createdDate;

}

# 7.1.2.基于接口的审计元数据

如果你不想使用注释来定义审计元数据,那么可以让你的域类实现Auditable接口。它公开了所有审计属性的 setter 方法。

# 7.1.3.AuditorAware

在使用@CreatedBy@LastModifiedBy的情况下,审计基础结构需要以某种方式意识到当前的主体。为了做到这一点,我们提供了一个AuditorAware<T>SPI 接口,你必须实现该接口,以告知基础结构当前与应用程序交互的用户或系统是谁。泛型类型T定义了用@CreatedBy@LastModifiedBy注释的属性必须是什么类型。

下面的示例展示了使用 Spring Security 的Authentication对象的接口的实现:

例 77。基于 Spring 安全性的AuditorAware的实现

class SpringSecurityAuditorAware implements AuditorAware<User> {

  @Override
  public Optional<User> getCurrentAuditor() {

    return Optional.ofNullable(SecurityContextHolder.getContext())
            .map(SecurityContext::getAuthentication)
            .filter(Authentication::isAuthenticated)
            .map(Authentication::getPrincipal)
            .map(User.class::cast);
  }
}

该实现访问由 Spring Security 提供的Authentication对象,并查找你在UserDetailsService实现中创建的自定义UserDetails实例。在这里,我们假设你通过UserDetails实现公开了域用户,但是,基于找到的Authentication,你也可以从任何地方查找它。

# 7.1.4.ReactiveAuditorAware

当使用反应性基础结构时,你可能希望利用上下文信息来提供@CreatedBy@LastModifiedBy信息。我们提供了一个ReactiveAuditorAware<T>SPI 接口,你必须实现该接口,以告知基础结构当前与应用程序交互的用户或系统是谁。泛型类型T定义了用@CreatedBy@LastModifiedBy注释的属性必须是什么类型。

下面的示例展示了接口的一个实现,该接口使用了 Responable Spring Security 的Authentication对象:

例 78。基于 Spring 安全性的ReactiveAuditorAware的实现

class SpringSecurityAuditorAware implements ReactiveAuditorAware<User> {

  @Override
  public Mono<User> getCurrentAuditor() {

    return ReactiveSecurityContextHolder.getContext()
                .map(SecurityContext::getAuthentication)
                .filter(Authentication::isAuthenticated)
                .map(Authentication::getPrincipal)
                .map(User.class::cast);
  }
}

该实现访问由 Spring Security 提供的Authentication对象,并查找你在UserDetailsService实现中创建的自定义UserDetails实例。这里我们假设你通过UserDetails实现公开了域用户,但是,基于找到的Authentication,你也可以从任何地方查找它。

# 附录

# 附录 A:名称空间引用

# <repositories />元素

<repositories />元素触发了 Spring 数据存储库基础设施的设置。最重要的属性是base-package,它定义了要扫描 Spring 数据存储库接口的包。见“XML 配置”。下表描述了<repositories />元素的属性:

Name 说明
base-package 定义要扫描存储库接口的包,这些存储库接口在自动检测模式下扩展*Repository(实际接口由特定的 Spring 数据模块决定)。配置包下面的所有包也会被扫描。允许使用通配符。
repository-impl-postfix 定义后缀以自动检测自定义存储库实现。名称以配置后缀结尾的类被视为候选类。默认值为Impl
query-lookup-strategy 确定用于创建查找器查询的策略。详见“查询查找策略”。默认值为create-if-not-found
named-queries-location 定义用于搜索包含外部定义的查询的属性文件的位置。
consider-nested-repositories 是否应该考虑嵌套的存储库接口定义。默认值为false

# 附录 B:Populators 名称空间引用

# <populator />元素

<populator />元素允许通过 Spring 数据存储库基础设施填充数据存储。[1]

Name 说明
locations 从存储库中查找要读取对象的文件的位置应该是填充的。

# 附录 C:存储库查询关键字

# 支持的查询方法主题关键字

下表列出了通常由 Spring 数据存储库支持的用于表示谓词的查询派生机制的主题关键字。请参阅特定于商店的文档以获得所支持的关键字的确切列表,因为此处列出的某些关键字在特定商店中可能不受支持。

Keyword 说明
find…By, read…By, get…By, query…By, search…By, stream…By 一般的查询方法通常返回存储库类型、CollectionStreamable子类型或结果包装器,如PageGeoResults或任何其他特定于存储的结果包装器。可以用作findBy…findMyDomainTypeBy…或与其他关键字组合使用。
exists…By 存在投影,通常返回boolean结果。
count…By 计数投影返回一个数字结果。
delete…By, remove…By 删除查询方法,返回不返回任何结果(void)或删除计数。
…First<number>…, …Top<number>… 将查询结果限制为结果的第一个<number>。这个关键字可以出现在find(和其他关键字)和by之间的主题的任何位置。
…Distinct… 使用不同的查询只返回唯一的结果。请参阅特定于商店的文档,以确定是否支持该功能。这个关键字可以出现在find(和其他关键字)和by之间的主题的任何位置。

# 支持的查询方法谓词关键字和修饰符

下表列出了通常由 Spring 数据存储库查询派生机制支持的谓词关键字。但是,请参阅特定于商店的文档以获得所支持的关键字的确切列表,因为此处列出的某些关键字在特定商店中可能不受支持。

Logical keyword 关键字表达式
AND And
OR Or
AFTER After, IsAfter
BEFORE Before, IsBefore
CONTAINING Containing, IsContaining, Contains
BETWEEN Between, IsBetween
ENDING_WITH EndingWith, IsEndingWith, EndsWith
EXISTS Exists
FALSE False, IsFalse
GREATER_THAN GreaterThan, IsGreaterThan
GREATER_THAN_EQUALS GreaterThanEqual, IsGreaterThanEqual
IN In, IsIn
IS IsEquals,(或者没有关键字)
IS_EMPTY IsEmpty, Empty
IS_NOT_EMPTY IsNotEmpty, NotEmpty
IS_NOT_NULL NotNull, IsNotNull
IS_NULL Null, IsNull
LESS_THAN LessThan, IsLessThan
LESS_THAN_EQUAL LessThanEqual, IsLessThanEqual
LIKE Like, IsLike
NEAR Near, IsNear
NOT Not, IsNot
NOT_IN NotIn, IsNotIn
NOT_LIKE NotLike, IsNotLike
REGEX Regex, MatchesRegex, Matches
STARTING_WITH StartingWith, IsStartingWith, StartsWith
TRUE True, IsTrue
WITHIN Within, IsWithin

除了筛选谓词外,还支持以下修饰符列表:

Keyword 说明
IgnoreCase, IgnoringCase 与谓词关键字一起使用,用于不区分大小写的比较。
AllIgnoreCase, AllIgnoringCase 忽略所有合适属性的大小写。在查询方法谓词中的某个地方使用。
OrderBy… 指定一个静态排序顺序,后面是属性路径和方向(例如OrderByFirstnameAscLastnameDesc)。

# 附录 D:存储库查询返回类型

# 支持的查询返回类型

下表列出了 Spring 数据存储库通常支持的返回类型。但是,请参阅特定于商店的文档,以获得所支持的返回类型的确切列表,因为此处列出的某些类型在特定商店中可能不受支持。

地理空间类型(例如GeoResultGeoResultsGeoPage)仅适用于支持地理空间查询的数据存储。
一些存储模块可能会定义自己的结果包装器类型。
Return type 说明
void 表示没有返回值。
Primitives Java 原语。
Wrapper types Java 包装器类型。
T 一个独特的实体。期望查询方法最多返回一个结果。如果没有找到结果,则返回null。多个结果会触发IncorrectResultSizeDataAccessException
Iterator<T> anIterator
Collection<T> aCollection
List<T> aList
Optional<T> 一个 Java8 或番石榴Optional。期望查询方法最多返回一个结果。如果没有找到结果,则返回Optional.empty()Optional.absent()。多个结果会触发IncorrectResultSizeDataAccessException
Option<T> 要么是 scala,要么是 vavrOption类型。在语义上与前面描述的 Java8 的Optional相同。
Stream<T> a java8Stream
Streamable<T> 这是Iterable的一个方便的扩展,Directy 将方法公开于流、映射和筛选结果、将它们连接等。
Types that implement Streamable and take a Streamable constructor or factory method argument 公开构造函数或….of(…)/….valueOf(…)工厂方法的类型,以Streamable为参数。详见返回自定义的可刷新包装器类型
Vavr Seq, List, Map, Set VAVR 集合类型。详见对 VAVR 收藏的支持
Future<T> aFuture。期望对方法进行@Async注释,并且需要启用 Spring 的异步方法执行功能。
CompletableFuture<T> a java8CompletableFuture。期望对方法进行@Async注释,并且需要启用 Spring 的异步方法执行功能。
ListenableFuture aorg.springframework.util.concurrent.ListenableFuture。期望对方法进行@Async注释,并且需要启用 Spring 的异步方法执行功能。
Slice<T> 表示是否有更多可用数据的大小的数据块。需要Pageable方法参数。
Page<T> aSlice带有附加信息,如结果的总数。需要Pageable方法参数。
GeoResult<T> 带有附加信息的结果条目,例如到参考位置的距离。
GeoResults<T> 带有附加信息的GeoResult<T>列表,例如到参考位置的平均距离。
GeoPage<T> aPageGeoResult<T>等参考位置的平均距离。
Mono<T> 一个项目反应堆Mono发射零或使用反应库的一个元素。期望查询方法最多返回一个结果。如果没有找到结果,则返回Mono.empty()。多个结果会触发IncorrectResultSizeDataAccessException
Flux<T> 一个项目反应堆Flux使用反应性存储库发射零、一个或多个元素。返回Flux的查询也可以发出无限数量的元素。
Single<T> 一个 RxJavaSingle使用反应库发射单个元素。期望查询方法最多返回一个结果。如果没有找到结果,则返回Mono.empty()。多个结果会触发IncorrectResultSizeDataAccessException
Maybe<T> 一个 RxJavaMaybe使用反应库发射零或一个元素。期望查询方法最多返回一个结果。如果没有找到结果,则返回Mono.empty()。多个结果触发IncorrectResultSizeDataAccessException
Flowable<T> 一个 RxJavaFlowable使用反应库发射零、一个或多个元素。返回Flowable的查询也可以发出无限数量的元素。

1。参见XML 配置