前言

Spring Data JPA为Java Persistence API(JPA)提供存储库支持。它使访问JPA数据源的应用程序开发变得更加简单。

1. 项目信息

2. 新特性和值得注意的东西

2.1. Spring Data JPA 1.11中的新功能

  • 改进了与Hibernate 5.2的兼容性。

  • 支持 按示例查询的任意匹配模式。

  • 分页查询执行优化。

  • 支持存储库查询派生中的 exists 投影。

2.2. Spring Data JPA 1.10中的新功能

  • 支持存储库查询方法中的 投影

  • 支持 按示例查询

  • 已启用以下注解以构建组合注解:@EntityGraph, @Lock, @Modifying, @Query, @QueryHints@Procedure

  • 支持集合表达式上的 Contains 关键字。

  • 增加JSR-310和ThreeTenBP的 ZoneIdAttributeConverter 实现。

  • 升级到Querydsl 4,Hibernate 5,OpenJPA 2.4和EclipseLink 2.6.1。

由于各个Spring Data模块的创建日期不同,因此大多数模块都带有不同的主,次要版本号。找到兼容版本的最简单方法是依赖我们 提供的Spring Data Release Train BOM。在Maven项目中,你将在POM的 <dependencyManagement/> 部分中声明此依赖项,如下所示:

Example 1. 使用Spring Data Release Train BOM
<dependencyManagement>
  <dependencies>
    <dependency>
      <groupId>org.springframework.data</groupId>
      <artifactId>spring-data-releasetrain</artifactId>
      <version>${release-train}</version>
      <scope>import</scope>
      <type>pom</type>
    </dependency>
  </dependencies>
</dependencyManagement>

目前的发布版本是 Kay-SR9。列车名称按字母顺序上升,此处列出了当前可用的列车。 版本名称遵循以下模式:${name}-${release},其中release可以是以下之一:

  • BUILD-SNAPSHOT: 当前快照版

  • M1, M2等:里程碑版

  • RC1, RC2等:发行版候选人

  • RELEASE: GA发行版

  • SR1, SR2等:服务发行版

可以在 Spring Data示例存储库中找到使用BOM的工作示例。 有了这个,你可以在 <dependencies/> 块中声明你想要使用的Spring Data模块而不需要版本,如下所示:

Example 2. 声明对Spring Data模块的依赖
<dependencies>
  <dependency>
    <groupId>org.springframework.data</groupId>
    <artifactId>spring-data-jpa</artifactId>
  </dependency>
<dependencies>

2.3. Spring Boot的依赖管理

Spring Boot为你选择最新版本的Spring Data模块。如果你仍想升级到更新版本,请将属性 spring-data-releasetrain.version 配置为你要使用的列车名称和迭代版本。

2.4. Spring框架

当前版本的Spring Data模块需要版本5.0.8.RELEASE或更高版本的Spring框架。这些模块也可以使用该次要版本的旧版本。 但是,强烈建议使用该代中的最新版本。

3. 使用Spring Data存储库

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

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

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

3.1. 核心概念

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

Example 3. 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 4. 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 5. 派生计数查询
interface UserRepository extends CrudRepository<User, Long> {

  long countByLastname(String lastname);
}

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

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

  long deleteByLastname(String lastname);

  List<User> removeByLastname(String lastname);
}

3.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");
      }
    }

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

3.3. 定义存储库接口

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

3.3.1. 微调存储库定义

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

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

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

Example 7. 有选择地暴露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不应在运行时创建该存储库接口的实例。

3.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 检查,如下所示:

  • @NonNullApi: 在包级别上使用, 以声明参数和返回值的默认行为是不接受或生成 null 值。

  • @NonNull: 用于不能为 null 的参数或返回值 (对于 @NonNullApi 适用的参数和返回值则不需要再加)。

  • @Nullable: 用于可以为 null 的参数或返回值。

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

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

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

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

Example 9. 使用不同的可空性约束
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 10. 在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

3.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 11. 使用特定于模块的接口的存储库定义
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 12. 使用通用接口的存储库定义
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 13. 使用带注解的域类的存储库
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 14. 使用具有混合注解的域类的存储库定义
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 15. 注解驱动的基础包配置
@EnableJpaRepositories(basePackages = "com.acme.repositories.jpa")
@EnableMongoRepositories(basePackages = "com.acme.repositories.mongo")
interface Configuration { }

3.4. 定义查询方法

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

  • 从方法名称派生查询。

  • 使用手动定义的查询。

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

3.4.1. 查询查找策略

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

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

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

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

3.4.2. 查询创建

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

Example 16. 从方法名称创建查询
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)来应用静态排序。 要创建支持动态排序的查询方法,请参阅 特殊参数处理

3.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命名约定(即,不在属性名称中使用下划线,而使用驼峰)。

3.4.4. 特殊参数处理

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

Example 17. 在查询方法中使用 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。在这种情况下,不会创建构建实际分页实例所需的其他元数据(这反过来意味着它不会发出必要的附加计数查询)。 相反,它限制查询仅查找给定范围的实体。

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

3.4.5. 限制查询结果

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

Example 18. 使用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”个元素的查询方法。

3.4.6. 流式查询结果

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

Example 19. 使用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 20. 使用try-with-resources块关闭 Stream<T>
try (Stream<User> stream = repository.findAllByCustomQueryAndStream()) {
  stream.forEach(…);
}
当前并非所有Spring Data模块都支持 Stream<T> 作为返回类型。

3.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 作为返回类型。

3.5. 创建存储库实例

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

3.5.1. XML配置

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

Example 21. 通过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 22. 使用exclude-filter元素
<repositories base-package="com.acme.repositories">
  <context:exclude-filter type="regex" expression=".*SomeRepository" />
</repositories>

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

3.5.2. Java配置

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

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

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

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

3.5.3. 独立使用

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

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

3.6. Spring Data Repositories的自定义实现

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

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

3.6.1. 自定义单个存储库

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

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

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

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

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

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

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

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

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

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

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

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

Example 28. 片段与它们的实现
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 29. 存储库接口的更改
interface UserRepository extends CrudRepository<User, Long>, HumanRepository, ContactRepository {

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

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

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

Example 30. 片段覆盖 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 31. 自定义存储库接口
interface UserRepository extends CrudRepository<User, Long>, CustomizedSave<User> {
}

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

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

Example 32. 配置示例
<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 33. 解决有歧义的多个实现
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 34. 手动接线自定义实现
<repositories base-package="com.acme.repository" />

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

3.6.2. 自定义基础Repository

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

Example 35. 自定义存储库基类
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 36. 使用JavaConfig配置自定义存储库基类
@Configuration
@EnableJpaRepositories(repositoryBaseClass = MyRepositoryImpl.class)
class ApplicationConfiguration { … }

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

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

3.7. 从聚合根发布事件

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

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

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

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

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

3.8. Spring Data扩展

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

3.8.1. Querydsl扩展

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

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

Example 39. 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 40. 在存储库中集成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);

3.8.2. Web支持

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

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

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

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

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

Example 42. 以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 43. 在方法签名中使用域类型的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 44. 使用 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 45. 使用 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 46. 使用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

有关更多信息,请参阅 Spring Data Examples典范存储库中的 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 属性。

