1. 前言

针对Apache Solr项目的Spring Data将核心Spring概念应用于使用Apache Solr搜索引擎开发解决方案。我们提供了一个“模板”作为存储和查询文档的高级抽象。 你会注意到与Spring Framework中mongodb支持的相似之处。

1.2. 要求

需要Java 8运行时和 Apache Solr 6.6。最好是6.6.x版本。

<dependency>
  <groupId>org.apache.solr</groupId>
  <artifactId>solr-solrj</artifactId>
  <version>${solr.version}</version>
</dependency>

2. 使用Spring Data存储库

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

Spring Data存储库文档和你的模块。

本章介绍Spring Data存储库的核心概念和接口。本章中的信息来自Spring Data Commons模块。 它使用Java Persistence API(JPA)模块的配置和代码示例。 你应该将XML名称空间声明和要扩展的类型调整为你所使用的特定模块的等效项。 命名空间参考 涵盖了XML配置参考,支持存储库API的所有Spring Data模块都支持XML配置。 存储库查询关键字 涵盖了存储库抽象支持的查询方法关键字。有关模块特定功能的详细信息,请参阅本文档该模块的章节。

2.1. 核心概念

Spring Data存储库抽象中的中央接口是 Repository。它将域类以及域类的ID类型作为类型参数进行管理。 此接口主要用作标记接口,用于捕获要使用的类型,并帮助你发现实现它的接口。 CrudRepository 为正在管理的实体类提供复杂的CRUD功能。

Example 1. CrudRepository 接口
public interface CrudRepository<T, ID extends Serializable>
  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)

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

CrudRepository 之上,有一个 PagingAndSortingRepository 抽象,它添加了额外的方法来简化对实体的分页访问:

Example 2. PagingAndSortingRepository 接口
public interface PagingAndSortingRepository<T, ID extends Serializable>
  extends CrudRepository<T, ID> {

  Iterable<T> findAll(Sort sort);

  Page<T> findAll(Pageable pageable);
}

要访问 User 的第二页且每页20个,你可以执行以下操作:

PagingAndSortingRepository<User, Long> repository = // … 获得对bean的访问权限
Page<User> users = repository.findAll(new PageRequest(1, 20)); // 注意第一页从0开始

除查询方法外,还可以使用计数和删除查询的查询派生。

以下列表显示派生计数查询的接口定义:

Example 3. 派生计数查询
interface UserRepository extends CrudRepository<User, Long> {

  long countByLastname(String lastname);
}

以下列表显示了派生删除查询的接口定义:

Example 4. 派生删除查询
interface UserRepository extends CrudRepository<User, Long> {

  long deleteByLastname(String lastname);

  List<User> removeByLastname(String lastname);
}

2.2. 查询方法

标准CRUD功能存储库通常对底层数据存储库进行查询。使用Spring Data,声明这些查询将分为四个步骤:

  1. 声明继承 Repository 或其子接口之一的接口,并键入它应处理的域类和ID类型,如以下示例所示:

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

    interface PersonRepository extends Repository<Person, Long> {
      List<Person> findByLastname(String lastname);
    }
  3. 设置Spring以使用 Java配置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
           http://www.springframework.org/schema/beans/spring-beans.xsd
           http://www.springframework.org/schema/data/jpa
           http://www.springframework.org/schema/data/jpa/spring-jpa.xsd">
      
         <jpa:repositories base-package="com.acme.repositories"/>
      
      </beans>

    在此示例中使用JPA命名空间。如果对任何其他存储使用存储库抽象,则需要将其声明为特定于存储模块的相应命名空间。 换句话说,例如你使用MongoDB则需要将 jpa 更改为 mongodb

    + 另请注意,JavaConfig配置未显式设置包,因为默认情况下使用带该注解的类的包。 要自定义要扫描的包,请使用特定于数据存储库的 @Enable${store}Repositories 注解的 basePackage 属性。

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

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

以下各节详细说明了每个步骤:

2.3. 定义存储库接口

首先,定义特定于域类的存储库接口。接口必须扩展 Repository 并键入域类和ID类型。如果要公开该域类型的CRUD方法,请扩展 CrudRepository 而不是 Repository

2.3.1. 微调存储库定义

通常,存储库接口扩展了 RepositoryCrudRepositoryPagingAndSortingRepository。或者,如果你不想扩展Spring Data接口,还可以使用 @RepositoryDe​​finition 标注存储库接口。扩展 CrudRepository 暴露了一整套操作实体的方法。如果你希望对所公开的方法有选择性,请将要从 CrudRepository 公开的方法复制到域存储库中。

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

以下示例显示如何有选择地公开CRUD方法(在本例中为 findByIdsave):

Example 5. 有选择地暴露CRUD方法
@NoRepositoryBean
interface MyBaseRepository<T, ID extends Serializable> 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 Data提供的所选存储的基本存储库实现中(例如,如果你使用JPA,则实现是SimpleJpaRepository),因为它们与 CrudRepository 中的方法签名匹配。因此,UserRepository 现在可以保存用户,按ID查找单个用户,通过电子邮件地址查找用户。

中间存储库接口需要添加 @NoRepositoryBean 注解。它会确保Spring Data不应在运行时创建该存储库接口的实例。

2.3.2. 存储库方法的null处理

从Spring Data 2.0开始,可以使用Java 8的 Optional 来指示存储库的CRUD方法所返回单个实例可能缺少值。 除此之外,Spring Data支持在查询方法上返回以下包装类型:

  • com.google.common.base.Optional

  • scala.Option

  • io.vavr.control.Option

  • javaslang.control.Option (已弃用,不推荐使用Javaslang)

或者,查询方法可以选择根本不使用包装类型。然后通过返回 null 来指示缺少查询结果。 保证返回集合,集合替代,包装器和流的存储库方法永远不会返回 null,而是返回相应的空表示。 有关详细信息,请参阅 “存储库查询返回类型”。

可空性注解

你可以使用 Spring Framework的可空性注解 来表达存储库方法的可空性约束。 它们在运行时提供了一种 工具友好 的方法和opt-in null 检查,如下所示:

  • {javadocSpringBaseUrl}/org/springframework/lang/NonNullApi.html[@NonNullApi]: 在包级别上使用, 以声明参数和返回值的默认行为是不接受或生成 null 值。

  • {javadocSpringBaseUrl}/org/springframework/lang/NonNull.html[@NonNull]: 用于不能为 null 的参数或返回值 (对于 @NonNullApi 适用的参数和返回值则不需要再加)。

  • {javadocSpringBaseUrl}/org/springframework/lang/Nullable.html[@Nullable]: 用于可以为 null 的参数或返回值。

Spring注解是使用 JSR 305注解进行元注释的(一种隐匿的但广泛传播的JSR)。 JSR 305元注释允许 IDEAEclipseKotlin 等工具供应商以通用方式提供null安全支持,而无需对Spring注解进行硬编码支持。要为查询方法启用运行时检查可空性约束, 需要在 package-info.java 中使用Spring的 @NonNullApi 来激活包级别的非可空性,如以下示例所示:

Example 6. 在package-info.java中声明不可为空性
@org.springframework.lang.NonNullApi
package com.acme;

一旦存在非空默认,就会在运行时验证存储库查询方法调用的可空性约束。如果查询执行结果违反了定义的约束,则抛出异常。 这种情况发生在当方法返回null但声明为非可空时(默认情况下,在存储库所在的包中定义了注解)。 如果你想再次选择使某方法可以返回为 null 的结果,请在该方法上选择使用 @Nullable。使用本节开头提到的结果包装器类型 则继续按预期工作:将空结果转换为表示缺席的 Optional 值。

以下示例显示了刚才描述的许多技术:

Example 7. 使用不同的可空性约束
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。 当传递给方法的 emailAddressnull 时,抛出 IllegalArgumentException
3 当执行的查询未产生结果时返回 null。同时接受 null 作为 emailAddress 的值。
4 当执行的查询没有产生结果时返回 Optional.empty()。当传递给方法的 emailAddressnull 时,抛出 IllegalArgumentException
基于Kotlin的存储库中的可空性

Kotlin对语言中的可空性约束进行了定义。Kotlin代码编译为字节码,它不通过方法签名表达可空性约束,而是通过编译元数据表达。 确保在项目中包含 kotlin-reflect JAR,以便对Kotlin的可空性约束进行内省。 Spring Data存储库使用语言机制来定义这些约束以应用相同的运行时检查,如下所示:

Example 8. 在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

2.3.3. 使用具有多个Spring Data模块的存储库

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

  1. 如果存储库定义 继承了特定于模块的存储库,那么它是特定Spring Data模块的有效候选者。

  2. 如果使用 特定于模块的类型注解 对域类进行注释,则它是特定Spring Data模块的有效候选者。Spring Data模块接受第三方注解 (例如JPA的 @Entity)或存储库已提供的自定义注解(例如Spring Data MongoDB和Spring Data Elasticsearch的 @Document)。

以下示例显示了使用特定于模块的接口的存储库(在本例中为JPA):

Example 9. 使用特定于模块的接口的存储库定义
interface MyRepository extends JpaRepository<User, Long> { }

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

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

MyRepositoryUserRepository 在其类型层次结构中继承 JpaRepository,因此它们是Spring Data JPA模块的有效候选者。

以下示例显示了使用通用接口的存储库:

Example 10. 使用通用接口的存储库定义
interface AmbiguousRepository extends Repository<User, Long> {
 …
}

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

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