3.8.3. 存储库填充

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

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

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

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

Example 48. 声明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 49. 使用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>

4. JPA存储库

本章指出JPA存储库支持的特性。这建立在“使用Spring Data存储库”中解释的核心存储库支持的基础上。 确保你对那里解释的基本概念有充分的理解。

4.1. 介绍

本节介绍通过以下任一方式配置Spring Data JPA的基础知识:

4.1.1. Spring命名空间

Spring Data的JPA模块包含一个允许定义存储库bean的自定义命名空间。它还包含JPA特有的某些功能和元素属性。 通常,可以使用 repositories 元素设置JPA存储库,如以下示例所示:

Example 50. 使用命名空间设置JPA存储库
<?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>

使用 repositories 元素查找Spring Data存储库,如“创建存储库实例”中所述。 除此之外,它还激活了使用 @Repository 注解的所有bean的持久性异常转换,以便将JPA持久性提供程序所抛出的异常转换为Spring的 DataAccessException 层次结构。

自定义命名空间属性

除了 repositories 元素的默认属性之外,JPA命名空间还提供了其他属性,使你可以更好地控制存储库的设置:

Table 2. repositories 元素的自定义JPA特定属性

entity-manager-factory-ref

显式声明要与 repositories 元素检测到的存储库一起使用的 EntityManagerFactory。 通常在应用程序中使用多个 EntityManagerFactory bean时使用。如果未配置,Spring Data会自动在 ApplicationContext 中查找名称为 entityManagerFactoryEntityManagerFactory bean。

transaction-manager-ref

显式声明要与 repositories 元素检测到的存储库一起使用的 PlatformTransactionManager。 通常仅在配置了多个事务管理器或 EntityManagerFactory bean时才需要。 默认为当前 ApplicationContext 中单个定义的 PlatformTransactionManager

如果没有定义显式的 transaction-manager-ref,Spring Data JPA需要一个名为 transactionManagerPlatformTransactionManager bean。

4.1.2. 基于注解的配置

Spring Data JPA存储库支持不仅可以通过XML命名空间激活,还可以通过JavaConfig使用注解来激活,如以下示例所示:

Example 51. 使用JavaConfig的Spring Data JPA存储库
@Configuration
@EnableJpaRepositories
@EnableTransactionManagement
class ApplicationConfig {

  @Bean
  public DataSource dataSource() {
    EmbeddedDatabaseBuilder builder = new EmbeddedDatabaseBuilder();
    return builder.setType(EmbeddedDatabaseType.HSQL).build();
  }

  @Bean
  public LocalContainerEntityManagerFactoryBean entityManagerFactory() {
    HibernateJpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
    vendorAdapter.setGenerateDdl(true);

    LocalContainerEntityManagerFactoryBean factory = new LocalContainerEntityManagerFactoryBean();
    factory.setJpaVendorAdapter(vendorAdapter);
    factory.setPackagesToScan("com.acme.domain");
    factory.setDataSource(dataSource());
    return factory;
  }

  @Bean
  public PlatformTransactionManager transactionManager(EntityManagerFactory entityManagerFactory) {
    JpaTransactionManager txManager = new JpaTransactionManager();
    txManager.setEntityManagerFactory(entityManagerFactory);
    return txManager;
  }
}
你必须直接创建 LocalContainerEntityManagerFactoryBean 而不是 EntityManagerFactory, 因为除了创建 EntityManagerFactory 之外,前者还包含了异常转换机制。

上述配置类使用 spring-jdbcEmbeddedDatabaseBuilder API设置数据源为嵌入式HSQL数据库。 然后Spring Data设置一个 EntityManagerFactory 并使用Hibernate作为样例的持久性提供程序。 这里声明的最后一个基础组件是 JpaTransactionManager。最后,该示例使用 @EnableJpaRepositories 注解激活Spring Data JPA存储库, 该注解基本上具有与XML命名空间相同的属性。如果未配置基础包,则使用配置类所在包作为基础包。

4.2. 持久化实体

本节介绍如何使用Spring Data JPA持久化(保存)实体。

4.2.1. 保存实体

可以使用 CrudRepository.save(…​) 方法来保存实体。它通过使用基础JPA EntityManager 来持久化或合并给定实体。 如果实体尚未持久化,则Spring Data JPA会通过调用 entityManager.persist(…​) 方法来保存实体。否则, 它调用 entityManager.merge(…​) 方法。

实体状态 - 检测策略

Spring Data JPA提供以下策略来检测实体是否是新实体:

  • Id-属性检查(默认):默认情况下,Spring Data JPA会检查给定实体的标识符属性。 如果id属性为 null,则假定该实体是新的。否则,它被认为不是新的。

  • 实现 Persistable:如果实体实现了 Persistable,Spring Data JPA会将检测委托给实体的 isNew(…​) 方法。 有关详细信息,请参阅 JavaDoc

  • 实现 EntityInformation:你可以通过创建 JpaRepositoryFactory 的子类并相应地覆盖 getEntityInformation(…​) 方法来自定义 SimpleJpaRepository 实现中使用的 EntityInformation 抽象。然后,你必须将 JpaRepositoryFactory 的自定义实现注册为 Spring bean。请注意,这种方案很少是必要的。有关详细信息,请参阅 JavaDoc

4.3. 查询方法

本节介绍使用Spring Data JPA创建查询的各种方法。

4.3.1. 查询查找策略

JPA模块支持手动定义查询字符串或从方法名称派生查询。

声明式查询

尽管获取从方法名称派生的查询非常方便,但是可能面临这样的情况:方法名称解析器不支持要使用的关键字,或者方法名称会变得不必要地丑陋。 因此,你可以通过命名约定使用JPA命名查询(请参阅 使用JPA命名查询 获取更多信息), 或者使用 @Query 注解查询方法(有关详细信息,请参阅 使用 @Query)。

4.3.2. 查询创建

通常,JPA的查询创建机制的工作方式如“查询方法”中所述。以下示例显示了JPA查询方法转换为的内容:

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

  List<User> findByEmailAddressAndLastname(String emailAddress, String lastname);
}

我们使用JPA标准API创建一个查询,但实际上,这会转换为以下查询:select u from User u where u.emailAddress = ?1 and u.lastname = ?2。 Spring Data JPA执行属性检查并遍历嵌套属性,如“属性表达式”中所述。

下表描述了JPA支持的关键字以及包含该关键字的方法将转换为:

Table 3. 方法名称中支持的关键字

关键字

查询方法名

JPQL 代码段

And

findByLastnameAndFirstname

… where x.lastname = ?1 and x.firstname = ?2

Or

findByLastnameOrFirstname

… where x.lastname = ?1 or x.firstname = ?2

Is,Equals

findByFirstname,findByFirstnameIs,findByFirstnameEquals

… where x.firstname = ?1

Between

findByStartDateBetween

… where x.startDate between ?1 and ?2

LessThan

findByAgeLessThan

… where x.age < ?1

LessThanEqual

findByAgeLessThanEqual

… where x.age <= ?1

GreaterThan

findByAgeGreaterThan

… where x.age > ?1

GreaterThanEqual

findByAgeGreaterThanEqual

… where x.age >= ?1

After