AmbiguousRepositoryAmbiguousUserRepository 在其类型层次结构中继承 RepositoryCrudRepository。 虽然在使用单一的Spring Data模块时这是完全正常的,但是多个模块时无法区分这些存储库应该绑定到哪个特定的Spring Data。

以下示例显示了使用带注解的域类的存储库:

Example 11. 使用带注解的域类的存储库
interface PersonRepository extends Repository<Person, Long> {
 …
}

@Entity
class Person {
  …
}

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

@Document
class User {
  …
}

PersonRepository 引用 Person,它使用JPA @Entity 注解进行批注,因此该存储库显然属于Spring Data JPA。 UserRepository 引用 User,它使用Spring Data MongoDB的 @Document 注解进行注释。

以下错误示例显示了使用具有混合注解的域类的存储库:

Example 12. 使用具有混合注解的域类的存储库定义
interface JpaPersonRepository extends Repository<Person, Long> {
 …
}

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

@Entity
@Document
class Person {
  …
}

此示例显示了使用JPA和Spring Data MongoDB注释的域类。它定义了两个存储库,JpaPersonRepositoryMongoDBPersonRepository。 一个用于JPA,另一个用于MongoDB用法。Spring Data不再能够将存储库分开,从而导致未定义的行为。

存储库类型详细信息区分域类注释 用于严格存储库配置,以识别特定Spring Data模块的存储库候选。在同一域类型上使用多个持久性技术特定的注解是可能的, 并允许跨多种持久性技术重用域类型。但是,Spring Data不再能够确定用于绑定存储库的唯一模块。

区分存储库的最后一种方法是使用存储库基础包。基础包定义了扫描存储库接口定义的起点,这意味着你需要手动将存储库定义放在相应的包中。 默认情况下,基于注解驱动的配置使用该配置类的包,但 基于XML的配置 中的基本包是必需手动配置的。

以下示例显示了注解驱动的基础包配置:

Example 13. 注解驱动的基础包配置
@EnableJpaRepositories(basePackages = "com.acme.repositories.jpa")
@EnableMongoRepositories(basePackages = "com.acme.repositories.mongo")
interface Configuration { }

2.4. 定义查询方法

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

  • 从方法名称派生查询。

  • 使用手动定义的查询。

可用选项取决于实际仓储。但是,必须有一个策略来决定如何创建实际查询。下一节将介绍可用策略选项。

2.4.1. 查询查找策略

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

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

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

  • CREATE_IF_NOT_FOUND(默认)结合 CREATEUSE_DECLARED_QUERY。它首先查找声明的查询,如果没有找到声明的查询, 它会创建一个基于自定义方法名称的查询。这是默认的查找策略,因此,如果你未明确配置任何内容,则使用此策略。 它允许通过方法名称快速查询,还可以根据需要引入声明的查询来自定义这些查询。

2.4.2. 查询创建

Spring Data存储库基础结构中的查询构建器机制对于构建对存储库实体的约束查询很有用。该机制剥离来自于方法的前缀 find…​Byread…​Byquery…​Bycount…​By,和 get…​By 并解析其余部分。 introduction子句可以包含更多表达式,例如 Distinct 在要创建的查询上设置去重标志。但是, 第一个 By 用作分隔符来指示实际条件的开始。在最基本的层面上,你可以在实体属性上定义条件,并将它们与 AndOr 连接起来。 以下示例显示了如何创建大量查询:

Example 14. 从方法名称创建查询
interface PersonRepository extends Repository<User, Long> {

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

  // 为查询启用distinct标志
  List<Person> findDistinctPeopleByLastnameOrFirstname(String lastname, String firstname);
  List<Person> findPeopleDistinctByLastnameOrFirstname(String lastname, String firstname);

  // 启用忽略单个属性的大小写
  List<Person> findByLastnameIgnoreCase(String lastname);
  // 启用忽略所有合适属性的大小写
  List<Person> findByLastnameAndFirstnameAllIgnoreCase(String lastname, String firstname);

  // 为查询启用静态ORDER BY
  List<Person> findByLastnameOrderByFirstnameAsc(String lastname);
  List<Person> findByLastnameOrderByFirstnameDesc(String lastname);
}

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

  • 表达式通常是属性遍历与可以连接的运算符相结合。你可以将属性表达式与 ANDOR 组合使用。 对于属性表达式,你还可以获得诸如 BetweenLessThanGreaterThanLike 之类的运算符的支持。 支持的运算符可能因仓储而异,因此请参阅参考文档的相应部分。

  • 方法解析器支持为各个属性设置 IgnoreCase 标志(例如,findByLastnameIgnoreCase(…​))或支持忽略大小写的类型的所有属性 (通常是String实例 - 例如,findByLastnameAndFirstnameAllIgnoreCase(…​))。是否支持忽略大小写可能因仓储而异, 因此请参阅参考文档中有关特定于仓储的查询方法的相关章节。

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

2.4.3. 属性表达式

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

List<Person> findByAddressZipCode(ZipCode zipCode);

假设 Person 有一个带 ZipCodeAddress。在这种情况下,该方法创建属性遍历 x.address.zipCode。 解析算法首先将整个部分(AddressZipCode)解释为属性,并检查域类中是否具有该名称的属性(未大写)。如果查找成功,则使用该属性。 如果没有,算法 自右向左 在方法签名属性的驼峰处进行切割,分成头部和尾部,并试图找到相应的属性 - 在我们的示例中是 AddressZipCode。如果算法找到具有该头部的属性,则会采用尾部并继续从那里构建查询树(以刚才描述的方式将尾部分开)。 如果第一个分割不匹配,算法会将分割点左移(Address,ZipCode)并继续检测。

虽然这适用于大多数情况,算法有可能选择错误的属性。假设 Person 类也有一个 addressZip 属性。 算法将在第一轮拆分中命中并选择错误的属性,然后失败(因为 addressZip 的类型可能没有 code 属性)。

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

List<Person> findByAddress_ZipCode(ZipCode zipCode);

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

2.4.4. 特殊参数处理

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

Example 15. 在查询方法中使用 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);

第一种方法允许你将 org.springframework.data.domain.Pageable 实例传递给查询方法,以动态地将分页添加到静态定义的查询中。 Page 知道可用的总元素数和总页数。内部通过触发 count 查询来实现计算总数。由于这可能很昂贵(取决于所使用的仓储), 你可以改为返回 SliceSlice 只知道是否还有下一个 Slice 可用,这在遍历更大的结果集时可能就足够了。

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

要了解实体究竟有多少页,你必须触发额外的计数查询。默认情况下,此查询是从你实际触发的查询派生的。

2.4.5. 限制查询结果

查询方法的结果可以通过使用 firsttop 关键字来限制,这些关键字可以互换使用。 可选的数值可以附加到 topfirst,以指定要返回的最大结果集的大小。如果省略该数字,则假定结果大小为1。 以下示例显示如何限制查询大小:

Example 16. 使用Top和First限制查询的结果大小
User findFirstByOrderByLastnameAsc();

User findTopByOrderByAgeDesc();

Page<User> queryFirst10ByLastname(String lastname, Pageable pageable);

Slice<User> findTop3ByLastname(String lastname, Pageable pageable);

List<User> findFirst10ByLastname(String lastname, Sort sort);

List<User> findTop10ByLastname(String lastname, Pageable pageable);

限制表达式也支持 Distinct 关键字。此外,对于将结果集限制为一个实例的查询,支持使用 Optional 关键字将结果包装。

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

通过使用 Sort 参数将结果与动态排序结合使用,可以用于表达最小“K”个元素以及最大“K”个元素的查询方法。

2.4.6. 流式查询结果

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

Example 17. 使用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 方法或使用Java 7 try-with-resources 块手动关闭 Stream,如以下示例所示:
Example 18. 使用try-with-resources块关闭 Stream<T>
try (Stream<User> stream = repository.findAllByCustomQueryAndStream()) {
  stream.forEach(…);
}
当前并非所有Spring Data模块都支持 Stream<T> 作为返回类型。

2.4.7. 异步查询结果

可以使用 Spring的异步方法执行功能 异步运行存储库查询。这意味着该方法在调用时立即返回,而实际的查询执行发生在已提交给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 使用Java 8 java.util.concurrent.CompletableFuture 作为返回类型。
3 使用 org.springframework.util.concurrent.ListenableFuture 作为返回类型。

2.5. 创建存储库实例

在本节中,你将为定义的存储库接口创建实例和bean定义。一种方法是使用随每个支持存储库机制的Spring Data模块一起提供的Spring命名空间, 尽管我们通常建议使用Java配置。

2.5.1. XML配置

每个Spring Data模块都包含一个存储库元素,允许你定义Spring扫描的基础包,如以下示例所示:

Example 19. 通过XML启用Spring Data存储库
<?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
    http://www.springframework.org/schema/beans/spring-beans.xsd
    http://www.springframework.org/schema/data/jpa
    http://www.springframework.org/schema/data/jpa/spring-jpa.xsd">

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

</beans:beans>

在前面的示例中,指示Spring扫描 com.acme.repositories 及其所有子包,以查找继承 Repository 或其子接口的接口。 对于找到的每个接口,基础结构都会注册特定于持久性技术的 FactoryBean,以创建相应代理去处理查询方法调用。 每个bean都以接口名称命名(首字母小写),因此 UserRepository 的接口将在 userRepository 下注册。 base-package 属性允许使用通配符,以便你可以定义扫描包的模式。

使用过滤器