findByStartDateAfter

… where x.startDate > ?1

Before

findByStartDateBefore

… where x.startDate < ?1

IsNull

findByAgeIsNull

… where x.age is null

IsNotNull,NotNull

findByAge(Is)NotNull

… where x.age not null

Like

findByFirstnameLike

… where x.firstname like ?1

NotLike

findByFirstnameNotLike

… where x.firstname not like ?1

StartingWith

findByFirstnameStartingWith

… where x.firstname like ?1 (parameter bound with appended %)

EndingWith

findByFirstnameEndingWith

… where x.firstname like ?1 (parameter bound with prepended %)

Containing

findByFirstnameContaining

… where x.firstname like ?1 (parameter bound wrapped in %)

OrderBy

findByAgeOrderByLastnameDesc

… where x.age = ?1 order by x.lastname desc

Not

findByLastnameNot

… where x.lastname <> ?1

In

findByAgeIn(Collection<Age> ages)

… where x.age in ?1

NotIn

findByAgeNotIn(Collection<Age> ages)

… where x.age not in ?1

True

findByActiveTrue()

… where x.active = true

False

findByActiveFalse()

… where x.active = false

IgnoreCase

findByFirstnameIgnoreCase

… where UPPER(x.firstame) = UPPER(?1)

InNotIn 也将 Collection 的任何子类作为参数以及数组或可变参数。 对于同一逻辑运算符的其他语法版本,请参阅“存储库查询关键字”。

4.3.3. 使用JPA命名查询

这些示例使用 <named-query/> 元素和 @NamedQuery 注解。必须在JPA查询语言中定义这些配置元素所对应的查询。 当然,你也可以使用 <named-native-query/>@NamedNativeQuery。这两个元素允许你通过失去数据库平台独立性来定义本地查询SQL。
XML命名查询定义

要使用XML配置,请将必要的 <named-query/> 元素添加到位于类路径的 META-INF 文件夹中的 orm.xml JPA配置文件中。 通过使用某些已定义的命名约定,可以自动调用命名查询。有关详细信息,请参阅下文。

Example 53. XML命名查询配置
<named-query name="User.findByLastname">
  <query>select u from User u where u.lastname = ?1</query>
</named-query>

该查询具有一个特殊名称,用于在运行时解析它。

基于注解的配置

基于注解的配置具有不需要编辑另一个配置文件的优点,从而降低了维护工作量。为此,你需要为每个新添加的查询声明重新编译域类。

Example 54. 基于注解的命名查询配置
@Entity
@NamedQuery(name = "User.findByEmailAddress",
  query = "select u from User u where u.emailAddress = ?1")
public class User {

}
声明接口

要允许执行这些命名查询,请按下示指定 UserRepository

Example 55. 在 UserRepository 中声明查询方法
public interface UserRepository extends JpaRepository<User, Long> {

  List<User> findByLastname(String lastname);

  User findByEmailAddress(String emailAddress);
}

Spring Data尝试将对这些方法的调用解析为命名查询,从配置的域类的简单名称开始,后跟由点分隔的方法名称。 因此,前面的示例将使用在样例中定义的命名查询,而不是尝试从方法名称创建查询。

4.3.4. 使用 @Query

使用命名查询来声明实体查询是一种有效的方法,适用于少量查询。由于查询本身与执行它们的Java方法相关联, 因此你实际上可以使用Spring Data JPA @Query 注解直接绑定它们,而不是将它们注释到域类。 这便于将域类从特定于持久性的信息中释放出来,并将查询与存储库接口放置在一起。

注释到查询方法的查询优先于使用 @NamedQuery 定义的查询或在 orm.xml 中声明的命名查询。

以下示例显示使用 @Query 注解创建的查询:

Example 56. 使用 @Query 在查询方法中声明查询
public interface UserRepository extends JpaRepository<User, Long> {

  @Query("select u from User u where u.emailAddress = ?1")
  User findByEmailAddress(String emailAddress);
}
使用高级LIKE表达式

使用 @Query 创建手动定义查询的查询执行机制,允许在查询定义中定义高级 LIKE 表达式,如以下示例所示:

Example 57. @Query 中的高级 LIKE 表达式
public interface UserRepository extends JpaRepository<User, Long> {

  @Query("select u from User u where u.firstname like %?1")
  List<User> findByFirstnameEndsWith(String firstname);
}

在前面的示例中,将识别 LIKE 分隔符(%),并将查询转换为有效的JPQL查询(删除 %)。 在执行查询时,传递给方法调用的参数将使用之前识别的 LIKE 模式进行扩充。

本地查询

@Query 注解允许通过将 nativeQuery 标志设置为 true 来运行本地查询,如以下示例所示:

Example 58. 使用 @Query 在查询方法中声明本地查询
public interface UserRepository extends JpaRepository<User, Long> {

  @Query(value = "SELECT * FROM USERS WHERE EMAIL_ADDRESS = ?1", nativeQuery = true)
  User findByEmailAddress(String emailAddress);
}
Spring Data JPA目前不支持对本地查询进行动态排序,因为它必须操纵声明的实际查询,而对于本地SQL,它无法可靠地执行。 但是,你可以通过自己指定计数查询来使用本地查询进行分页,如以下示例所示:
Example 59. 使用 @Query 在查询方法中声明分页的本地计数查询
public interface UserRepository extends JpaRepository<User, Long> {

  @Query(value = "SELECT * FROM USERS WHERE LASTNAME = ?1",
    countQuery = "SELECT count(*) FROM USERS WHERE LASTNAME = ?1",
    nativeQuery = true)
  Page<User> findByLastname(String lastname, Pageable pageable);
}

类似的方法也适用于本地命名查询,方法是将 .count 后缀添加到复制的查询方法名中。但是,你可能需要为计数查询注册结果集映射。

4.3.5. 使用排序

可以通过提供 PageRequest 或直接使用 Sort 来进行排序。SortOrder 实例中实际使用的属性需要与你的域模型匹配, 这意味着它们需要解析为查询中使用的属性或别名。JPQL将其定义为状态字段路径表达式。

使用任何不可引用的路径表达式会导致 Exception

但是,与 @Query 一起使用的 Sort 中可以潜入 ORDER BY 子句中包含函数的无需路径检查的 Order 实例。 这是可能的,因为 Order 被附加到给定的查询字符串。默认情况下,Spring Data JPA拒绝任何包含函数调用的 Order 实例, 但你可以使用 JpaSort.unsafe 明确添加可能并不安全的排序。

以下示例使用 SortJpaSort,包括 JpaSort 上的不安全选项:

Example 60. 使用 SortJpaSort
public interface UserRepository extends JpaRepository<User, Long> {

  @Query("select u from User u where u.lastname like ?1%")
  List<User> findByAndSort(String lastname, Sort sort);

  @Query("select u.id, LENGTH(u.firstname) as fn_len from User u where u.lastname like ?1%")
  List<Object[]> findByAsArrayAndSort(String lastname, Sort sort);
}