默认情况下,基础结构会选择位于已配置的基本包下,继承特定于持久性技术的 Repository 子接口的每个接口,并为其创建一个bean实例。 但是,你可能希望对某些接口为其创建bean实例,进行更细粒度的控制。为此,请在 <repositories/> 元素中使用 <include-filter/><exclude-filter/> 元素。语义完全等同于Spring的上下文命名空间中的元素。 有关详细信息,请参阅这些元素的 Spring参考文档

例如,要排除某些接口从而不实例化为存储库bean,可以使用以下配置:

Example 20. 使用exclude-filter元素
<repositories base-package="com.acme.repositories">
  <context:exclude-filter type="regex" expression=".*SomeRepository" />
</repositories>

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

2.5.2. Java配置

还可以通过在JavaConfig类上使用特定于仓储的 @Enable${store}Repositories 注解来启用某种存储库基础设施。 有关Spring容器的基于Java的配置的介绍,请参阅 Spring参考文档中的JavaConfig

启用S​​pring Data存储库的示例配置类似于以下内容:

Example 21. 基于注解的存储库配置示例
@Configuration
@EnableJpaRepositories("com.acme.repositories")
class ApplicationConfiguration {

  @Bean
  EntityManagerFactory entityManagerFactory() {
    // …
  }
}
上面的示例使用特定JPA的注解,你可以根据实际使用的存储库模块进行更改。这同样适用于 EntityManagerFactory bean的定义。 请参阅有关特定于仓储的配置的部分。

2.5.3. 独立使用

你还可以在Spring容器之外使用存储库基础结构 - 例如,在CDI环境中。你仍然需要在类路径中使用一些Spring库, 但通常也可以通过编程方式设置存储库。提供存储库支持的Spring Data模块提供了一个特定于持久性技术的 RepositoryFactory, 你可以按如下方式使用它:

Example 22. 存储库工厂的独立使用
RepositoryFactorySupport factory = … // 在这里实例化工厂
UserRepository repository = factory.getRepository(UserRepository.class);

2.6. Spring Data Repositories的自定义实现

本节介绍存储库自定义以及片段如何构成复合存储库。

当查询方法需要不同的行为或无法通过查询派生实现时,则需要提供自定义实现。 Spring Data存储库允许你提供自定义存储库代码,并将其与通用CRUD抽象和查询方法功能集成。

2.6.1. 自定义单个存储库

要使用自定义功能丰富存储库,必须首先定义片段接口和自定义功能的实现,如以下示例所示:

Example 23. 自定义存储库功能的片段接口
interface CustomizedUserRepository {
  void someCustomMethod(User user);
}

然后,你可以让存储库接口继承片段接口,如以下示例所示:

Example 24. 自定义存储库功能的实现
class CustomizedUserRepositoryImpl implements CustomizedUserRepository {

  public void someCustomMethod(User user) {
    // 你的自定义实现
  }
}
为片段接口实现类的名字添加 Impl 后缀很重要。

实现本身不依赖于Spring Data,可以是常规的Spring bean。因此,你可以使用标准依赖项注入行为来注入对其他bean (例如JdbcTemplate)的引用等等。

你可以让存储库接口继承自片段接口,如以下示例所示:

Example 25. 存储库接口的更改
interface UserRepository extends CrudRepository<User, Long>, CustomizedUserRepository {

  // 在这里声明查询方法
}

使用存储库接口继承片段接口可以组合CRUD和自定义功能,并使其可供客户端使用。

Spring Data存储库通过使用构成存储库组合的片段来实现。片段是基本存储库,特定功能方面(如 QueryDsl), 自定义接口及其实现。每次向存储库接口添加接口时,都可以通过添加片段来增强组合。每个Spring Data模块都提供了基本存储库和存储库方面的实现。

以下示例显示了自定义接口及其实现:

Example 26. 片段与它们的实现
interface HumanRepository {
  void someHumanMethod(User user);
}

class HumanRepositoryImpl implements HumanRepository {

  public void someHumanMethod(User user) {
    // 你的自定义实现
  }
}

interface ContactRepository {

  void someContactMethod(User user);

  User anotherContactMethod(User user);
}

class ContactRepositoryImpl implements ContactRepository {

  public void someContactMethod(User user) {
    // 你的自定义实现
  }

  public User anotherContactMethod(User user) {
    // 你的自定义实现
  }
}

以下示例显示了继承 CrudRepository 的自定义存储库的接口:

Example 27. 存储库接口的更改
interface UserRepository extends CrudRepository<User, Long>, HumanRepository, ContactRepository {

  // 在这里声明查询方法
}

存储库可以由多个自定义实现组成,这些实现按其声明的顺序导入。自定义实现的优先级高于基本实现和存储库方面实现。 如果两个片段提供相同的方法签名,则此排序机制允许你覆盖基本存储库和存储库方面的方法并解决歧义。 存储库片段不限于在单个存储库接口中使用。多个存储库可以使用相同的片段接口,以便你在不同的存储库中重用自定义功能。

以下示例显示了存储库片段及其实现:

Example 28. 片段覆盖 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) {
    // 你的自定义实现
  }
}

以下示例显示了使用前面的存储库片段的存储库:

Example 29. 自定义存储库接口
interface UserRepository extends CrudRepository<User, Long>, CustomizedSave<User> {
}

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

如果使用命名空间配置,则存储库基础结构会通过尝试扫描其找到存储库的包下面的类来自动检测片段的自定义实现。 这些类需要遵循命名约定 - 将命名空间元素配置的 repository-impl-postfix 属性值,后缀到片段接口实现类的名称。 此后缀默认为 Impl。以下示例显示了使用默认后缀的存储库以及为后缀设置自定义值的存储库:

Example 30. 配置示例
<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 Data使用bean名来标识要使用的bean。

给定前面显示的 CustomizedUserRepository 的以下两个自定义实现,则会选择使用第一个实现。 它的bean名称是 customizedUserRepositoryImpl,它与片段接口(CustomizedUserRepository + Impl 后缀)的名称相匹配。

Example 31. 解决有歧义的多个实现
package com.acme.impl.one;

class CustomizedUserRepositoryImpl implements CustomizedUserRepository {

  // 你的自定义实现
}
package com.acme.impl.two;

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

  // 你的自定义实现
}

如果使用 @Component("specialCustom") 注解 UserRepository 接口,那么,bean名称加上 Impl 将与 com.acme.impl.two 中为存储库实现定义的名称相匹配,而不再使用第一个名称。

手动接线

如果你的自定义实现仅使用基于注解的配置和自动装配,则前面展示的方法效果很好,因为它被视为任何其他Spring bean。 如果你的实现片段bean需要特殊布线,你可以声明bean并根据 前一节 中描述的约定对其进行命名。然后,基础结构按名称引用手动定义的bean定义,而不是自己创建一个。 以下示例显示如何手动接线自定义实现:

Example 32. 手动接线自定义实现
<repositories base-package="com.acme.repository" />

<beans:bean id="userRepositoryImpl" class="…">
  <!-- 进一步配置 -->
</beans:bean>

2.6.2. 自定义基础Repository

当你要自定义基本存储库行为以便所有存储库都受到影响时,上一节 中描述的方法需要自定义每个存储库接口。 要改为更改所有存储库的行为,可以创建一个继承特定于持久性技术的存储库基类的实现。然后,此类充当存储库代理的自定义基类,如以下示例所示:

Example 33. 自定义存储库基类
class MyRepositoryImpl<T, ID extends Serializable>
  extends SimpleJpaRepository<T, ID> {

  private final EntityManager entityManager;

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

    // 持有EntityManager可以使用新引入的方法
    this.entityManager = entityManager;
  }

  @Transactional
  public <S extends T> S save(S entity) {
    // 在这里实施自定义
  }
}
该类需要具有特定于存储库工厂实现中,所使用的超类的构造函数。如果存储库基类具有多个构造函数, 则覆盖含有 EntityInformation 和存储特定基础结构对象的构造函数(例如 EntityManager 或模板类)。

最后一步是使Spring Data基础结构了解你自定义的存储库基类。在Java配置中,你可以使用 @Enable${store}Repositories 注解的 repositoryBaseClass 属性来执行此操作,如以下示例所示:

Example 34. 使用JavaConfig配置自定义存储库基类
@Configuration
@EnableJpaRepositories(repositoryBaseClass = MyRepositoryImpl.class)
class ApplicationConfiguration { … }

XML命名空间中提供了相应的属性,如以下示例所示:

Example 35. 使用XML配置自定义存储库基类
<repositories base-package="com.acme.repository" base-class="….MyRepositoryImpl" />

2.7. 从聚合根发布事件

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

Example 36. 公开来自聚合根的域事件
class AnAggregateRoot {

    @DomainEvents                (1)
    Collection<Object> domainEvents() {
        // … 返回要在此处发布的事件
    }

    @AfterDomainEventPublication (2)
    void callbackMethod() {
       // … 可能会清理域事件列表
    }
}
1 使用 @DomainEvents 的方法可以返回单个事件实例或事件集合。它不能携带任何参数。
2 在所有事件发布后,我们有一个使用 @AfterDomainEventPublication 注解的方法。 它可用于潜在地清除要发布的事件列表(以及其他用途)。

每次调用一个Spring Data存储库 save(…​) 方法时都会调用这些方法。

2.8. Spring Data扩展

本节介绍了一组Spring Data扩展,它们可以在各种上下文中使用Spring Data。目前,大多数集成都针对Spring MVC。

2.8.1. Querydsl扩展

Querydsl是一个框架,可以通过其流式API构建静态类型的SQL类查询。

几个Spring Data模块通过 QuerydslPredicateExecutor 提供与Querydsl的集成,如以下示例所示:

Example 37. 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)

  // … 省略了更多功能
}
1 查找并返回与 Predicate 匹配的单个实体。
2 查找并返回与 Predicate 匹配的所有实体。
3 返回与 Predicate 匹配的实体数。
4 返回是否存在与 Predicate 匹配的实体。

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

Example 38. 在存储库中集成Querydsl
interface UserRepository extends CrudRepository<User, Long>, QuerydslPredicateExecutor<User> {
}

上面的示例允许你使用Querydsl Predicate 实例编写类型安全查询,如以下示例所示:

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

userRepository.findAll(predicate);

2.8.2. Web支持

本节包含Spring Data web支持的文档,因为它在Spring Data Commons的当前(及更高版本)版本中已实现。 由于新引入的支持更改了许多内容,因此我们在 web 遗留 中保留了以前行为的文档。

支持存储库编程模型的Spring Data模块具有各种Web支持。与Web相关的组件需要Spring MVC JAR位于类路径上。 其中一些甚至提供与 Spring HATEOAS的集成。 通常,通过在JavaConfig配置类中使用 @EnableSpringDataWebSupport 注解来启用集成支持,如以下示例所示:

Example 39. 启用S​​pring Data Web支持
@Configuration
@EnableWebMvc
@EnableSpringDataWebSupport
class WebConfiguration {}

@EnableSpringDataWebSupport 注解注册了一些我们稍后会讨论的组件。它还将检测类路径上的Spring HATEOAS, 并为它注册集成组件(如果存在)。

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

Example 40. 以XML配置启用Spring Data Web支持
<bean class="org.springframework.data.web.config.SpringDataWebConfiguration" />

<!-- 如果你使用Spring HATEOAS,请注册这个而不是前者 -->
<bean class="org.springframework.data.web.config.HateoasAwareSpringDataWebConfiguration" />
基本Web支持

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

  • 一个 DomainClassConverter 让Spring MVC从请求参数或路径变量中解析存储库管理的域类实例。

  • HandlerMethodArgumentResolver 实现让Spring MVC从请求参数中解析 PageableSort 实例。

DomainClassConverter

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

Example 41. 在方法签名中使用域类型的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 才有资格被发现并进行转换。
为了分页和排序的 HandlerMethodArgumentResolvers

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

Example 42. 使用 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 实例:

Table 1. 为 Pageable 实例评估请求参数

page

要检索的页码。0索引开始并默认为0。

size

要检索的每页元素数。默认为20。

sort

应按格式 property,property(,ASC|DESC) 排序的属性。默认排序方向是升序。 如果要切换方向,请使用多个 sort 参数 - 例如,?sort=firstname&sort=lastname,asc

要自定义此行为,请分别注册实现 PageableHandlerMethodArgumentResolverCustomizer 接口或 SortHandlerMethodArgumentResolverCustomizer 接口的bean。调用其 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 相当于 new PageRequest(0,20),但可以通过在 Pageable 参数上使用 @PageableDefault 注解进行自定义。

Pageables 的超媒体支持

Spring HATEOAS附带了一个表示模型类(PagedResources),它允许使用必要的 Page 元数据丰富 Page 实例的内容以生成允许客户端 轻松浏览页面的链接。将 Page 转换为 PagedResources 是通过Spring HATEOAS ResourceAssembler 接口的实现完成的, 该接口称为 PagedResourcesAssembler。以下示例显示如何将 PagedResourcesAssembler 用作控制器方法参数:

Example 43. 使用 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 可能会显示并附加下一页的链接。链接指向方法映射到的URI。添加到方法的分页参数与 PageableHandlerMethodArgumentResolver 的设置相匹配,以确保稍后可以解析链接。

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

{
  "links" : [
    {
      "rel" : "next",
      "href" : "http://localhost:8080/persons?page=1&size=20
    }
  ],
  "content" : [
     … // 此处呈现20个Person实例
  ],
  "pageMetadata" : {
    "size" : 20,
    "totalElements" : 30,
    "totalPages" : 2,
    "number" : 0
  }
}

你会看到组装者生成了正确的URI,并且还选择了默认配置以将参数解析为即将发出的请求的 Pageable。 这意味着,如果更改该配置,链接将遵循设置自动更改。默认情况下,组装者指向它所调用的控制器方法, 但可以通过交换自定义链接来自定义链接以构建分页链接,这会用到重载的 PagedResourcesAssembler.toResource(…​) 方法。

Web数据绑定支持