repo.findByAndSort("lannister", new Sort("firstname"));               (1)
repo.findByAndSort("stark", new Sort("LENGTH(firstname)"));           (2)
repo.findByAndSort("targaryen", JpaSort.unsafe("LENGTH(firstname)")); (3)
repo.findByAsArrayAndSort("bolton", new Sort("fn_len"));              (4)
1 有效的 Sort 表达式指向域模型中的属性。
2 包含函数调用的无效排序。抛出异常。
3 有效的 Sort 明确包含 不安全Order
4 有效的 Sort 表达式指向别名函数。

4.3.6. 使用命名参数

默认情况下,Spring Data JPA使用基于位置的参数绑定,如前面所有示例中所述。这使得查询方法在重构参数位置时容易出错。 要解决此问题,可以使用 @Param 注解为方法参数指定具体名称并在查询中绑定名称,如以下示例所示:

Example 61. 使用命名参数
public interface UserRepository extends JpaRepository<User, Long> {

  @Query("select u from User u where u.firstname = :firstname or u.lastname = :lastname")
  User findByLastnameOrFirstname(@Param("lastname") String lastname,
                                 @Param("firstname") String firstname);
}
方法参数根据定义查询中的顺序进行了调整。
从版本4开始,Spring完全支持基于 -parameters 编译器标志的Java 8参数名称发现。 通过在构建中使用此标志作为调试信息的替代方法,可以省略命名参数的 @Param 注解。

4.3.7. 使用SpEL表达式

从Spring Data JPA 1.4版开始,我们支持在使用 @Query 定义的手动定义查询中使用受限制的SpEL模板表达式。 在执行查询时,将根据预定义的变量集评估这些表达式。Spring Data JPA支持名为 entityName 的变量。 它的用法是 select x from #{#entityName} x。它插入与给定存储库关联的域类型的 entityNameentityName 的解析如下:如果域类型在 @Entity 注释上设置了 name 属性,它会被使用。否则,使用域类型的简单类名。

以下示例演示了查询字符串中 #{#entityName} 表达式的一个使用场景,你希望使用查询方法和手动定义查询语句来定义存储库接口:

Example 62. 在存储库查询方法中使用SpEL表达式 - entityName
@Entity
public class User {

  @Id
  @GeneratedValue
  Long id;

  String lastname;
}

public interface UserRepository extends JpaRepository<User,Long> {

  @Query("select u from #{#entityName} u where u.lastname = ?1")
  List<User> findByLastname(String lastname);
}

要避免在 @Query 注解的查询字符串中声明实际实体名称,可以使用 #{#entityName} 变量。

可以在 @Entity 注解中自定义 entityName。SpEL表达式不支持在 orm.xml 中自定义。

当然,你可以直接在查询声明中使用 User,但这也需要你更改查询。对 #entityName 的引用可以将 User 类重映射到另一个实体名称 (例如,使用 @Entity(name = "MyUser"),以防止未来重构类名。

查询字符串中 #{#entityName} 表达式的另一个使用场景是,如果要为具体域类型定义具有专用存储库接口的通用存储库接口。 要在具体接口上不重复自定义查询方法的定义,可以在通用存储库接口中的 @Query 注解的查询字符串中使用实体名称表达式,如以下示例所示:

Example 63. 在存储库查询方法中使用SpEL表达式 - 带继承的 entityName
@MappedSuperclass
public abstract class AbstractMappedType {
  …
  String attribute
}

@Entity
public class ConcreteType extends AbstractMappedType { … }

@NoRepositoryBean
public interface MappedTypeRepository<T extends AbstractMappedType>
  extends Repository<T, Long> {

  @Query("select t from #{#entityName} t where t.attribute = ?1")
  List<T> findAllByAttribute(String attribute);
}

public interface ConcreteRepository
  extends MappedTypeRepository<ConcreteType> { … }

在前面的示例中,MappedTypeRepository 接口是继承 AbstractMappedType 的几种域类型的公共父接口。 它还定义了通用的 findAllByAttribute(…​) 方法,该方法可用于专用存储库接口的实例。如果现在在 ConcreteRepository 上调用 findAllByAttribute(…​),则对应的查询是 select t from ConcreteType t where t.attribute = ?1

4.3.8. 修改查询

前面的所有部分都描述了如何声明查询以访问给定实体或实体集合。你可以使用“Spring Data Repositories的自定义实现” 中描述的方法添加自定义修改行为。由于此方法对于全面的定制功能是可行的,因此你可以通过使用 @Modifying 注解查询方法来修改仅需要参数绑定的查询,如以下示例所示:

Example 64. 声明更新查询
@Modifying
@Query("update User u set u.firstname = ?1 where u.lastname = ?2")
int setFixedFirstnameFor(String firstname, String lastname);

这样做会触发将方法注释为更新查询而不是选择查询。由于 EntityManager 在执行修改查询后可能包含过时的实体, 我们不会自动清除它(有关详细信息,请参阅 EntityManager.clear()JavaDoc), 因为这会有效地丢弃 EntityManager 中仍未处理的所有未刷新的更改。如果希望 EntityManager 自动清除, 可以将 @Modifying 注解的 clearAutomatically 属性设置为 true

派生删除查询

Spring Data JPA还支持派生删除查询,使你可以避免显式声明JPQL查询,如以下示例所示:

Example 65. 使用派生的删除查询
interface UserRepository extends Repository<User, Long> {

  void deleteByRoleId(long roleId);

  @Modifying
  @Query("delete from User u where user.role.id = ?1")
  void deleteInBulkByRoleId(long roleId);
}

虽然 deleteByRoleId(…​) 方法看起来像与 deleteInBulkByRoleId(…​) 产生相同的结果,两个方法声明在执行方式上存在重要差异。 顾名思义,后一种方法针对数据库发出单个JPQL查询(在注解中定义的查询)。这意味着即使当前加载了User实例也看不到调用了它的生命周期回调。

为了确保实际调用生命周期查询,deleteByRoleId(…​) 的调用执行查询,然后逐个删除返回的实例, 这样持久性提供程序就可以在这些实体上调用 @PreRemove 回调。

实际上,派生删除查询是执行查询然后在结果上调用 CrudRepository.delete(Iterable <User> users) 并保持行为与 CrudRepository 中其他 delete(…​) 方法的实现同步的快捷方式。

4.3.9. 应用查询提示

要将JPA查询提示应用于存储库接口中声明的查询,可以使用 @QueryHints 注解。它需要一组JPA @QueryHint 注解加上一个布尔标志来默认禁止在分页时需要调用的计数查询上使用查询提示,如以下示例所示:

Example 66. 使用 QueryHints 的存储库方法
public interface UserRepository extends Repository<User, Long> {

  @QueryHints(value = { @QueryHint(name = "name", value = "value")},
              forCounting = false)
  Page<User> findByLastname(String lastname, Pageable pageable);
}

前面的声明将为实际查询应用已配置的 @QueryHint,但省略将其应用于触发的计数查询以计算总页数。

4.3.10. 配置Fetch-和LoadGraphs

JPA 2.1规范引入了对指定Fetch-和LoadGraphs的支持,我们也提供了 @EntityGraph 注解,它允许你引用 @NamedEntityGraph 定义。 你可以在实体上使用该注解来配置生成查询的获取计划。可以使用 @EntityGraph 注解上的 type 属性配置获取的类型(FetchLoad)。 有关更多参考,请参阅JPA 2.1 Spec 3.7.4。