Spring Data投影(在Projections中描述)可用于通过使用 JSONPath 表达式来绑定传入的请求有效载荷(需要 Jayway JsonPathXPath表达式(需要 XmlBeam),如以下示例所示:

Example 44. 使用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。 前面的方法声明将尝试在给定文档中的任何位置查找 firstnamelastname XML查找在传入文档的顶级执行。 JSON变体首先尝试顶级 lastname,但如果前者没有返回值,也会尝试查找嵌套在 user 子文档中的 lastname。 这样,可以轻松地减轻源文档结构的变化,而无需客户端调用公开的方法(通常是基于类的有效负载绑定的缺点)。

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

对于Spring MVC,只要开启 @EnableSpringDataWebSupport 注解,就会自动注册必要的转换器,并且类路径上可以使用所需的依赖项。 要与 RestTemplate 一起使用,请手动注册 ProjectingJackson2HttpMessageConverter(JSON)或 XmlBeamHttpMessageConverter

有关更多信息,请参阅 {exampleProjectBaseUrl}[Spring Data Examples]典范存储库中的 {exampleProjectBaseUrl}/tree/master/web/projection[Web投影示例]。

Querydsl Web支持

对于那些具有 QueryDSL集成的仓储,可以从 Request 查询字符串中包含的属性派生查询。

请考虑以下查询字符串:

?firstname=Dave&lastname=Matthews

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

QUser.user.firstname.eq("Dave").and(QUser.user.lastname.eq("Matthews"))
在类路径中找到Querydsl时,将自动启用该功能以及 @EnableSpringDataWebSupport

@QuerydslPredicate 添加到方法签名提供了一个可立即使用的谓词,可以使用 QuerydslPredicateExecutor 运行。

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

以下示例显示如何在方法签名中使用 @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 将查询字符串参数解析为匹配 UserPredicate

默认绑定如下:

  • Object 在简单属性上做 eq

  • Object 在集合是否有某属性上做 contains

  • Collection 在简单的属性上做 in

可以通过 @QuerydslPredicatebindings 属性或通过使用Java 8默认方法并将 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 Predicate 解析中排除 password 属性。

2.8.3. 存储库填充

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

假设你有一个文件 data.json,其中包含以下内容:

Example 45. 以JSON定义的数据
[
    {
        "_class" : "com.acme.Person",
        "firstname" : "Dave",
        "lastname" : "Matthews"
    },
    {
        "_class" : "com.acme.Person",
        "firstname" : "Carter",
        "lastname" : "Beauford"
    }
]

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

Example 46. 声明Jackson存储库populator
<?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
    http://www.springframework.org/schema/beans/spring-beans.xsd
    http://www.springframework.org/schema/data/repository
    http://www.springframework.org/schema/data/repository/spring-repository.xsd">

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

</beans>

上述声明会导致由Jackson ObjectMapper 来读取和反序列化 data.json 文件。

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

可以使用 unmarshaller-populator 元素声明使用XML来定义填充存储库的数据,可将其配置为使用Spring OXM中提供的 可选XML marshaller之一。有关详细信息,请参阅 Spring参考文档。 以下示例说明如何使用JAXB解组存储库填充:

Example 47. 使用JAXB解组的存储库populator
<?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
    http://www.springframework.org/schema/beans/spring-beans.xsd
    http://www.springframework.org/schema/data/repository
    http://www.springframework.org/schema/data/repository/spring-repository.xsd
    http://www.springframework.org/schema/oxm
    http://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>

3. Solr存储库

以下包含Solr存储库实现的详细信息。

3.1. Spring命名空间

Spring Data Solr模块包含一个自定义命名空间,允许定义存储库bean以及实例化 SolrClient 元素。

使用 repositories 元素查找Spring Data存储库,如创建存储库实例中所述。

Example 48. 使用命名空间设置Solr存储库
<?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:solr="http://www.springframework.org/schema/data/solr"
  xsi:schemaLocation="http://www.springframework.org/schema/beans
    http://www.springframework.org/schema/beans/spring-beans.xsd
    http://www.springframework.org/schema/data/solr
    http://www.springframework.org/schema/data/solr/spring-solr.xsd">

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

使用 solr-serverembedded-solr-server 元素在上下文中注册 SolrClient 的实例。

Example 49. 使用命名空间的HttpSolrClient
<?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:solr="http://www.springframework.org/schema/data/solr"
  xsi:schemaLocation="http://www.springframework.org/schema/beans
    http://www.springframework.org/schema/beans/spring-beans.xsd
    http://www.springframework.org/schema/data/solr
    http://www.springframework.org/schema/data/solr/spring-solr.xsd">

  <solr:solr-client id="solrClient" url="http://locahost:8983/solr" />
</beans>
Example 50. 使用命名空间的LBSolrClient
<?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:solr="http://www.springframework.org/schema/data/solr"
  xsi:schemaLocation="http://www.springframework.org/schema/beans
    http://www.springframework.org/schema/beans/spring-beans.xsd
    http://www.springframework.org/schema/data/solr
    http://www.springframework.org/schema/data/solr/spring-solr.xsd">

  <solr:solr-client id="solrClient" url="http://locahost:8983/solr,http://localhost:8984/solr" />
</beans>
Example 51. 使用命名空间的EmbeddedSolrServer
<?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:solr="http://www.springframework.org/schema/data/solr"
  xsi:schemaLocation="http://www.springframework.org/schema/beans
    http://www.springframework.org/schema/beans/spring-beans.xsd
    http://www.springframework.org/schema/data/solr
    http://www.springframework.org/schema/data/solr/spring-solr.xsd">

  <solr:embedded-solr-server id="solrClient" solrHome="classpath:com/acme/solr" />
</beans>

3.2. 基于注解的配置

可以通过XML命名空间和通过Java配置使用注解来激活Spring Data Solr存储库支持。

Example 52. 使用JavaConfig的Spring Data Solr存储库
@Configuration
@EnableSolrRepositories
class ApplicationConfig {

  @Bean
  public SolrClient solrClient() {
    EmbeddedSolrServerFactory factory = new EmbeddedSolrServerFactory("classpath:com/acme/solr");
    return factory.getSolrServer();
  }

  @Bean
  public SolrOperations solrTemplate() {
    return new SolrTemplate(solrClient());
  }
}

上面的配置设置了 SolrTemplate 使用的 EmbeddedSolrServer。Spring Data Solr存储库使用 @EnableSolrRepositories 注解激活,该注解基本上具有与XML命名空间相同的属性。 如果未配置基础包,则将使用配置类所在的基础包。

3.3. 使用CDI设置Solr存储库

你还可以使用CDI设置Spring Data Solr存储库,如以下示例所示:

Example 53. 使用Java配置的Spring Data Solr存储库
class SolrTemplateProducer {

  @Produces
  @ApplicationScoped
  public SolrOperations createSolrTemplate() {
    return new SolrTemplate(new EmbeddedSolrServerFactory("classpath:com/acme/solr"));
  }
}

class ProductService {

  private ProductRepository repository;

  public Page<Product> findAvailableProductsByName(String name, Pageable pageable) {
    return repository.findByAvailableTrueAndNameStartingWith(name, pageable);
  }

  @Inject
  public void setRepository(ProductRepository repository) {
    this.repository = repository;
  }
}

3.4. 事务支持

Solr支持服务器级别的事务意味着创建,更新,删除操作,自上次提交/优化/回滚以来在服务器上排队并最终一次性提交/优化/回滚。 Spring Data Solr存储库将在完成时参与Spring被管理事务的提交/回滚更改。

@Transactional
public Product save(Product product) {
  Product savedProduct = jpaRepository.save(product);
  solrRepository.save(savedProduct);
  return savedProduct;
}

3.5. 查询方法

本节介绍如何使用Java类中的方法创建查询。

3.5.1. 查询查找策略

Solr模块支持手动将查询定义为 String 或将其从方法名称派生。

目前没有QueryDSL支持。
声明式查询

从方法名称派生查询并不总是合适的,并且可能导致不可读的方法名称。在这种情况下,你可以使用Solr命名查询(请参阅“使用命名查询”)或使用 @Query 注解(请参阅“使用 @Query 注解”)。

3.5.2. 查询创建

通常,Solr的查询创建机制的工作方式如查询方法中所述。以下示例显示了Solr查询方法:

Example 54. 从方法名称创建查询
public interface ProductRepository extends Repository<Product, String> {
  List<Product> findByNameAndPopularity(String name, Integer popularity);
}

前面的示例转换为以下Solr查询:

q=name:?0 AND popularity:?1

下表描述了Solr支持的关键字:

Table 2. 方法名称中支持的关键字
关键字 样例 Solr查询字符串

And

findByNameAndPopularity

q=name:?0 AND popularity:?1

Or

findByNameOrPopularity

q=name:?0 OR popularity:?1

Is

findByName

q=name:?0

Not

findByNameNot

q=-name:?0

IsNull

findByNameIsNull

q=-name:[* TO *]

IsNotNull

findByNameIsNotNull

q=name:[* TO *]

Between

findByPopularityBetween

q=popularity:[?0 TO ?1]

LessThan

findByPopularityLessThan

q=popularity:[* TO ?0}

LessThanEqual

findByPopularityLessThanEqual

q=popularity:[* TO ?0]

GreaterThan

findByPopularityGreaterThan

q=popularity:{?0 TO *]

GreaterThanEqual

findByPopularityGreaterThanEqual

q=popularity:[?0 TO *]

Before

findByLastModifiedBefore

q=last_modified:[* TO ?0}

After

findByLastModifiedAfter

q=last_modified:{?0 TO *]

Like

findByNameLike

q=name:?0*

NotLike

findByNameNotLike

q=-name:?0*

StartingWith

findByNameStartingWith

q=name:?0*

EndingWith

findByNameEndingWith

q=name:*?0

Containing

findByNameContaining

q=name:*?0*

Matches

findByNameMatches

q=name:?0

In

findByNameIn(Collection<String> names)

q=name:(?0…​ )

NotIn

findByNameNotIn(Collection<String> names)

q=-name:(?0…​ )

Within

findByStoreWithin(Point, Distance)

q={!geofilt pt=?0.latitude,?0.longitude sfield=store d=?1}

Near

findByStoreNear(Point, Distance)

q={!bbox pt=?0.latitude,?0.longitude sfield=store d=?1}

Near

findByStoreNear(Box)

q=store[?0.start.latitude,?0.start.longitude TO ?0.end.latitude,?0.end.longitude]

True

findByAvailableTrue

q=inStock:true

False

findByAvailableFalse

q=inStock:false

OrderBy

findByAvailableTrueOrderByNameDesc

q=inStock:true&sort=name desc

集合类型可以与’Like','Not Like','Starting With','Ending With' 和 'Containing' 一起使用。
Page<Product> findByNameLike(Collection<String> name);

3.5.3. 使用 @Query 注解

使用命名查询(请参阅“使用命名查询”)来声明实体查询是一种有效的方法,适用于少量查询。由于查询本身与执行它们的Java方法相关联,因此你实际上可以使用Spring Data Solr @Query 注解直接绑定它们。以下示例使用 @Query 注解声明查询:

Example 55. 使用 @Query 注解在方法上声明查询
public interface ProductRepository extends SolrRepository<Product, String> {
  @Query("inStock:?0")
  List<Product> findByAvailable(Boolean available);
}

3.5.4. 使用命名查询

命名查询可以保存在属性文件中并连接到相应的方法。你应该记住“查询查找策略”中描述的命名约定或使用 @Query。以下示例显示如何在属性文件中声明命名查询:

Example 56. 在属性文件中声明命名查询
Product.findByNamedQuery=popularity:?0
Product.findByName=name:?0

以下示例使用前面示例中声明的 findByName 命名查询:

public interface ProductRepository extends SolrCrudRepository<Product, String> {

  List<Product> findByNamedQuery(Integer popularity);

  @Query(name = "Product.findByName")
  List<Product> findByAnnotatedNamedQuery(String name);

}

3.6. 文档映射

尽管SolrJ中已经支持实体映射,但Spring Data Solr附带了自己的映射机制(在下一节中描述)。

DocumentObjectBinder 具有卓越的性能。因此,如果你不需要自定义映射,我们建议你使用它。你可以通过在 SolrTemplate 中注册 SolrJConverter 来切换到 DocumentObjectBinder

3.6.1. 对象映射基础

本节介绍Spring Data对象映射,对象创建,字段和属性访问,可变性和不变性的基础知识。注意,本节仅适用于不使用底层数据存储的对象映射的Spring Data模块(如JPA)。另外,请务必查阅特定于存储的部分以了解特定于存储的对象映射,例如索引,自定义列或字段名称等。

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

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

  2. 填充实例所有暴露的属性。

对象创建

Spring Data会自动尝试检测持久化实体的构造函数,以用于实现该类型的对象。检测算法的工作原理如下:

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

  2. 如果有一个有参构造函数,它将被使用。

  3. 如果有多个有参构造函数,则必须使用 @PersistenceConstructor 指明Spring Data要使用的构造函数。

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

可以使用Spring Framework的 @Value 值注解使用特定于存储的SpEL表达式来自定义值解析。有关更多详细信息,请参阅有关存储特定映射的部分。

对象创建的内部机制

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

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 Data使用的构造函数不能是私有的

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

属性填充

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

属性填充的内部机制

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

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;
  }
}
Example 57. 生成的属性访问器
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 Runtime必须允许在原始 ClassLoader 中声明类。Java 9和更新版本施加了某些限制。

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

我们来看看以下实体:

Example 58. 样本实体
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);
  }

  void setRemarks(String remarks) {                                         (5)
    this.remarks = remarks;
  }
}
1 identifier属性是final,但在构造函数中设置为null。该类公开了一个 withId(…​) 方法,该方法用于设置标识符,例如将实例插入数据存储区并生成标识符时,原始Person实例在创建新实例时保持不变。相同的模式通常应用于存储管理的其他属性,但可能必须更改以进行持久性操作。
2 firstnamelastname 属性是可能通过getter公开的普通不可变属性。
3 age 属性是一个不可变的,但是值来自于 birthday 属性。根据所显示的设计,数据库值将胜过默认值,因为Spring Data使用唯一声明的构造函数。即使目的是要优先考虑计算,但重要的是这个构造函数也将 age 作为参数(可能忽略它),否则属性填充步骤将尝试设置 age 字段并由于它是不可变的而失败。
4 comment 属性是可变的,通过直接设置其字段来填充。
5 remarks 属性是可变的,并通过直接设置字段或通过调用setter方法来填充。
6 该类公开了一个工厂方法和一个用于创建对象的构造函数。这里的核心思想是使用工厂方法而不是其他构造函数来避免通过 @PersistenceConstructor 消除构造函数的歧义。相反,在工厂方法中处理属性的默认值。
一般建议
  • 坚持使用不可变对象 — 创建不可变对象很简单,因为实现对象只需要调用它的构造函数。此外,这可以避免域对象被允许客户机代码操作对象状态的setter方法打乱。如果你需要这些,请将它们设置为包保护,以便只能通过有限数量的共存类型来调用它们。仅构造函数实例化比属性填充总体快30%。

  • 提供所有参数的构造函数 — 即使你不能或不希望将实体建模为不可变值,提供一个将实体的所有属性(包括可变属性)作为参数的构造函数仍然是有价值的,因为这允许对象映射跳过属性填充以获得最佳性能。

  • 使用工厂方法而不是重载的构造函数来避免使用 @PersistenceConstructor — 使用最佳性能所需的全参数构造函数,我们通常希望公开更多的应用程序用例特定构造函数,这些构造函数省略了自动生成的标识符等内容。使用静态工厂方法公开all-args构造函数的这些变体是一种已建立的模式。

  • 确保遵守允许使用生成的实例化器和属性访问器类的约束 — 

  • 对于要生成的标识符,仍然使用final字段和wither方法组合使用 — 

  • 使用Lombok避免样板代码 — 由于持久性操作通常需要构造函数接受所有参数,因此它们的声明变成了对字段赋值的样板参数的繁琐重复,使用Lombok的 @AllArgsConstructor 可以最好地避免这种重复。

3.6.2. MappingSolrConverter

MappingSolrConverter 允许你为 SolrDocumentSolrInputDocument 以及嵌套在bean中的其他类型注册自定义转换器。转换器与 DocumentObjectBinder 不是100%兼容,并且 @Indexed 必须添加 readonly = true 来忽略写入Solr的字段。以下示例映射文档中的许多字段:

Example 59. 样本文档映射
public class Product {
  @Field
  private String simpleProperty;

  @Field("somePropertyName")
  private String namedPropery;

  @Field
  private List<String> listOfValues;

  @Indexed(readonly = true)
  @Field("property_*")
  private List<String> ignoredFromWriting;

  @Field("mappedField_*")
  private Map<String, List<String>> mappedFieldValues;

  @Dynamic
  @Field("dynamicMappedField_*")
  private Map<String, String> dynamicMappedFieldValues;

  @Field
  private GeoLocation location;

}

下表描述了可以使用 MappingSolrConverter 映射的属性:

属性 写映射

simpleProperty

<field name="simpleProperty">value</field>

namedPropery

<field name="somePropertyName">value</field>

listOfValues

<field name="listOfValues">value 1</field> <field name="listOfValues">value 2</field> <field name="listOfValues">value 3</field>

ignoredFromWriting

//not written to document

mappedFieldValues

<field name="mapentry[0].key">mapentry[0].value[0]</field> <field name="mapentry[0].key">mapentry[0].value[1]</field> <field name="mapentry[1].key">mapentry[1].value[0]</field>

dynamicMappedFieldValues

<field name="'dynamicMappedField_' + mapentry[0].key">mapentry[0].value[0]</field> <field name="'dynamicMappedField_' + mapentry[0].key">mapentry[0].value[1]</field> <field name="'dynamicMappedField_' + mapentry[1].key">mapentry[1].value[0]</field>

location

<field name="location">48.362893,14.534437</field>

你可以通过向 SolrTemplate 添加 CustomConversions 并使用你自己的 Converter 实现对其进行初始化来注册自定义转换器,如以下示例所示:

<bean id="solrConverter" class="org.springframework.data.solr.core.convert.MappingSolrConverter">
  <constructor-arg>
	  <bean class="org.springframework.data.solr.core.mapping.SimpleSolrMappingContext" />
  </constructor-arg>
  <property name="customConversions" ref="customConversions" />
</bean>

<bean id="customConversions" class="org.springframework.data.solr.core.convert.SolrCustomConversions">
  <constructor-arg>
	  <list>
		  <bean class="com.acme.MyBeanToSolrInputDocumentConverter" />
	  </list>
  </constructor-arg>
</bean>

<bean id="solrTemplate" class="org.springframework.data.solr.core.SolrTemplate">
  <constructor-arg ref="solrClient" />
  <property name="solrConverter" ref="solrConverter" />
</bean>

4. 其他Solr操作支持

本章介绍了无法通过存储库接口直接访问的Solr操作(如分面)的其他支持。建议将这些操作添加为自定义实现,如Spring Data Repositories的自定义实现中所述。

4.1. 集合/内核名称

使用 @SolrDocument 注解,可以通过为其提供静态值或使用 SpEL进行动态评估来自定义使用的集合名称。

@SolrDocument(collection = "techproducts")
class StaticCollectionName { ... }

@SolrDocument(collection = "#{@someBean.getCollectionName()}")
class DynamicCollectionName { ... }
使用 @SolrDocument 注解的类型可通过表达式中的 targetType 变量获得。

4.2. 原子更新

PartialUpdates 可以使用实现 UpdatePartialUpdate 完成。

PartialUpdate update = new PartialUpdate("id", "123");
update.add("name", "updated-name");
solrTemplate.saveBean("collection-1", update);

4.3. 投影

通过在 @Query 中使用字段值应用投影。

@Query(fields = { "name", "id" })
List<ProductBean> findByNameStartingWith(String name);

4.4. 分面

使用 SolrRepository 无法直接应用分面,但 SolrTemplate 支持此功能。以下示例显示了一个分面查询:

FacetQuery query = new SimpleFacetQuery(new Criteria(Criteria.WILDCARD).expression(Criteria.WILDCARD))
  .setFacetOptions(new FacetOptions().addFacetOnField("name").setFacetLimit(5));
FacetPage<Product> page = solrTemplate.queryForFacetPage("collection-1", query, Product.class);

字段或查询的分面也可以使用 @Facet 定义。请记住,结果是 FacetPage

使用 @Facet 可以定义使用输入参数作为值的占位符。

以下示例使用 @Facet 注解定义分面查询:

@Query(value = "*:*")
@Facet(fields = { "name" }, limit = 5)
FacetPage<Product> findAllFacetOnName(Pageable page);

以下示例显示了另一个分面查询,其前缀为:

@Query(value = "popularity:?0")
@Facet(fields = { "name" }, limit = 5, prefix="?1")
FacetPage<Product> findByPopularityFacetOnName(int popularity, String prefix, Pageable page);

Solr允许基于每个字段定义分面参数。要向定义的字段添加特殊分面选项,请使用 FieldWithFacetParameters,如以下示例所示:

// produces: f.name.facet.prefix=spring
FacetOptions options = new FacetOptions();
options.addFacetOnField(new FieldWithFacetParameters("name").setPrefix("spring"));

4.4.1. 区间分面

你可以通过在 FacetOptions 上配置所需范围来创建区间分面查询。你可以通过创建 FacetOptions 实例,将选项设置为 FacetQuery,并通过 SolrTemplate 查询分面页来请求范围,如下所示:

FacetOptions facetOptions = new FacetOptions()
  .addFacetByRange(
     new FieldWithNumericRangeParameters("price", 5, 20, 5)
       .setHardEnd(true)
       .setInclude(FacetRangeInclude.ALL)
  )
  .addFacetByRange(
    new FieldWithDateRangeParameters("release", new Date(1420070400), new Date(946684800), "+1YEAR")
      .setInclude(FacetRangeInclude.ALL)
      .setOther(FacetRangeOther.BEFORE)
  );
facetOptions.setFacetMinCount(0);

Criteria criteria = new SimpleStringCriteria("*:*");
SimpleFacetQuery facetQuery = new SimpleFacetQuery(criteria).setFacetOptions(facetOptions);
FacetPage<ExampleSolrBean> statResultPage = solrTemplate.queryForFacetPage("collection-1", facetQuery, ExampleSolrBean.class);

分面区间请求有两种字段实现:

  • Numeric Facet Range: Used to perform range faceting over numeric fields. To request range faceting, you can use an instance of the org.springframework.data.solr.core.query.FacetOptions.FieldWithNumericRangeParameters class. Its instantiation requires a field name, a start value (number), an end value (number), and a gap (number);

  • Date Facet Range: Used to perform range faceting over date fields. To request range faceting, you can use an instance of the org.springframework.data.solr.core.query.FacetOptions.FieldWithDateRangeParameters class. Its instantiation requires a field name, a start value (date), an end value (date), and a gap (string). You can define the gap for this kind of field by using org.apache.solr.util.DateMathParser (for example, +6MONTHS+3DAYS/DAY means six months and three days in the future, rounded down to the nearest day).

Additionally, the following properties can be configured for a field with range parameters (org.springframework.data.solr.core.query.FacetOptions.FieldWithRangeParameters):

  • Hard End: setHardEnd(Boolean) defines whether the last range should be abruptly ended even if the end does not satisfy (start - end) % gap = 0.

  • Include: setInclude(org.apache.solr.common.params.FacetParams.FacetRangeInclude) defines how boundaries (lower and upper) should be handled (exclusive or inclusive) on range facet requests.

  • Other: setOther(org.apache.solr.common.params.FacetParams.FacetRangeOther) defines the additional (other) counts for the range facet (such as count of documents that are before the start of the range facet, after the end of the range facet, or even between the start and the end).

4.4.2. 支点分面

还支持支点分面(决策树),可以使用 @Facet 注解查询,如下所示:

public interface {

  @Facet(pivots = @Pivot({ "category", "dimension" }, pivotMinCount = 0))
  FacetPage<Product> findByTitle(String title, Pageable page);

  @Facet(pivots = @Pivot({ "category", "dimension" }))
  FacetPage<Product> findByDescription(String description, Pageable page);

}

或者,可以使用 SolrTemplate 查询,如下所示:

FacetQuery facetQuery = new SimpleFacetQuery(new SimpleStringCriteria("title:foo"));
FacetOptions facetOptions = new FacetOptions();
facetOptions.setFacetMinCount(0);
facetOptions.addFacetOnPivot("category","dimension");
facetQuery.setFacetOptions(facetOptions);
FacetPage<Product> facetResult = solrTemplate.queryForFacetPage("collection-1", facetQuery, Product.class);

要检索数据透视结果,请使用 getPivot 方法,如下所示:

List<FacetPivotFieldEntry> pivot = facetResult.getPivot(new SimplePivotField("categories","available"));

4.5. 词项

词项向量不能直接在 SolrRepository 中使用,但可以通过 SolrTemplate 应用。请记住,结果是 TermsPage。以下示例显示如何创建词项查询:

TermsQuery query = SimpleTermsQuery.queryBuilder().fields("name").build();
TermsPage page = solrTemplate.queryForTermsPage("collection-1", query);

4.6. 结果分组/字段折叠

结果分组不能直接在 SolrRepository 中使用,但可以通过 SolrTemplate 应用。请记住,结果是 GroupPage。以下示例显示如何创建结果组:

Field field = new SimpleField("popularity");
Function func = ExistsFunction.exists("description");
Query query = new SimpleQuery("inStock:true");

SimpleQuery groupQuery = new SimpleQuery(new SimpleStringCriteria("*:*"));
GroupOptions groupOptions = new GroupOptions()
	.addGroupByField(field)
	.addGroupByFunction(func)
	.addGroupByQuery(query);
groupQuery.setGroupOptions(groupOptions);

GroupPage<Product> page = solrTemplate.queryForGroupPage("collection-1", query, Product.class);

GroupResult<Product> fieldGroup = page.getGroupResult(field);
GroupResult<Product> funcGroup = page.getGroupResult(func);
GroupResult<Product> queryGroup = page.getGroupResult(query);

4.7. 字段统计

字段统计信息用于检索Solr给定字段的统计信息(max, min, sum, count, mean, missing, stddevdistinct 计算)。你可以为查询提供 StatsOptions,并从返回的 StatsPage 中读取 FieldStatsResult。例如,你可以使用 SolrTemplate,如下所示:

// simple field stats
StatsOptions statsOptions = new StatsOptions().addField("price");

// query
SimpleQuery statsQuery = new SimpleQuery("*:*");
statsQuery.setStatsOptions(statsOptions);
StatsPage<Product> statsPage = solrTemplate.queryForStatsPage("collection-1", statsQuery, Product.class);

// retrieving stats info
FieldStatsResult priceStatResult = statResultPage.getFieldStatsResult("price");
Object max = priceStatResult.getMax();
Long missing = priceStatResult.getMissing();

你可以通过使用 @Stats 注解存储库方法来实现相同的结果,如下所示:

@Query("name:?0")
@Stats(value = { "price" })
StatsPage<Product> findByName(String name, Pageable page);

还支持不同的计算和faceting:

// for distinct calculation
StatsOptions statsOptions = new StatsOptions()
    .addField("category")
    // for distinct calculation
    .setCalcDistinct(true)
    // for faceting
    .addFacet("availability");

// query
SimpleQuery statsQuery = new SimpleQuery("*:*");
statsQuery.setStatsOptions(statsOptions);
StatsPage<Product> statsPage = solrTemplate.queryForStatsPage("collection-1", statsQuery, Product.class);

// field stats
FieldStatsResult categoryStatResult = statResultPage.getFieldStatsResult("category");

// retrieving distinct
List<Object> categoryValues = priceStatResult.getDistinctValues();
Long distinctCount = categoryStatResult.getDistinctCount();

// retrieving faceting
Map<String, StatsResult> availabilityFacetResult = categoryStatResult.getFacetStatsResult("availability");
Long availableCount = availabilityFacetResult.get("true").getCount();

前面示例的带注解(并因此更短)的版本如下:

@Query("name:?0")
@Stats(value = "category", facets = { "availability" }, calcDistinct = true)
StatsPage<Product> findByName(String name);

为了执行选择性faceting或选择性不同的计算,你可以使用 @SelectiveStats,如下所示:

// selective distinct faceting
...
Field facetField = getFacetField();
StatsOptions statsOptions = new StatsOptions()
    .addField("price")
    .addField("category").addSelectiveFacet("name").addSelectiveFacet(facetField);
...
// or annotating repository method as follows
...
@Stats(value = "price", selective = @SelectiveStats(field = "category", facets = { "name", "available" }))
...

// selective distinct calculation
...
StatsOptions statsOptions = new StatsOptions()
    .addField("price")
    .addField("category").setSelectiveCalcDistinct(true);
...
// or annotating repository method as follows
...
@Stats(value = "price", selective = @SelectiveStats(field = "category", calcDistinct = true))
...

4.8. 过滤查询

过滤查询可提高查询速度,但不会影响文档分数。我们建议将地理空间搜索实现为过滤查询。

在Solr中,除非另有说明,否则所有距离单位均为千米,点数为纬度和经度。

以下示例显示了地理点的过滤查询(在本例中为奥地利):

Query query = new SimpleQuery(new Criteria("category").is("supercalifragilisticexpialidocious"));
FilterQuery fq = new SimpleFilterQuery(new Criteria("store")
  .near(new Point(48.305478, 14.286699), new Distance(5)));
query.addFilterQuery(fq);

你还可以使用 @Query 定义简单的过滤查询。

使用 @Query 可以定义使用输入参数作为值的占位符。

以下示例显示了带占位符的查询(:):

@Query(value = "*:*", filters = { "inStock:true", "popularity:[* TO 3]" })
List<Product> findAllFilterAvailableTrueAndPopularityLessThanEqual3();

4.9. 搜索完成所允许的时间

你可以设置搜索完成所允许的时间。此值仅适用于搜索,而不适用于一般的请求。时间以毫秒为单位。小于或等于零的值意味着没有时间限制。如果有的话,可能会返回部分结果。以下示例将搜索时间限制为100毫秒:

Query query = new SimpleQuery(new SimpleStringCriteria("field_1:value_1"));
// Allowing maximum of 100ms for this search
query.setTimeAllowed(100);

4.10. 提升文档分数

你可以提高匹配条件的文档分数以影响结果顺序。你可以通过设置 Criteria 上的boost或使用 @Boost 来获取派生查询来实现。以下示例提升了 findByNameOrDescription 查询的 name 参数:

Page<Product> findByNameOrDescription(@Boost(2) String name, String description);

4.10.1. 索引时间提升

基于文档和基于字段的索引时间提升已从Apache Solr 7中删除,因此也从Apache Solr 4.x的Spring Data中删除。

4.11. 选择请求处理器

你可以直接在 Query 中通过 qt 参数选择请求处理器,或者在方法签名中添加 @Query。以下示例通过添加 @Query 来实现:

@Query(requestHandler = "/instock")
Page<Product> findByNameOrDescription(String name, String description);

4.12. 使用连接

你可以通过定义 QueryJoin 属性在一个Solr核心中使用连接。

在Solr 4.x之前无法使用连接。

以下示例显示了如何使用连接:

SimpleQuery query = new SimpleQuery(new SimpleStringCriteria("text:ipod"));
query.setJoin(Join.from("manu_id_s").to("id"));

4.13. 高亮显示

要高亮显示搜索结果中的匹配项,可以将 HighlightOptions 添加到 SimpleHighlightQuery。提供没有任何其他属性的 HighlightOptions 会在 SolrDocument 中的所有字段上高亮显示。

你可以通过将 FieldWithHighlightParameters 添加到 HighlightOptions 来设置特定于字段的高亮参数。

以下示例为查询中的所有字段设置高亮显示:

SimpleHighlightQuery query = new SimpleHighlightQuery(new SimpleStringCriteria("name:with"));
query.setHighlightOptions(new HighlightOptions());
HighlightPage<Product> page = solrTemplate.queryForHighlightPage("collection-1", query, Product.class);

并非所有参数都可通过setter和getter获得,但可以直接添加。

以下示例在两个字段上设置高亮显示:

SimpleHighlightQuery query = new SimpleHighlightQuery(new SimpleStringCriteria("name:with"));
query.setHighlightOptions(new HighlightOptions().addHighlightParameter("hl.bs.country", "at"));

要将高亮显示应用于派生查询,可以使用 @Highlight。如果没有定义 fields,则高亮显示应用于所有字段。

@Highlight(prefix = "<b>", postfix = "</b>")
HighlightPage<Product> findByName(String name, Pageable page);