以下示例显示如何在实体上定义命名实体图:

Example 67. 在实体上定义命名实体图
@Entity
@NamedEntityGraph(name = "GroupInfo.detail",
  attributeNodes = @NamedAttributeNode("members"))
public class GroupInfo {

  // 默认拉取模式是懒加载模式
  @ManyToMany
  List<GroupMember> members = new ArrayList<GroupMember>();

  …
}

以下示例显示如何在存储库查询方法上引用命名实体图:

Example 68. 在存储库查询方法上引用命名实体图定义
@Repository
public interface GroupRepository extends CrudRepository<GroupInfo, String> {

  @EntityGraph(value = "GroupInfo.detail", type = EntityGraphType.LOAD)
  GroupInfo getByGroupName(String name);

}

也可以使用 @EntityGraph 定义ad hoc实体图。提供的 attributePaths 将转换为相应的 EntityGraph, 而无需将 @NamedEntityGraph 显式添加到你的域类型中,如以下示例所示:

Example 69. 在存储库查询方法上使用AD-HOC实体图定义
@Repository
public interface GroupRepository extends CrudRepository<GroupInfo, String> {

  @EntityGraph(attributePaths = { "members" })
  GroupInfo getByGroupName(String name);

}

4.3.11. 投影

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

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

Example 70. 一个聚合根和存储库样本
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 Data提供了什么方法来实现这一目标?本章的其余部分回答了这个问题。

基于接口的投影

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

Example 71. 用于检索属性子集的投影接口
interface NamesOnly {

  String getFirstname();
  String getLastname();
}

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

Example 72. 使用基于接口的投影和查询方法的存储库
interface PersonRepository extends Repository<Person, UUID> {

  Collection<NamesOnly> findByLastname(String lastname);
}

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

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

Example 73. 用于检索属性子集的投影接口
interface PersonSummary {

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

  interface AddressSummary {
    String getCity();
  }
}

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

闭合投影

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

Example 74. 一个闭合投影
interface NamesOnly {

  String getFirstname();
  String getLastname();
}

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

开放投影

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

Example 75. 一个开放投影
interface NamesOnly {

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

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

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

Example 76. 使用默认方法进行自定义逻辑的投影接口
interface NamesOnly {

  String getFirstname();
  String getLastname();

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

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

Example 77. Person对象样例
@Component
class MyBean {

  String getFullName(Person person) {
    …
  }
}

interface NamesOnly {

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

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

Example 78. Person对象样例
interface NamesOnly {

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

同样,对于更复杂的表达式,你应该使用Spring bean并通过表达式调用方法,如所述。

基于类的投影(DTOs)

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

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

以下示例显示了投影DTO:

Example 79. 投影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(…) 和 hashCode() 实现
}

避免投影DTOs的样板代码

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

@Value
class NamesOnly {
	String firstname, lastname;
}

默认情况下,字段是 private final,并且该类公开一个构造函数,该构造函数接受所有字段, 而且也自动实现 equals(…​)hashCode() 方法。

动态投影

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

Example 80. 使用动态投影参数的存储库
interface PersonRepository extends Repository<Person, UUID> {

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

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

Example 81. 使用具有动态投影的存储库
void someMethod(PersonRepository people) {

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

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

4.4. 存储过程

JPA 2.1规范引入了对使用JPA条件查询API调用存储过程的支持。我们引入了 @Procedure 注解,用于在存储库方法上声明存储过程元数据。

以下示例使用如下的存储过程:

Example 82. HSQLDB中 plus1inout 存储过程的定义
/;
DROP procedure IF EXISTS plus1inout
/;
CREATE procedure plus1inout (IN arg int, OUT res int)
BEGIN ATOMIC
 set res = arg + 1;
END
/;

可以使用实体类型上的 NamedStoredProcedureQuery 注解来配置存储过程的元数据。

Example 83. 实体上的 StoredProcedure 元数据定义
@Entity
@NamedStoredProcedureQuery(name = "User.plus1", procedureName = "plus1inout", parameters = {
  @StoredProcedureParameter(mode = ParameterMode.IN, name = "arg", type = Integer.class),
  @StoredProcedureParameter(mode = ParameterMode.OUT, name = "res", type = Integer.class) })
public class User {}

你可以通过多种方式从存储库方法引用存储过程。要调用的存储过程可以使用 @Procedure 注解的 valueprocedureName 属性直接定义, 也可以使用 name 属性间接定义。如果未配置名称,则将使用存储库方法的名称作为降级方案。

以下示例显示如何显式引用存储过程:

Example 84. 在数据库中显式引用名为“plus1inout”的存储过程
@Procedure("plus1inout")
Integer explicitlyNamedPlus1inout(Integer arg);

以下示例说明如何使用 procedureName 别名隐式引用存储过程:

Example 85. 通过 procedureName 别名在数据库中隐式引用名为“plus1inout”的存储过程
@Procedure(procedureName = "plus1inout")
Integer plus1inout(Integer arg);

以下示例显示如何在 EntityManager 中显式引用映射的存储过程:

Example 86. 在 EntityManager 中显式引用映射的命名存储过程“User.plus1IO”。
@Procedure(name = "User.plus1IO")
Integer entityAnnotatedCustomNamedProcedurePlus1IO(@Param("arg") Integer arg);

以下示例说明如何使用方法名称在 EntityManager 中隐式引用命名的存储过程:

Example 87. 使用方法名称在 EntityManager 中隐式引用映射的命名存储过程“User.plus1”。
@Procedure
Integer plus1(@Param("arg") Integer arg);

4.5. 规范

JPA 2引入了一个标准API,你可以使用它以编程方式构建查询。通过编写 criteria,可以为域类定义查询的 where 子句。 再退一步,可以将这些标准视为JPA标准API约束描述实体的谓词。

Spring Data JPA采用Eric Evans的书籍“Domain Driven Design”中的规范概念,遵循相同的语义并提供API以使用JPA条件API定义此类规范。 要支持规范,可以在存储库接口中继承 JpaSpecificationExecutor 接口,如下所示:

public interface CustomerRepository extends CrudRepository<Customer, Long>, JpaSpecificationExecutor {
 …
}

附加接口具有允许你以各种方式执行规范的方法。例如,findAll 方法返回与规范匹配的所有实体,如以下示例所示:

List<T> findAll(Specification<T> spec);

Specification 接口定义如下:

public interface Specification<T> {
  Predicate toPredicate(Root<T> root, CriteriaQuery<?> query,
            CriteriaBuilder builder);
}

可以轻松地使用规范在实体之上构建可扩展的谓词集,然后可以将其与 JpaRepository 结合使用, 而无需为每个所需组合声明查询(方法),如以下示例所示:

Example 88. 客户域的规格
public class CustomerSpecs {

  public static Specification<Customer> isLongTermCustomer() {
    return new Specification<Customer>() {
      public Predicate toPredicate(Root<Customer> root, CriteriaQuery<?> query,
            CriteriaBuilder builder) {

         LocalDate date = new LocalDate().minusYears(2);
         return builder.lessThan(root.get(_Customer.createdAt), date);
      }
    };
  }

  public static Specification<Customer> hasSalesOfMoreThan(MontaryAmount value) {
    return new Specification<Customer>() {
      public Predicate toPredicate(Root<T> root, CriteriaQuery<?> query,
            CriteriaBuilder builder) {

         // 在这里构建查询
      }
    };
  }
}

不可否认,样板的数量仍有改进的空间(最终可能会因Java 8闭包有所减少),但客户端变得更好,正如你将在本节后面看到的那样。 _Customer 类型是使用JPA Metamodel生成器生成的元模型类型(有关示例,请参阅 Hibernate实现的文档)。 因此,表达式 _Customer.createdAt 假定 Customer 具有 Date 类型的 createdAt 属性。 除此之外,我们已经在业务需求抽象级别上表达了一些标准并创建了可执行 Specifications。所以客户端可能会像下面那样使用 Specification

Example 89. 使用简单的规范
List<Customer> customers = customerRepository.findAll(isLongTermCustomer());

为什么不为这种数据访问创建查询?使用单个 Specification 并不比普通的查询声明有很多好处。 但当你将它们组合起来创建新的 Specification 对象时,规范的强大功能真的很棒。 你可以通过我们提供的规范帮助程序类来实现此目的,以构建类似于以下内容的表达式:

Example 90. 组合规范
MonetaryAmount amount = new MonetaryAmount(200.0, Currencies.DOLLAR);
List<Customer> customers = customerRepository.findAll(
  where(isLongTermCustomer()).or(hasSalesOfMoreThan(amount)));

Specification 提供了一些链接和组合 Specification 实例的“胶水代码”方法。 这些方法允许你通过创建新的 Specification 实现并将它们与现有实现相结合来扩展数据访问层。

4.6. 按示例查询

4.6.1. 介绍

本章介绍Query by Example并说明如何使用它。

按示例查询(QBE)是一种用户友好的查询技术,具有简单的接口。它允许动态创建查询,并且不需要你编写包含字段名称的查询。 实际上,Query by Example不要求你使用特定于存储库的查询语言来编写查询。

4.6.2. 用法

Query by Example API由三部分组成:

  • 探针:具有填充字段的域对象的实际示例。

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

  • ExampleExample 由探针和 ExampleMatcher 组成。它用于创建查询。

按示例查询非常适合几种场景:

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

  • 频繁重构域对象,而不必担心破坏现有查询。

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

按示例查询也有几个限制:

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

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

在开始使用Query by Example之前,你需要拥有一个域对象。首先,为存储库创建一个接口,如以下示例所示:

Example 91. Person对象样例
public class Person {

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

  // … getters 和 setters 省略了
}

前面的示例显示了一个简单的域对象。你可以使用它来创建 Example。默认情况下,将忽略具有 null 的字段, 并使用特定于存储库的默认值匹配字符串。可以使用工厂方法或使用 ExampleMatcher 构建示例。 以下清单显示了一个简单的示例:

Example 92. 简单的示例
Person person = new Person();                     (1)
person.setFirstname("Dave");                      (2)

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

理想情况下,使用存储库执行示例。为此,请让你的存储库接口扩展 QueryByExampleExecutor<T>。 以下清单显示了 QueryByExampleExecutor 接口的摘录:

Example 93. QueryByExampleExecutor
public interface QueryByExampleExecutor<T> {

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

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

  // … 省略了更多功能
}

4.6.3. 示例匹配器

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

Example 94. 具有自定义匹配的示例匹配器
Person person = new Person();                          (1)
person.setFirstname("Dave");                           (2)

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

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

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

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

Example 95. 配置匹配器选项
ExampleMatcher matcher = ExampleMatcher.matching()
  .withMatcher("firstname", endsWith())
  .withMatcher("lastname", startsWith().ignoreCase());
}

配置matcher选项的另一种方法是使用lambda表达式(在Java 8中引入)。此方法创建一个回调,要求实现者修改匹配器。 你无需返回匹配器,因为配置选项保存在匹配器实例中。以下示例显示了使用lambdas的匹配器:

Example 96. 使用lambdas配置匹配器选项
ExampleMatcher matcher = ExampleMatcher.matching()
  .withMatcher("firstname", match -> match.endsWith())
  .withMatcher("firstname", match -> match.startsWith());
}

Example 创建的查询使用配置的合并视图。可以在 ExampleMatcher 级别设置默认匹配设置,而可以将单个设置应用于特定属性路径。 除非明确定义,否则 ExampleMatcher 上配置的设置将由属性路径设置继承。属性路径设置优先于默认设置。 下表描述了各种 ExampleMatcher 设置的作用域:

Table 4. ExampleMatcher 设置的作用域

设置

作用域

null值处理

ExampleMatcher

字符串匹配

ExampleMatcher和属性路径

忽略属性

属性路径

区分大小写

ExampleMatcher和属性路径

值转换

属性路径

4.6.4. 执行示例

在Spring Data JPA中,你可以使用存储库按示例查询,如以下示例所示:

Example 97. 使用存储库按示例查询
public interface PersonRepository extends JpaRepository<Person, String> { … }

public class PersonService {

  @Autowired PersonRepository personRepository;

  public List<Person> findPeople(Person probe) {
    return personRepository.findAll(Example.of(probe));
  }
}
目前,只有 SingularAttribute 属性可用于属性匹配。

属性说明符接受属性名称(例如 firstnamelastname)。你可以通过将属性与点(address.city)链接在一起来进行导航。 你还可以使用匹配选项和区分大小写来调整它。

下表显示了可以使用的各种 StringMatcher 选项以及在名为 firstname 的字段上使用它们的结果:

Table 5. StringMatcher 选项

匹配

逻辑结果

DEFAULT (区分大小写)

firstname = ?0

DEFAULT (不区分大小写)

LOWER(firstname) = LOWER(?0)

EXACT (区分大小写)

firstname = ?0

EXACT (不区分大小写)

LOWER(firstname) = LOWER(?0)

STARTING (区分大小写)

firstname like ?0 + '%'

STARTING (不区分大小写)

LOWER(firstname) like LOWER(?0) + '%'

ENDING (区分大小写)

firstname like '%' + ?0

ENDING (不区分大小写)

LOWER(firstname) like '%' + LOWER(?0)

CONTAINING (区分大小写)

firstname like '%' + ?0 + '%'

CONTAINING (不区分大小写)

LOWER(firstname) like '%' + LOWER(?0) + '%'

4.7. 事务

默认情况下,存储库实例上的CRUD方法是事务性的。对于读取操作,事务配置 readOnly 标志设置为 true。 所有其他配置都使用普通的 @Transactional,以便应用默认事务配置。有关详细信息,请参阅 SimpleJpaRepository 的JavaDoc。 如果需要为存储库中声明的方法之一调整事务配置,请在存储库接口中重新声明该方法,如下所示:

Example 98. CRUD的自定义事务配置
public interface UserRepository extends CrudRepository<User, Long> {

  @Override
  @Transactional(timeout = 10)
  public List<User> findAll();

  // 其它的查询方法声明
}

这样做会导致 findAll() 方法以10秒的超时时间运行并且没有 readOnly 标志。

更改事务行为的另一种方法是使用(通常)覆盖多个存储库的外观或服务实现。其目的是为非CRUD操作定义事务边界。 以下示例显示如何将此类Facade用于多个存储库:

Example 99. 使用facade为多个存储库调用定义事务
@Service
class UserManagementImpl implements UserManagement {