4.14. 拼写检查

拼写检查根据实际查询提供搜索词建议。有关详细信息,请参阅 Solr指南

4.14.1. 拼写检查选项

设置 SpellcheckOptions 后,拼写检查查询参数将添加到请求中,如以下示例所示:

SimpleQuery q = new SimpleQuery("name:gren");
q.setSpellcheckOptions(SpellcheckOptions.spellcheck()               (1)
  .dictionaries("dict1", "dict2")                                   (2)
  .count(5)                                                         (3)
  .extendedResults());                                              (4)
q.setRequestHandler("/spell");                                      (5)

SpellcheckedPage<Product> found = template.query(q, Product.class); (6)
1 通过设置 SpellcheckOptions 启用拼写检查。设置 spellcheck = on 请求参数。
2 设置用于查找的词典。
3 设置要返回的最大建议数。
4 启用扩展结果,包括术语频率等。
5 设置请求处理器,该处理程序必须能够处理建议。
6 运行查询。

4.14.2. @Spellcheck

@Spellcheck 注解允许在存储库级别使用拼写检查功能。以下示例显示了如何使用它:

public interface ProductRepository extends Repository<Product, String> {

  @Query(requestHandler = "/spell")
  @Spellcheck(dictionaries = { "dict1", "dic2" }, count=5, extendedResults = true)
  SpellcheckedPage<Product> findByName(String name, Pageable page);

}

4.15. 使用函数

Solr支持查询中的几个函数表达式,并包含许多函数。你可以通过实现 Function 来添加自定义函数。下表列出了支持的功能:

Table 3. 函数
Solr函数

CurrencyFunction

currency(field_name,[CODE])

DefaultValueFunction

def(field|function,defaultValue)

DistanceFunction

dist(power, pointA, pointB)

DivideFunction

div(x,y)

ExistsFunction

exists(field|function)

GeoDistanceFunction

geodist(sfield, latitude, longitude)

GeoHashFunction

geohash(latitude, longitude)

IfFunction

if(value|field|function,trueValue,falseValue)

MaxFunction

max(field|function,value)

NotFunction

not(field|function)

ProductFunction

product(x,y,…​)

QueryFunction

query(x)

TermFrequencyFunction

termfreq(field,term)

以下示例使用 QueryFunction

SimpleQuery query = new SimpleQuery(new SimpleStringCriteria("text:ipod"));
query.addFilterQuery(new FilterQuery(Criteria.where(QueryFunction.query("name:sol*"))));

4.16. 实时获取

实时获取允许使用唯一密钥检索任何文档的最新版本,而无需重新打开搜索者。

实时获取依赖于更新日志功能。

以下示例显示了实时获取:

Example 60. 实时获取
Optional<Product> product = solrTemplate.getById("collection-1", "123", Product.class);

你可以通过提供一组 ids 来检索多个文档,如下所示:

Example 61. 实时多重获取
Collection<String> ids = Arrays.asList("123", "134");
Collection<Product> products = solrTemplate.getByIds("collection-1", ids, Product.class);

4.17. 特殊字段

Solr包括许多特殊字段,包括得分字段。

4.17.1. @Score

为了加载查询结果的得分信息,你可以添加一个带有 @Score 注解的字段,指示该属性包含文档的得分。

得分属性需要是数字,并且每个文档只能出现一次。

以下示例显示了包含得分字段的文档:

public class MyEntity {

    @Id
    private String id;

    @Score
    private Float score;

    // setters and getters ...

}

4.18. 嵌套文档

嵌套文档允许父子关系中的其他文档内的文档。

嵌套文档需要与父文档一起编制索引,并且不能单独更新。但是,嵌套文档在索引中显示为单个文档。解析父子关系是在查询时完成的。

要指示属性应被视为嵌套对象,必须使用 @o.a.s.c.solrj.beans.Field(child=true)@o.s.d.s.core.mapping.ChildDocument 进行注解。以下示例使用 @ChildDocument 注解:

public class Book {

    @Id String id;
    @Indexed("type_s") String type;
    @Indexed("title_t") String title;
    @Indexed("author_s") String author;
    @Indexed("publisher_s") String publisher;

    @ChildDocument List<Review> reviews;      (1)

    // setters and getters ...

}

public class Review {

    @Id String id;                            (2)
    @Indexed("type_s") String type;
    @Indexed("review_dt") Date date;
    @Indexed("stars_i") int stars;
    @Indexed("author_s") String author;
    @Indexed("comment_t") String comment;

}
1 多个子文档可以与父文档关联,或使用域类型存储单个关系。
2 请注意,嵌套文档还需要分配唯一的 id

假设 Book#typebookReview#type 解析为 review,可以通过改变 fl 查询参数来检索 book 及其子关系 reviews,如下例所示:

Query query = new SimpleQuery(where("id").is("theWayOfKings"));
query.addProjectionOnField(new SimpleField("*"));
query.addProjectionOnField(new SimpleField("[child parentFilter=type_s:book]")); (1)

return solrTemplate.queryForObject("books", query, Book.class);
1 父过滤器始终在索引中定义完整的父文档集,而不是单个文档的父文档。

5. 附录

5.1. 命名空间参考

5.1.1. <repositories/> 元素

<repositories/> 元素触发Spring Data存储库基础结构的设置。最重要的属性是 base-package, 它定义了扫描Spring Data存储库接口的包。参考 XML配置。下表描述了 <repositories/> 元素的属性:

Table 4. 属性
名字 描述

base-package

定义要扫描的存储库接口的包,该存储库接口继承 *Repository (实际接口由特定的Spring Data模块确定)。 也会扫描配置包下面的所有包。允许使用通配符。

repository-impl-postfix

定义后缀以自动检测自定义存储库实现。名称以配置的后缀结尾的类被视为候选人。默认后缀为 Impl

query-lookup-strategy

确定用于创建查询的策略。有关详细信息,请参考 查询查找策略。默认为 create-if-not-found

named-queries-location

定义搜索的包含外部定义查询的Properties文件的位置。

consider-nested-repositories

是否应考虑嵌套存储库接口定义。默认为 false

5.2. Populators命名空间参考

5.2.1. <populator/> 元素

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

Table 5. 属性
名字 描述

locations

用于填充存储库的对象的值的文件位置。

5.3. 存储库查询关键字

5.3.1. 支持的查询关键字

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

Table 6. 查询关键字
逻辑关键字 关键字表达式

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

Is, Equals, (or no keyword)

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

5.4. 存储库查询返回类型

5.4.1. 支持的查询返回类型

下表列出了Spring Data存储库通常支持的返回类型。但是,请查阅特定于存储库的文档以获取支持的返回类型的确切列表, 因为特定存储库可能不支持此处列出的某些类型。

地理空间类型(例如 GeoResultGeoResultsGeoPage)仅适用于支持地理空间查询的数据存储。
Table 7. 查询返回类型

返回类型

描述

void

表示没有返回值。

Primitives

Java原语。

Wrapper types

Java包装器类型。

T

一个特定的实体。期望查询方法最多返回一个结果。如果未找到结果,则返回 null。 多个结果会触发 IncorrectResultSizeDataAccessException

Iterator<T>

迭代器。

Collection<T>

集合。

List<T>

列表。

Optional<T>

Java 8或Guava Optional。期望查询方法最多返回一个结果。如果未找到结果, 则返回 Optional.empty()Optional.absent()。多个结果会触发 IncorrectResultSizeDataAccessException

Option<T>

Scala或Javaslang Option 类型。语义上与前面描述的Java 8的 Optional 相同。

Stream<T>

Java 8流 Stream

Future<T>

Future。期望使用 @Async 注解的方法,并且需要启用Spring的异步方法执行功能。

CompletableFuture<T>

Java 8 CompletableFuture。期望使用 @Async 注解的方法,并且需要启用Spring的异步方法执行功能。

ListenableFuture

org.springframework.util.concurrent.ListenableFuture。 期望使用 @Async 注解的方法,并且需要启用Spring的异步方法执行功能。

Slice

数据块切片,指示是否有更多数据可用。需要 Pageable 方法参数。

Page<T>

带有附加信息的切片,例如结果总数。需要 Pageable 方法参数。

GeoResult<T>

带有附加信息的结果条目,例如到参考位置的距离。

GeoResults<T>

带有附加信息的 GeoResult<T> 列表,例如到参考位置的平均距离。

GeoPage<T>

包含 GeoResult<T> 的页,例如到参考位置的平均距离。

Mono<T>

Project Reactor Mono 使用响应式存储库发射零或一个元素 。期望查询方法最多返回一个结果。 如果未找到结果,则返回 Mono.empty()。多个结果会触发 IncorrectResultSizeDataAccessException

Flux<T>

Project Reactor Flux 使用响应式存储库发射零个,一个或多个元素。返回 Flux 的查询也可以发出无限数量的元素。

Single<T>

RxJava Single 使用响应式存储库发射单个元素。期望查询方法最多返回一个结果。如果未找到结果,则返回 Mono.empty()。 多个结果会触发 IncorrectResultSizeDataAccessException

Maybe<T>

RxJava Maybe 使用响应式存储库发射零个或一个元素。期望查询方法最多返回一个结果。如果未找到结果, 则返回 Mono.empty()。多个结果会触发 IncorrectResultSizeDataAccessException

Flowable<T>

RxJava Flowable 使用响应式存储库发射零个,一个或多个元素。返回 Flowable 的查询也可以发出无限数量的元素。


1. 请参阅 XML配置