  private final UserRepository userRepository;
  private final RoleRepository roleRepository;

  @Autowired
  public UserManagementImpl(UserRepository userRepository,
    RoleRepository roleRepository) {
    this.userRepository = userRepository;
    this.roleRepository = roleRepository;
  }

  @Transactional
  public void addRoleToAllUsers(String roleName) {

    Role role = roleRepository.findByName(roleName);

    for (User user : userRepository.findAll()) {
      user.addRole(role);
      userRepository.save(user);
    }
}

此示例导致调用 addRoleToAllUsers(…​) 在事务内部运行(加入现有事务或创建新事务(如果没有已运行的))。 然后忽略存储库中的事务配置,因为外部事务配置确定所使用的实际配置。请注意,你必须激活 <tx:annotation-driven/> 或显式使用 @EnableTransactionManagement 以使基于注解的外观配置起作用。此示例假定你已启用组件扫描。

4.7.1. 事务性查询方法

要让你的查询方法是事务性的,请在你定义的存储库接口上使用 @Transactional,如以下示例所示:

Example 100. 在查询方法中使用 @Transactional
@Transactional(readOnly = true)
public interface UserRepository extends JpaRepository<User, Long> {

  List<User> findByLastname(String lastname);

  @Modifying
  @Transactional
  @Query("delete from User u where u.active = false")
  void deleteInactiveUsers();
}

通常,你希望将 readOnly 标志设置为 true,因为大多数查询方法只读取数据。与此相反,deleteInactiveUsers() 使用 @Modifying 注解并覆盖事务配置。因此,该方法在 readOnly 标志设置为 false 的情况下运行。

你可以将事务用于只读查询,并通过设置 readOnly 标志来标记它们。但是,这样做不会检查你是否触发了非查询操作(尽管某些数据库拒绝只读事务中的 INSERTUPDATE 语句)。而 readOnly 标志则作为提示传播到底层JDBC驱动程序以进行性能优化。此外,Spring对底层JPA提供程序执行了一些优化。 例如,当与Hibernate一起使用时,当你将事务配置为 readOnly 时,刷新模式将设置为 NEVER,这会导致Hibernate跳过脏检查(对大型对象树的显著性能改进)。

4.8. 锁

指定要使用锁定模式,可以在查询方法上使用 @Lock 注解,如以下示例所示:

Example 101. 在查询方法上定义锁元数据
interface UserRepository extends Repository<User, Long> {

  // 简单查询方法
  @Lock(LockModeType.READ)
  List<User> findByLastname(String lastname);
}

此方法声明使触发的查询 LockModeTypeREAD。你还可以通过在存储库接口中重新声明CRUD方法并添加 @Lock 注解来定义CRUD方法的锁定,如以下示例所示:

Example 102. 在CRUD方法上定义锁元数据
interface UserRepository extends Repository<User, Long> {

  // 重新声明CRUD方法
  @Lock(LockModeType.READ);
  List<User> findAll();
}

4.9. 审计

4.9.1. 基础

Spring Data提供了复杂的支持,可以透明地跟踪创建或更改实体的人员以及更改发生的时间。要从该功能中受益, 你必须为你的实体类配备审计元数据,该元数据可以使用注解或通过实现接口来定义。

基于注解的审计元数据

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

Example 103. 经审计的实体
class Customer {

  @CreatedBy
  private User user;

  @CreatedDate
  private DateTime createdDate;

  // … 其他属性省略
}

如你所见,可以有选择地应用注解,具体取决于你要捕获的信息。进行更改时捕获的注解可用于Joda-Time,DateTime, 旧Java DateCalendar,JDK8日期和时间类型以及 longLong 类型的属性。

基于接口的审计元数据

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

还有一个方便的基类 AbstractAuditable,你可以继承它以避免需要手动实现接口方法。这样做会增加域类与Spring Data的耦合, 这可能是你想要避免的。通常,基于注解的定义审计元数据的方式是优选的,因为它具有较小的侵入性和更灵活性。

AuditorAware

如果你使用 @CreatedBy@LastModifiedBy,审计基础设施需要以某种方式了解当前主体。 为此,我们提供了一个 AuditorAware<T> SPI接口,你必须实现该接口,以告知基础设施当前与应用程序交互的用户。 泛型类型 T 定义了使用 @CreatedBy@LastModifiedBy 注解的属性的类型。

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

Example 104. 基于Spring Security的 AuditorAware 实现
class SpringSecurityAuditorAware implements AuditorAware<User> {

  public User getCurrentAuditor() {

    Authentication authentication = SecurityContextHolder.getContext().getAuthentication();

    if (authentication == null || !authentication.isAuthenticated()) {
      return null;
    }

    return ((MyUserDetails) authentication.getPrincipal()).getUser();
  }
}

该实现访问Spring Security提供的 Authentication 对象,并查找你在 UserDetailsS​​ervice 实现中创建的自定义 UserDetails 实例。 我们假设你通过 UserDetails 实现公开域用户,根据找到的身份验证获取它,你也可以从任何地方查找它。

4.9.2. JPA审计

通用审计配置

Spring Data JPA附带了一个实体监听器,可用于触发审计信息的捕获。首先,你必须注册 AuditingEntityListener 以用于审计 orm.xml 文件中持久性上下文中的所有实体,如以下示例所示:

Example 105. 审计配置orm.xml
<persistence-unit-metadata>
  <persistence-unit-defaults>
    <entity-listeners>
      <entity-listener class="….data.jpa.domain.support.AuditingEntityListener" />
    </entity-listeners>
  </persistence-unit-defaults>
</persistence-unit-metadata>

你还可以将 @EntityListeners 注解在每个实体上以启用 AuditingEntityListener,如下所示:

@Entity
@EntityListeners(AuditingEntityListener.class)
public class MyEntity {

}
审计功能要求类路径上有 spring-aspects.jar

通过适当修改 orm.xml 和类路径上持有 spring-aspects.jar,激活审计功能是将Spring Data JPA审计命名空间元素添加到配置中, 如下所示:

Example 106. 使用XML配置激活审计
<jpa:auditing auditor-aware-ref="yourAuditorAwareBean" />

从Spring Data JPA 1.5开始,你可以通过添加 @EnableJpaAuditing 注解到配置类来启用审计。 你仍然必须修改 orm.xml 文件并在类路径上使用 spring-aspects.jar。以下示例显示如何使用 @EnableJpaAuditing 注解:

Example 107. 使用Java配置激活审计
@Configuration
@EnableJpaAuditing
class Config {

  @Bean
  public AuditorAware<AuditableUser> auditorProvider() {
    return new AuditorAwareImpl();
  }
}

如果将 AuditorAware 类型的bean暴露在 ApplicationContext,则审计基础设施会自动选择它并使用它来确定要在域类型上设置的当前用户。 如果在 ApplicationContext 中注册了多个实现,则可以通过显式设置 @EnableJpaAuditingauditorAwareRef 属性来选择要使用的实现。

4.10. 其他考虑因素

4.10.1. 在自定义实现中使用 JpaContext

使用多个 EntityManager 实例和自定义存储库实现时, 需要将正确的EntityManager连接到存储库实现类。你可以通过在 @PersistenceContext 注解中显式命名 EntityManager 来实现, 或者,如果 EntityManager@Autowired 标注,则使用 @Qualifier 来区分。

从Spring Data JPA 1.9开始,Spring Data JPA包含一个名为 JpaContext 的类,它允许你通过托管域类获取 EntityManager, 假设它仅由应用程序中的一个 EntityManager 实例管理。以下示例显示如何在自定义存储库中使用 JpaContext

Example 108. 在自定义存储库实现中使用 JpaContext
class UserRepositoryImpl implements UserRepositoryCustom {

  private final EntityManager em;

  @Autowired
  public UserRepositoryImpl(JpaContext context) {
    this.em = context.getEntityManagerByManagedType(User.class);
  }

  …
}

此方法的优点是,如果将域类型分配给不同的持久性单元,则不必触及存储库来更改对持久性单元的引用。

4.10.2. 合并持久性单元

Spring支持具有多个持久性单元。但是,有时你可能希望模块化你的应用程序,但仍然确保所有这些模块在单个持久性单元内运行。 为了实现该行为,Spring Data JPA提供了一个 PersistenceUnitManager 实现,该实现根据其名称自动合并持久性单元,如以下示例所示:

Example 109. 使用 MergingPersistenceUnitmanager
<bean class="….LocalContainerEntityManagerFactoryBean">
  <property name="persistenceUnitManager">
    <bean class="….MergingPersistenceUnitManager" />
  </property>
</bean>
@Entity 类和JPA映射文件的类路径扫描

普通的JPA设置要求在 orm.xml 中列出所有注解映射的实体类。这同样适用于其它XML映射文件。Spring Data JPA 提供了一个 ClasspathScanningPersistenceUnitPostProcessor,它可以获取配置的基础包,并可选择采用映射文件名模式。 然后,它会扫描给定的包以获取使用 @Entity@MappedSuperclass 注解的类,加载与文件名模式匹配的配置文件,并将它们交给JPA配置。 后置处理器必须配置如下:

Example 110. 使用 ClasspathScanningPersistenceUnitPostProcessor
<bean class="….LocalContainerEntityManagerFactoryBean">
  <property name="persistenceUnitPostProcessors">
    <list>
      <bean class="org.springframework.data.jpa.support.ClasspathScanningPersistenceUnitPostProcessor">
        <constructor-arg value="com.acme.domain" />
        <property name="mappingFileNamePattern" value="**/*Mapping.xml" />
      </bean>
    </list>
  </property>
</bean>
从Spring 3.1开始,可以直接在 LocalContainerEntityManagerFactoryBean 上配置要扫描的包, 以便为实体类启用类路径扫描。有关详细信息,请参阅 JavaDoc

4.10.3. CDI集成

存储库接口的实例通常由容器创建,在使用Spring Data时,Spring是最自然的选择。Spring提供了对创建bean实例的复杂支持, 如创建存储库实例中所述。从版本1.1.0开始,Spring Data JPA附带了一个自定义CDI扩展, 允许在CDI环境中使用存储库抽象。扩展是JAR的一部分。要激活它,请在类路径中包含Spring Data JPA JAR。

你现在可以通过为 EntityManagerFactoryEntityManager 实现CDI Producer来设置基础结构,如以下示例所示:

class EntityManagerFactoryProducer {

  @Produces
  @ApplicationScoped
  public EntityManagerFactory createEntityManagerFactory() {
    return Persistence.createEntityManagerFactory("my-presistence-unit");
  }

  public void close(@Disposes EntityManagerFactory entityManagerFactory) {
    entityManagerFactory.close();
  }

  @Produces
  @RequestScoped
  public EntityManager createEntityManager(EntityManagerFactory entityManagerFactory) {
    return entityManagerFactory.createEntityManager();
  }

  public void close(@Disposes EntityManager entityManager) {
    entityManager.close();
  }
}

必要的设置可能因JavaEE环境而异。你可能只需要将 EntityManager 重新声明为CDI bean,如下所示:

class CdiConfig {

  @Produces
  @RequestScoped
  @PersistenceContext
  public EntityManager entityManager;
}

在前面的示例中,容器必须能够自己创建JPA EntityManagers。所有配置都将JPA EntityManager 重新导出为CDI bean。

5. 附录

5.1. 命名空间参考

5.1.1. <repositories/> 元素

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

Table 6. 属性
名字 描述

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 7. 属性
名字 描述

locations

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

5.3. 存储库查询关键字

5.3.1. 支持的查询关键字

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

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

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 9. 查询返回类型

返回类型

描述

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 的查询也可以发出无限数量的元素。

5.5. FAQ

5.5.1. 常见

  1. 我想获得更详细的日志信息,以便了解在 JpaRepository 中调用了哪些方法,该怎么做呢?

    你可以使用Spring提供的 CustomizableTraceInterceptor,如以下示例所示:

    <bean id="customizableTraceInterceptor" class="
      org.springframework.aop.interceptor.CustomizableTraceInterceptor">
      <property name="enterMessage" value="Entering $[methodName]($[arguments])"/>
      <property name="exitMessage" value="Leaving $[methodName](): $[returnValue]"/>
    </bean>
    
    <aop:config>
      <aop:advisor advice-ref="customizableTraceInterceptor"
        pointcut="execution(public * org.springframework.data.jpa.repository.JpaRepository+.*(..))"/>
    </aop:config>

5.5.2. 基础设施

  1. 目前,我已经实现了基于 HibernateDaoSupport 的存储库层。我使用Spring的 AnnotationSessionFactoryBean 创建一个 SessionFactory。如何在此环境中使用Spring Data存储库?

    你必须使用 HibernateJpaSessionFactoryBean 替换 AnnotationSessionFactoryBean,如下所示:

    Example 111. 从 HibernateEntityManagerFactory 查找 SessionFactory
    <bean id="sessionFactory" class="org.springframework.orm.jpa.vendor.HibernateJpaSessionFactoryBean">
      <property name="entityManagerFactory" ref="entityManagerFactory"/>
    </bean>

5.5.3. 审计

  1. 我想使用Spring Data JPA审计功能,但我的数据库已配置为在实体上设置修改和创建日期。如何防止Spring Data以编程方式设置日期?

    auditing 命名空间元素的 set-dates 属性设置为 false

5.6. 词汇表

AOP

面向切面编程

Commons DBCP

Commons DataBase Connection Pools - 来自Apache基础的库,提供 DataSource 接口的池实现

CRUD

创建,读取,更新,删除 - 基本持久性操作

DAO

数据访问对象 - 用于将持久化逻辑与要持久化的对象分开的模式

Dependency Injection

模式将组件的依赖关系从外部传递给组件,降低组件间耦合。有关更多信息, 请参阅 http://en.wikipedia.org/wiki/Dependency_Injection

EclipseLink

实现JPA的对象关系映射器 - http://www.eclipselink.org

Hibernate

实现JPA的对象关系映射器 - http://www.hibernate.org

JPA

Java持久化API

Spring

Java应用程序框架 - http://projects.spring.io/spring-framework


1. 请参阅 XML配置