Preface

©2008-2019 原作者。

Note

本文档的副本可以供您自己使用,也可以分发给其他人,但前提是您不对此类副本收取任何费用,并且还应确保每份副本均包含本版权声明(无论是印刷版本还是电子版本)。

目录

Spring Data JPA 为 Java Persistence API(JPA)提供了存储库支持。它简化了需要访问 JPA 数据源的应用程序的开发。

1.项目元数据

2.新功能

2.1. Spring Data JPA 1.11 的新增功能

Spring Data JPA 1.11 添加了以下功能:

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

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

  • 分页查询执行优化。

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

2.2. Spring Data JPA 1.10 的新增功能

Spring Data JPA 1.10 添加了以下功能:

  • 在存储库查询方法中支持Projections

  • 支持实例查询

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

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

  • JSR-310 和 ThreeTenBP 的ZoneIdAttributeConverter实现。

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

3. Dependencies

由于各个 Spring Data 模块的起始日期不同,因此大多数模块带有不同的主要和次要版本号。查找兼容版本的最简单方法是依赖于我们附带定义的兼容版本的 Spring Data Release Train BOM。在 Maven 项目中,您可以在 POM 的<dependencyManagement />部分中声明此依赖关系,如下所示:

例子 1.使用 Spring Data Release 系列 BOM

<dependencyManagement>
  <dependencies>
    <dependency>
      <groupId>org.springframework.data</groupId>
      <artifactId>spring-data-releasetrain</artifactId>
      <version>Lovelace-SR5</version>
      <scope>import</scope>
      <type>pom</type>
    </dependency>
  </dependencies>
</dependencyManagement>

当前的发行版本是Lovelace-SR5。火车名称按字母 Sequences 升序,当前可用的火车列于here。版本名称遵循以下模式:${name}-${release},其中 release 可以是以下之一:

  • BUILD-SNAPSHOT:当前快照

  • M1M2,依此类推:里程碑

  • RC1RC2,依此类推:发布候选

  • RELEASE:GA 发布

  • SR1SR2等:服务版本

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

例子 2.声明对 Spring Data 模块的依赖

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

3.1. 使用 Spring Boot 进行依赖 Management

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

3.2. Spring Framework

当前版本的 Spring Data 模块要求使用 5.1.5.RELEASE 或更高版本的 Spring Framework。这些模块也可以与该次要版本的较旧错误修正版本一起使用。但是,强烈建议使用该版本中的最新版本。

4.使用 Spring 数据存储库

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

Tip

  • Spring 数据存储库文档和您的模块*

本章介绍了 Spring Data 存储库的核心概念和接口。本章中的信息来自 Spring Data Commons 模块。它使用 Java Persistence API(JPA)模块的配置和代码示例。您应该使 XML 名称空间声明和类型适应于所使用的特定模块的等效项。 “ Namespace reference”涵盖 XML 配置,所有支持存储库 API 的 Spring Data 模块均支持该配置。 “ Repositories 查询关键字”通常涵盖存储库抽象支持的查询方法关键字。有关模块的特定功能的详细信息,请参阅本文档中有关该模块的章节。

4.1. 核心概念

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

例子 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)

  // … more functionality omitted.
}
  • (1) 保存给定的实体。
  • (2) 返回由给定 ID 标识的实体。
  • (3) 返回所有实体。
  • (4) 返回实体数。
  • (5) 删除给定的实体。
  • (6) 指示是否存在具有给定 ID 的实体。

Note

我们还提供特定于持久性技术的抽象,例如JpaRepositoryMongoRepository。这些接口扩展了CrudRepository,并且除了诸如CrudRepository之类的与通用技术无关的通用接口之外,还公开了底层持久性技术的功能。

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

例子 4. PagingAndSortingRepository界面

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

  Iterable<T> findAll(Sort sort);

  Page<T> findAll(Pageable pageable);
}

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

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

除了查询方法之外,还可以使用计数和删除查询的查询派生。以下列表显示派生计数查询的接口定义:

例子 5.派生计数查询

interface UserRepository extends CrudRepository<User, Long> {

  long countByLastname(String lastname);
}

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

例子 6.派生的删除查询

interface UserRepository extends CrudRepository<User, Long> {

  long deleteByLastname(String lastname);

  List<User> removeByLastname(String lastname);
}

4.2. 查询方法

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

  • 声明扩展存储库的接口或其子接口之一,然后将其键入到它应处理的域类和 ID 类型,如以下示例所示:
interface PersonRepository extends Repository<Person, Long> { … }
  • 在接口上声明查询方法。
interface PersonRepository extends Repository<Person, Long> {
  List<Person> findByLastname(String lastname);
}
  • 设置 Spring 以使用JavaConfigXML configuration为这些接口创建代理实例。

  • 要使用 Java 配置,请创建类似于以下内容的类:

import org.springframework.data.jpa.repository.config.EnableJpaRepositories;

@EnableJpaRepositories
class Config {}
  • 要使用 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 命名空间。如果将存储库抽象用于任何其他 Store,则需要将其更改为 Store 模块的适当名称空间声明。换句话说,您应该将jpa换成mongodb

  • 另外,请注意,JavaConfig 变量不会显式配置程序包,因为默认情况下使用带 Comments 的类的程序包。要自定义要扫描的包,请使用特定于数据存储的存储库@Enable${store}Repositories-Comments 的basePackage…属性之一。
  • 注入存储库实例并使用它,如以下示例所示:
class SomeClient {

  private final PersonRepository repository;

  SomeClient(PersonRepository repository) {
    this.repository = repository;
  }

  void doSomething() {
    List<Person> persons = repository.findByLastname("Matthews");
  }
}

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

4.3. 定义存储库接口

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

4.3.1. 微调存储库定义

通常,您的存储库界面扩展了RepositoryCrudRepositoryPagingAndSortingRepository。另外,如果您不想扩展 Spring Data 接口,也可以使用@RepositoryDefinitionComments 存储库接口。扩展CrudRepository公开了一套完整的方法来操纵您的实体。如果您希望对公开的方法保持选择性,请将要公开的方法从CrudRepository复制到域存储库中。

Note

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

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

例子 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 查找单个用户,并触发查询以通过电子邮件地址查找Users

Note

中间存储库接口用@NoRepositoryBeanComments。确保将 Comments 添加到所有存储库接口,Spring Data 不应在运行时为其创建实例。

4.3.2. 存储库方法的空处理

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

  • com.google.common.base.Optional

  • scala.Option

  • io.vavr.control.Option

  • javaslang.control.Option(不赞成使用 Javaslang)

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

Nullability Annotations

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

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

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

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

SpringComments 使用JSR 305Comments(休眠但分布广泛的 JSR)进行元 Comments。 JSR 305 元 Comments 使诸如IDEAEclipseKotlin之类的工具供应商以通用方式提供了空安全支持,而无需硬编码对 SpringComments 的支持。为了对查询方法的可空性约束进行运行时检查,您需要通过使用package-info.java中的 Spring 的@NonNullApi在包级别激活非空性,如以下示例所示:

例子 8.在package-info.java中声明不可空性

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

一旦设置了非 null 的默认值,就可以在运行时验证存储库查询方法的调用是否具有可空性约束。如果查询执行结果违反了定义的约束,则会引发异常。当该方法返回null但被声明为不可为空时(在存储库所在的包中定义了 Comments 的默认值),就会发生这种情况。如果您想再次选择接受可为空的结果,请在各个方法上有选择地使用@Nullable。使用本节开头提到的结果包装器类型可以按预期 continue 工作:将空结果转换为表示缺席的值。

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

例子 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) 存储库位于我们为其定义了非 null 行为的包(或子包)中。
  • (2) 当执行的查询未产生结果时,引发EmptyResultDataAccessException。当传递给该方法的emailAddressnull时抛出IllegalArgumentException
  • (3) 当执行的查询未产生结果时,返回null。还接受null作为emailAddress的值。
  • (4) 当执行的查询未产生结果时,返回Optional.empty()。当传递给该方法的emailAddressnull时抛出IllegalArgumentException
基于 Kotlin 的存储库中的可空性

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

例子 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) 此方法接受null作为firstname参数,如果查询执行未产生结果,则返回null

4.3.3. 将存储库与多个 Spring 数据模块一起使用

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

  • 如果存储库定义为扩展特定模块的存储库,则它是特定 Spring Data 模块的有效候选者。

  • 如果域类是使用模块特定的类型 Comments 进行 Comments,那么它是特定 Spring Data 模块的有效候选者。 Spring Data 模块可以接受第三方 Comments(例如 JPA 的@Entity),也可以提供自己的 Comments(例如,针对 Spring Data MongoDB 和 Spring Data Elasticsearch 的@Document)。

以下示例显示了使用模块特定接口(在这种情况下为 JPA)的存储库:

例子 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 模块的有效候选者。

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

例子 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。

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

例子 13.使用带有 Comments 的域类的存储库定义

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

@Entity
class Person {
  …
}

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

@Document
class User {
  …
}

PersonRepository引用Person,并使用 JPA @EntityComments 进行了 Comments,因此该存储库显然属于 Spring Data JPA。 UserRepository引用User,使用 Spring Data MongoDB 的@DocumentComments 进行 Comments。

以下不良示例显示了使用带有混合注解的域类的存储库:

例子 14.使用带有混合 Comments 的域类的存储库定义

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 不再能够区分存储库,这导致不确定的行为。

Repositories 类型详细信息区分领域类 Comments用于严格的存储库配置,以标识特定 Spring Data 模块的存储库候选者。在同一个域类型上使用多个特定于持久性技术的 Comments 是可能的,并且可以跨多种持久性技术重用域类型。但是,Spring Data 无法再确定用于绑定存储库的唯一模块。

区分存储库的最后一种方法是确定存储库基础包的范围。基本软件包定义了扫描存储库接口定义的起点,这意味着将存储库定义放在适当的软件包中。默认情况下,Comments 驱动的配置使用配置类的包。 基于 XML 的配置中的基本软件包是必填项。

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

例子 15.基础包的 Comments 驱动配置

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

4.4. 定义查询方法

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

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

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

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

4.4.1. 查询查询策略

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

  • CREATE尝试从查询方法名称构造特定于 Store 的查询。通用方法是从方法名称中删除一组给定的众所周知的前缀,然后解析该方法的其余部分。您可以在“ Query Creation”中阅读有关查询构造的更多信息。

  • USE_DECLARED_QUERY尝试查找已声明的查询,如果找不到则抛出异常。该查询可以通过某处的 Comments 定义,也可以通过其他方式声明。请查阅特定 Store 的文档以找到该 Store 的可用选项。如果存储库基础结构在引导时找不到该方法的声明查询,则它将失败。

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

4.4.2. 查询创建

内置在 Spring Data 存储库基础结构中的查询构建器机制对于在存储库实体上构建约束查询很有用。该机制从方法中剥离前缀find…Byread…Byquery…Bycount…Byget…By,并开始解析其余部分。 Introduction 子句可以包含其他表达式,例如Distinct,以在要创建的查询上设置不同的标志。但是,第一个By充当分隔符,以指示实际标准的开始。在最基本的级别上,您可以定义实体属性的条件,并将它们与AndOr串联。以下示例显示了如何创建许多查询:

例子 16.从方法名查询创建

interface PersonRepository extends Repository<User, Long> {

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

  // Enables the distinct flag for the query
  List<Person> findDistinctPeopleByLastnameOrFirstname(String lastname, String firstname);
  List<Person> findPeopleDistinctByLastnameOrFirstname(String lastname, String firstname);

  // Enabling ignoring case for an individual property
  List<Person> findByLastnameIgnoreCase(String lastname);
  // Enabling ignoring case for all suitable properties
  List<Person> findByLastnameAndFirstnameAllIgnoreCase(String lastname, String firstname);

  // Enabling static ORDER BY for a query
  List<Person> findByLastnameOrderByFirstnameAsc(String lastname);
  List<Person> findByLastnameOrderByFirstnameDesc(String lastname);
}

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

  • 表达式通常是属性遍历,并带有可串联的运算符。您可以将属性表达式与ANDOR结合使用。您还将获得对属性表达式的支持,例如BetweenLessThanGreaterThanLike。支持的运算符可能因数据存储而异,因此请参考参考文档的相应部分。

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

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

4.4.3. 属性表达式

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

List<Person> findByAddressZipCode(ZipCode zipCode);

假设Person具有AddressZipCode。在这种情况下,该方法将创建属性遍历x.address.zipCode。解析算法首先将整个部分(AddressZipCode)解释为属性,然后在域类中检查具有该名称的属性(未大写)。如果算法成功,它将使用该属性。如果不是,该算法将驼峰案例部分的源从右侧分为头和尾,并尝试找到相应的属性,在我们的示例中为AddressZipCode。如果该算法找到了具有该头部的属性,则将其取为尾部,并 continue 从此处开始构建树,以刚才描述的方式将尾部向上拆分。如果第一个分割不匹配,则算法将分割点移到左侧(AddressZipCode)并 continue。

尽管这在大多数情况下应该可行,但算法可能会选择错误的属性。假设Person类也具有addressZip属性。该算法将在第一轮拆分中已经匹配,选择错误的属性,然后失败(因为addressZip的类型可能没有code属性)。

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

List<Person> findByAddress_ZipCode(ZipCode zipCode);

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

4.4.4. 特殊参数处理

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

例子 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知道可用元素和页面的总数。它是通过基础结构触发计数查询来计算总数来实现的。由于这可能很昂贵(取决于使用的 Store),因此您可以返回SliceSlice仅知道下一个Slice是否可用,当遍历较大的结果集时可能就足够了。

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

Note

要查明整个查询可获得多少页,您必须触发另一个计数查询。默认情况下,此查询源自您实际触发的查询。

4.4.5. 限制查询结果

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

例子 18.用TopFirst限制查询的结果大小

User findFirstByOrderByLastnameAsc();

User findTopByOrderByAgeDesc();

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

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

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

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

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

如果将分页或切片应用于限制查询分页(以及对可用页面数的计算),则会在限制结果内应用分页或切片。

Note

通过使用Sort参数来限制结果与动态排序的组合,可以让您表达对最小的“ K”元素和对“ K”的最大元素的查询方法。

4.4.6. 流查询结果

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

例子 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);

Note

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

例子 20.使用Stream<T>导致 try-with-resources 块

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

Note

目前,并非所有的 Spring Data 模块都支持Stream<T>作为返回类型。

4.4.7. 异步查询结果

可以使用Spring 的异步方法执行能力异步运行存储库查询。这意味着该方法在调用时立即返回,而实际查询执行发生在已提交给 Spring TaskExecutor的任务中。异步查询执行与反应式查询执行不同,因此不应混为一谈。有关响应式支持的更多详细信息,请参阅 Store 特定的文档。以下示例显示了许多异步查询:

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

4.5. 创建存储库实例

在本部分中,将为已定义的存储库接口创建实例和 Bean 定义。一种方法是使用支持存储库机制的每个 Spring Data 模块随附的 Spring 名称空间,尽管我们通常建议使用 Java 配置。

4.5.1. XML 配置

每个 Spring Data 模块都包含一个repositories元素,可让您定义 Spring 会为您扫描的基本包,如以下示例所示:

例子 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 都使用从接口名称派生的 bean 名称进行注册,因此UserRepository的接口将注册在userRepository下。 base-package属性允许使用通配符,以便您可以定义扫描程序包的模式。

Using filters

默认情况下,基础结构会拾取扩展位于配置的基本程序包下的特定于持久性技术的Repository子接口的每个接口,并为其创建一个 bean 实例。但是,您可能希望更精细地控制哪些接口具有为其创建的 Bean 实例。为此,请在<repositories />元素内使用<include-filter /><exclude-filter />元素。语义完全等同于 Spring 的上下文名称空间中的元素。有关详细信息,请参见Spring 参考文档

例如,要将某些接口从实例中排除为存储库 Bean,可以使用以下配置:

例子 22.使用 exclude-filter 元素

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

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

4.5.2. JavaConfig

还可以通过在 JavaConfig 类上使用 Store 特定的@Enable${store}RepositoriesComments 来触发存储库基础结构。有关 Spring 容器的基于 Java 的配置的介绍,请参见Spring 参考文档中的 JavaConfig

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

示例 23.基于 samplesComments 的存储库配置

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

  @Bean
  EntityManagerFactory entityManagerFactory() {
    // …
  }
}

Note

前面的示例使用特定于 JPA 的 Comments,您将根据实际使用的 Store 模块对其进行更改。 EntityManagerFactory bean 的定义也是如此。请参阅有关 Store 特定配置的部分。

4.5.3. 独立使用

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

例子 24.仓库工厂的独立使用

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

4.6. Spring 数据存储库的定制实现

本节介绍存储库定制以及片段如何形成复合存储库。

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

4.6.1. 自定义单个存储库

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

例子 25.定制仓库功能的接口

interface CustomizedUserRepository {
  void someCustomMethod(User user);
}

然后,可以让您的存储库接口另外从片段接口扩展,如以下示例所示:

例子 26.定制仓库功能的实现

class CustomizedUserRepositoryImpl implements CustomizedUserRepository {

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

Note

与片段接口相对应的类名称中最重要的部分是Impl后缀。

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

您可以让您的存储库接口扩展片段接口,如以下示例所示:

示例 27.对您的存储库界面的更改

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

  // Declare query methods here
}

将片段接口扩展为您的存储库接口,将 CRUD 和自定义功能结合在一起,并使它可用于 Client 端。

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

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

例子 28.片段及其实现

interface HumanRepository {
  void someHumanMethod(User user);
}

class HumanRepositoryImpl implements HumanRepository {

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

interface ContactRepository {

  void someContactMethod(User user);

  User anotherContactMethod(User user);
}

class ContactRepositoryImpl implements ContactRepository {

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

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

以下示例显示了扩展了CrudRepository的自定义存储库的界面:

示例 29.对您的存储库界面的更改

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

  // Declare query methods here
}

存储库可能由多个自定义实现组成,这些自定义实现按其声明 Sequences 导入。定制实现比基础实现和存储库方面的优先级更高。通过此排序,可以覆盖基本存储库和方面方法,并在两个片段贡献相同方法签名的情况下解决歧义。存储库片段不限于在单个存储库界面中使用。多个存储库可以使用片段接口,使您可以跨不同的存储库重用自定义项。

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

例子 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) {
    // Your custom implementation
  }
}

以下示例显示了使用上述存储库片段的存储库:

例子 31.定制的存储库界面

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

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

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

例子 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 名称来标识要使用的那个。

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

例子 33.歧义实现的解决

package com.acme.impl.one;

class CustomizedUserRepositoryImpl implements CustomizedUserRepository {

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

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

  // Your custom implementation
}

如果用@Component("specialCustom")CommentsUserRepository接口,则 Bean 名称加Impl会匹配com.acme.impl.two中为存储库实现定义的名称,并且将使用它代替第一个。

Manual Wiring

如果您的自定义实现仅使用基于 Comments 的配置和自动装配,则上述显示的方法可以很好地工作,因为它被视为其他任何 Spring bean。如果实现片段 bean 需要特殊的接线,则可以声明 Bean 并根据preceding section中描述的约定对其进行命名。然后,基础结构通过名称引用手动定义的 bean 定义,而不是自己创建一个。以下示例显示如何手动连接自定义实现:

例子 34.定制实现的手工接线

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

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

4.6.2. 自定义基础存储库

当您要自定义基本存储库行为时,preceding section中描述的方法要求自定义每个存储库接口,以使所有存储库均受到影响。要改为更改所有存储库的行为,您可以创建一个实现,以扩展特定于持久性技术的存储库 Base Class。然后,该类充当存储库代理的自定义 Base Class,如以下示例所示:

例子 35.定制存储库 Base Class

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

  private final EntityManager entityManager;

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

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

  @Transactional
  public <S extends T> S save(S entity) {
    // implementation goes here
  }
}

Warning

该类需要具有特定于存储库的存储库工厂实现使用的超类的构造函数。如果存储库 Base Class 具有多个构造函数,则使用EntityInformation加上 Store 特定的基础结构对象(例如EntityManager或模板类)覆盖该构造函数。

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

例子 36.使用 JavaConfig 配置一个定制的仓库 Base Class

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

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

例子 37.使用 XML 配置一个定制的存储库 Base Class

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

4.7. 从汇总根发布事件

由存储库 Management 的实体是聚合根。在域驱动的设计应用程序中,这些聚合根通常发布域事件。 Spring Data 提供了一个名为@DomainEvents的 Comments,您可以在聚合根的方法上使用该 Comments,以使该发布尽可能容易,如以下示例所示:

例子 38.从聚合根公开域事件

class AnAggregateRoot {

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

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

每次调用 Spring Data 存储库的save(…)方法之一时,就会调用这些方法。

4.8. Spring 数据扩展

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

4.8.1. Querydsl 扩展

Querydsl是一个框架,可通过其流畅的 API 构造静态类型的类似 SQL 的查询。

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

例子 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)

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

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

例子 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);

4.8.2. 网路支援

Note

本部分包含 Spring Data Web 支持的文档,该文档在 Spring Data Commons 的当前(和更高版本)中实现。随着新引入的支持发生了许多变化,我们将以前的行为的文档保存在[web.legacy]中。

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

例子 41.启用 Spring Data Web 支持

@Configuration
@EnableWebMvc
@EnableSpringDataWebSupport
class WebConfiguration {}

@EnableSpringDataWebSupportComments 注册了一些我们稍后将讨论的组件。它还将在 Classpath 上检测 Spring HATEOAS,并为其注册集成组件(如果存在)。

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

例子 42.在 XML 中启用 Spring Data Web 支持

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

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

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

DomainClassConverter

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

例子 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(…)来访问该实例来解决该实例。

Note

当前,存储库必须实现CrudRepository才能被发现以进行转换。

用于分页和排序的 HandlerMethodArgumentResolvers

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

例子 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实例:

表 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()方法,然后导入自定义的配置文件,而不使用@EnableComments。

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

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

您必须填充thing1_pagething2_page,依此类推。

传递给该方法的默认Pageable等效于PageRequest.of(0, 20),但可以使用Pageable参数上的@PageableDefaultComments 进行自定义。

超媒体对分页的支持

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

例子 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可能会附加prevnext链接,具体取决于页面的状态。链接指向方法 Map 到的 URI。添加到该方法的分页参数与PageableHandlerMethodArgumentResolver的设置匹配,以确保以后可以解析链接。

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

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

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

Web 数据绑定支持

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

例子 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。这样,可以轻松缓解源文档结构的更改,而无需 Client 端调用公开的方法(通常是基于类的有效负载绑定的缺点)。

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

对于 Spring MVC,一旦@EnableSpringDataWebSupport处于活动状态,所需的转换器就会自动注册,并且所需的依赖项在 Classpath 上可用。要与RestTemplate结合使用,请手动注册ProjectingJackson2HttpMessageConverter(JSON)或XmlBeamHttpMessageConverter

有关更多信息,请参见规范Spring 数据示例存储库中的网络投影示例

Querydsl Web 支持

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

考虑以下查询字符串:

?firstname=Dave&lastname=Matthews

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

QUser.user.firstname.eq("Dave").and(QUser.user.lastname.eq("Matthews"))

Note

当在 Classpath 中找到 Querydsl 时,该功能将与@EnableSpringDataWebSupport一起自动启用。

在方法签名中添加@QuerydslPredicate即可使用的Predicate,可以使用QuerydslPredicateExecutor来运行。

Tip

类型信息通常从方法的返回类型中解析。由于该信息不一定与域类型匹配,因此最好使用QuerydslPredicateroot属性。

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

默认绑定如下:

  • Object用作eq的简单属性。

  • Object收集诸如contains之类的属性。

  • Collection用作in的简单属性。

可以通过@QuerydslPredicatebindings属性或通过使用 Java 8 default methods并将QuerydslBinderCustomizer方法添加到存储库接口来定制那些绑定。

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

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

    bindings.bind(user.username).first((path, value) -> path.contains(value))    (3)
    bindings.bind(String.class)
      .first((StringPath path, String value) -> path.containsIgnoreCase(value)); (4)
    bindings.excluding(user.password);                                           (5)
  }
}
  • (1) QuerydslPredicateExecutor提供对Predicate的特定查找器方法的访问权限。
  • (2) 在存储库界面上定义的QuerydslBinderCustomizer会自动显示,快捷方式@QuerydslPredicate(bindings=…)
  • (3)username属性的绑定定义为简单的contains绑定。
  • (4)String属性的默认绑定定义为不区分大小写的contains匹配。
  • (5)Predicate分辨率中排除password属性。

4.8.3. 存储库填充器

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

假设您有一个具有以下内容的文件data.json

例子 47.用 JSON 定义的数据

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

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

例子 48.声明一个 Jackson 存储库填充器

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xmlns:repository="http://www.springframework.org/schema/data/repository"
  xsi:schemaLocation="http://www.springframework.org/schema/beans
    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 对象解组到的类型。基础结构最终选择适当的存储库来处理反序列化的对象。

要改为使用 XML 定义应使用存储库填充的数据,可以使用unmarshaller-populator元素。您可以将其配置为使用 Spring OXM 中可用的 XML marshaller 选项之一。有关详情,请参见Spring 参考文档。以下示例显示如何使用 JAXB 解组存储库填充器:

例子 49.声明一个解组存储库填充器(使用 JAXB)

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xmlns:repository="http://www.springframework.org/schema/data/repository"
  xmlns:oxm="http://www.springframework.org/schema/oxm"
  xsi:schemaLocation="http://www.springframework.org/schema/beans
    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>

Reference Documentation

5. JPA 存储库

本章指出了 JPA 信息库支持的特长。这构建在“ 使用 Spring 数据存储库”中解释的核心存储库支持的基础上。确保您对这里介绍的基本概念有很好的理解。

5.1. Introduction

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

5.1.1. Spring 命名空间

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

例子 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 存储库。除此之外,它还为所有带有@RepositoryComments 的 bean 激活持久性异常转换,以使 JPA 持久性提供程序引发的异常转换为 Spring 的DataAccessException层次结构。

自定义命名空间属性

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

表 2. repositories元素的特定于 JPA 的定制属性

entity-manager-factory-ref明确地将要使用的EntityManagerFactoryrepositories元素检测到的存储库进行连线。通常在应用程序中使用多个EntityManagerFactory bean 的情况下使用。如果未配置,Spring Data 会在ApplicationContext中自动查找名称为entityManagerFactoryEntityManagerFactory bean。
transaction-manager-ref明确地将要使用的PlatformTransactionManagerrepositories元素检测到的存储库进行连线。通常仅在配置了多个事务 Management 器或EntityManagerFactory bean 时才需要。默认为当前ApplicationContext内部的单个已定义PlatformTransactionManager

Note

如果未定义显式transaction-manager-ref,Spring Data JPA 要求提供一个名为transactionManagerPlatformTransactionManager bean。

5.1.2. 基于 Comments 的配置

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

例子 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;
  }
}

Note

您必须直接创建LocalContainerEntityManagerFactoryBean而不是EntityManagerFactory,因为前者除了创建EntityManagerFactory之外还参与异常转换机制。

前面的配置类通过使用spring-jdbcEmbeddedDatabaseBuilder API 来设置嵌入式 HSQL 数据库。然后,Spring Data 设置EntityManagerFactory并将 Hibernate 用作示例持久性提供程序。在此声明的最后一个基础结构组件是JpaTransactionManager。最后,该示例通过使用@EnableJpaRepositoriesComments 激活 Spring Data JPA 存储库,该 Comments 实质上具有与 XML 名称空间相同的属性。如果未配置任何基本程序包,它将使用配置类所在的程序包。

5.1.3. 引导模式

默认情况下,Spring Data JPA 存储库是默认的 Spring Bean。它们是单例作用域的,并且急于初始化。在启动期间,他们已经与 JPA EntityManager进行交互,以进行验证和元数据分析。 Spring Framework 在后台线程中支持 JPA EntityManagerFactory的初始化,因为该过程通常在 Spring 应用程序中占用大量启动时间。为了有效地利用该后台初始化,我们需要确保 JPA 存储库尽可能早地初始化。

从 Spring Data JPA 2.1 开始,您现在可以配置BootstrapMode(通过@EnableJpaRepositoriesComments 或 XML 名称空间),该BootstrapMode具有以下值:

  • DEFAULT(默认)-急切地实例化存储库,除非用@Lazy显式 Comments。仅当没有任何 ClientBean 需要存储库实例时,lazification 才有效,因为这将需要初始化存储库 bean。

  • LAZY —隐式地声明所有存储库 bean 都是惰性的,并且还使创建的惰性初始化代理被注入到 Client 端 bean 中。这意味着,如果 Client 机 bean 仅将实例存储在字段中并且在初始化期间不使用存储库,则不会实例化存储库。首次与存储库交互时,将初始化和验证存储库实例。

  • DEFERRED —与LAZY基本相同的操作模式,但响应ContextRefreshedEvent触发存储库初始化,以便在应用程序完全启动之前验证存储库。

Recommendations

如果您不使用默认引导方式的异步 JPA 引导棒。

如果您以异步方式引导 JPA,则DEFERRED是一个合理的默认值,因为它可以确保 Spring Data JPA 引导仅 awaitEntityManagerFactory设置(如果其花费的时间比初始化所有其他应用程序组件所需的时间长)。尽管如此,它仍可以确保在应用程序发出 signal 之前,对存储库进行了正确的初始化和验证。

LAZY是测试方案和本地开发的不错选择。一旦确定了存储库将正确引导后,或者在测试应用程序的其他部分时,对所有存储库执行验证可能只会不必要地增加启动时间。这同样适用于本地开发,在本地开发中,您仅访问应用程序的某些部分,而这些部分可能只需要初始化一个存储库即可。

5.2. 持久实体

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

5.2.1. 保存实体

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

实体状态检测策略

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

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

  • 实现Persistable:如果实体实现Persistable,则 Spring Data JPA 将新的检测委托给该实体的isNew(…)方法。有关详情,请参见JavaDoc

  • 实现EntityInformation:通过创建JpaRepositoryFactory的子类并相应地覆盖getEntityInformation(…)方法,可以自定义SimpleJpaRepository实现中使用的EntityInformation抽象。然后,您必须将JpaRepositoryFactory的自定义实现注册为 Spring bean。请注意,这几乎没有必要。有关详情,请参见JavaDoc

5.3. 查询方法

本节描述了使用 Spring Data JPA 创建查询的各种方法。

5.3.1. 查询查询策略

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

Declared Queries

尽管从方法名派生一个查询很方便,但可能会遇到这样一种情况,即方法名解析器不支持一个人想使用的关键字,或者方法名不必要地变得丑陋。因此,您可以通过命名约定使用 JPA 命名查询(有关更多信息,请参见使用 JPA 命名查询),或者可以通过@QueryComments 您的查询方法(有关详细信息,请参见Using @Query)。

5.3.2. 查询创建

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

例子 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 进行属性检查并遍历嵌套的属性,如“ Property Expressions”中所述。

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

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

KeywordSampleJPQL snippet
AndfindByLastnameAndFirstname… where x.lastname = ?1 and x.firstname = ?2
OrfindByLastnameOrFirstname… where x.lastname = ?1 or x.firstname = ?2
Is,EqualsfindByFirstname , findByFirstnameIs , findByFirstnameEquals… where x.firstname = ?1
BetweenfindByStartDateBetween… where x.startDate between ?1 and ?2
LessThanfindByAgeLessThan… where x.age < ?1
LessThanEqualfindByAgeLessThanEqual… where x.age <= ?1
GreaterThanfindByAgeGreaterThan… where x.age > ?1
GreaterThanEqualfindByAgeGreaterThanEqual… where x.age >= ?1
AfterfindByStartDateAfter… where x.startDate > ?1
BeforefindByStartDateBefore… where x.startDate < ?1
IsNullfindByAgeIsNull… where x.age is null
IsNotNull,NotNullfindByAge(Is)NotNull… where x.age not null
LikefindByFirstnameLike… where x.firstname like ?1
NotLikefindByFirstnameNotLike… where x.firstname not like ?1
StartingWithfindByFirstnameStartingWith… where x.firstname like ?1(参数附加了%)
EndingWithfindByFirstnameEndingWith… where x.firstname like ?1(参数与前缀%绑定)
ContainingfindByFirstnameContaining… where x.firstname like ?1(参数绑定在%中)
OrderByfindByAgeOrderByLastnameDesc… where x.age = ?1 order by x.lastname desc
NotfindByLastnameNot… where x.lastname <> ?1
InfindByAgeIn(Collection<Age> ages)… where x.age in ?1
NotInfindByAgeNotIn(Collection<Age> ages)… where x.age not in ?1
TruefindByActiveTrue()… where x.active = true
FalsefindByActiveFalse()… where x.active = false
IgnoreCasefindByFirstnameIgnoreCase… where UPPER(x.firstame) = UPPER(?1)

Note

InNotIn还将Collection的任何子类作为参数以及数组或 varargs。对于同一逻辑运算符的其他语法版本,请选中“ Repositories 查询关键字”。

5.3.3. 使用 JPA 命名查询

Note

这些示例使用<named-query />元素和@NamedQueryComments。这些配置元素的查询必须用 JPA 查询语言定义。当然,您也可以使用<named-native-query />@NamedNativeQuery。这些元素使您可以通过失去数据库平台独立性来在本机 SQL 中定义查询。

XML 命名查询定义

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

例子 53. XML 命名查询配置

<named-query name="User.findByLastname">
  <query>select u from User u where u.lastname = ?1</query>
</named-query>

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

Annotation-based Configuration

基于 Comments 的配置的优点是不需要编辑另一个配置文件,从而减少了维护工作。您需要为每个新的查询声明重新编译域类,从而为此付出了代价。

例子 54.基于 Comments 的命名查询配置

@Entity
@NamedQuery(name = "User.findByEmailAddress",
  query = "select u from User u where u.emailAddress = ?1")
public class User {

}
Declaring Interfaces

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

例子 55. UserRepository 中的查询方法声明

public interface UserRepository extends JpaRepository<User, Long> {

  List<User> findByLastname(String lastname);

  User findByEmailAddress(String emailAddress);
}

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

5.3.4. 使用@Query

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

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

以下示例显示了使用@QueryComments 创建的查询:

例子 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表达式,如以下示例所示:

例子 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模式进行扩充。

Native Queries

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

例子 58.使用@Query 在查询方法中声明一个本地查询

public interface UserRepository extends JpaRepository<User, Long> {

  @Query(value = "SELECT * FROM USERS WHERE EMAIL_ADDRESS = ?1", nativeQuery = true)
  User findByEmailAddress(String emailAddress);
}

Note

Spring Data JPA 当前不支持对本机查询进行动态排序,因为它必须操纵声明的实际查询,而这对于本机 SQL 无法可靠地完成。但是,您可以通过自己指定 count 查询来使用本机查询进行分页,如以下示例所示:

例子 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后缀添加到查询副本中,类似的方法也可用于命名本地查询。不过,您可能需要为计数查询注册结果集 Map。

5.3.5. 使用排序

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

Note

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

但是,结合使用Sort@Query可以让您潜入包含ORDER BY子句中的函数的未经路径检查的Order实例。这是可能的,因为Order附加到给定的查询字符串。默认情况下,Spring Data JPA 拒绝任何包含函数调用的Order实例,但是您可以使用JpaSort.unsafe添加潜在的不安全排序。

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

例子 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) 包含函数调用的Sort无效。 Thows 异常。
  • (3) 有效Sort明确包含不安全 Order
  • (4) 指向别名函数的有效Sort表达式。

5.3.6. 使用命名参数

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

例子 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);
}

Note

方法参数根据其在定义的查询中的 Sequences 进行切换。

Note

从版本 4 开始,Spring 完全支持基于-parameters编译器标志的 Java 8 参数名称发现。通过在构建中使用此标志作为调试信息的替代方法,可以省略命名参数的@Param注解。

5.3.7. 使用 SpEL 表达式

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

以下示例演示了查询字符串中#{#entityName}表达式的一种用例,您想在其中使用查询方法和手动定义的查询来定义存储库接口:

例子 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}变量。

Note

可以使用@EntityComments 来自定义entityName。 SpEL 表达式不支持orm.xml中的自定义。

当然,您可能只是直接在查询声明中使用了User,但这也需要您更改查询。对#entityName的引用将User类将来可能的重新 Map 选择为另一个实体名称(例如,通过使用@Entity(name = "MyUser"))。

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

示例 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上调用findByAllAttribute(…),则查询变为select t from ConcreteType t where t.attribute = ?1

5.3.8. 修改查询

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

例子 64.声明操作查询

@Modifying
@Query("update User u set u.firstname = ?1 where u.lastname = ?2")
int setFixedFirstnameFor(String firstname, String lastname);

这样做将触发 Comments 该方法的查询作为更新查询,而不是选择查询。由于EntityManager在执行修改查询后可能包含过时的实体,因此我们不会自动清除它(有关详细信息,请参见EntityManager.clear()JavaDoc),因为这实际上会将所有尚未刷新的更改丢弃在EntityManager中。如果希望自动清除EntityManager,则可以将@ModifyingComments 的clearAutomatically属性设置为true

@Modifying注解仅与@Query注解结合使用。派生的查询方法或自定义方法不需要此 Comments。

派生删除查询

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

例子 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(…)方法的实现保持同步的快捷方式。

5.3.9. 应用查询提示

要将 JPA 查询提示应用于在存储库接口中声明的查询,可以使用@QueryHints注解。它需要一个 JPA @QueryHintComments 数组和一个布尔标志,以潜在地禁用应用于应用分页时触发的附加计数查询的提示,如以下示例所示:

例子 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,但是省略了将其应用于为计算总页数而触发的计数查询。

5.3.10. 配置提取和加载图

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

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

例子 67.在一个实体上定义一个命名实体图。

@Entity
@NamedEntityGraph(name = "GroupInfo.detail",
  attributeNodes = @NamedAttributeNode("members"))
public class GroupInfo {

  // default fetch mode is lazy.
  @ManyToMany
  List<GroupMember> members = new ArrayList<GroupMember>();

  …
}

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

示例 68.在存储库查询方法上引用命名实体图定义。

@Repository
public interface GroupRepository extends CrudRepository<GroupInfo, String> {

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

}

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

示例 69.在存储库查询方法上使用 AD-HOC 实体图定义。

@Repository
public interface GroupRepository extends CrudRepository<GroupInfo, String> {

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

}

5.3.11. Projections

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

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

例子 70.一个 samples 集合和存储库

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 提供什么手段来实现这一目标?本章其余部分将回答该问题。

Interface-based Projections

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

例子 71.一个投影界面来检索属性的子集

interface NamesOnly {

  String getFirstname();
  String getLastname();
}

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

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

interface PersonRepository extends Repository<Person, UUID> {

  Collection<NamesOnly> findByLastname(String lastname);
}

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

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

例子 73.一个投影界面来检索属性的子集

interface PersonSummary {

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

  interface AddressSummary {
    String getCity();
  }
}

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

Closed Projections

其访问者方法均与目标集合的属性完全匹配的投影接口被视为封闭投影。下面的示例(也在本章前面使用过)是一个封闭的投影:

例子 74.一个封闭的投影

interface NamesOnly {

  String getFirstname();
  String getLastname();
}

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

Open Projections

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

例子 75.一个开放的投影

interface NamesOnly {

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

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

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

例子 76.使用默认方法自定义逻辑的投影接口

interface NamesOnly {

  String getFirstname();
  String getLastname();

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

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

例子 77.samples 人对象

@Component
class MyBean {

  String getFullName(Person person) {
    …
  }
}

interface NamesOnly {

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

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

例子 78. Sample Person 对象

interface NamesOnly {

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

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

基于类的投影(DTO)

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

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

以下示例显示了一个预计的 DTO:

例子 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(…) and hashCode() implementations
}

Avoid boilerplate code for projection DTOs

您可以使用Project Lombok大大简化 DTO 的代码,该Project Lombok提供@ValueComments(不要与前面的界面示例中显示的 Spring 的@ValueComments 混淆)。如果您使用 Project Lombok 的@Value注解,则前面显示的示例 DTO 将变为以下内容:

@Value
class NamesOnly {
	String firstname, lastname;
}

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

Dynamic Projections

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

例子 80.使用动态投影参数的存储库

interface PersonRepository extends Repository<Person, UUID> {

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

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

例子 81.使用带有动态投影的存储库

void someMethod(PersonRepository people) {

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

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

5.4. 存储过程

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

下面的示例使用以下过程:

示例 82. HSQL DB 中的plus1inout过程的定义。

/;
DROP procedure IF EXISTS plus1inout
/;
CREATE procedure plus1inout (IN arg int, OUT res int)
BEGIN ATOMIC
 set res = arg + 1;
END
/;

可以通过在实体类型上使用NamedStoredProcedureQueryComments 来配置存储过程的元数据。

例子 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属性间接地定义。如果未配置任何名称,则将存储库方法的名称用作备用。

下面的示例演示如何引用显式 Map 的过程:

示例 84.在数据库中引用名称为“ plus1inout”的显式 Map 过程。

@Procedure("plus1inout")
Integer explicitlyNamedPlus1inout(Integer arg);

下面的示例演示如何使用procedureName别名引用隐式 Map 的过程:

示例 85.通过procedureName别名在数据库中引用名称为“ plus1inout”的隐式 Map 过程。

@Procedure(procedureName = "plus1inout")
Integer plus1inout(Integer arg);

下面的示例演示如何在EntityManager中引用显式 Map 的命名过程:

例子 86.在EntityManager中引用显式 Map 的命名存储过程“ User.plus1IO”。

@Procedure(name = "User.plus1IO")
Integer entityAnnotatedCustomNamedProcedurePlus1IO(@Param("arg") Integer arg);

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

例子 87.使用方法名在EntityManager中引用隐式 Map 的命名存储过程“ User.plus1”。

@Procedure
Integer plus1(@Param("arg") Integer arg);

5.5. Specifications

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

Spring Data JPA 遵循 Eric Evans 的书“域驱动设计”中的规范概念,遵循相同的语义,并提供了使用 JPA 标准 API 定义此类规范的 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组合并使用,而无需为每个所需的组合声明查询(方法),如以下示例所示:

例子 88.Client 规范

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(MonetaryAmount value) {
    return new Specification<Customer>() {
      public Predicate toPredicate(Root<T> root, CriteriaQuery<?> query,
            CriteriaBuilder builder) {

         // build query here
      }
    };
  }
}

诚然,样板文件的数量尚有待改进(最终可能会因 Java 8 闭包而减少),但 Client 端会变得更好,正如您将在本节后面看到的那样。 _Customer类型是使用 JPA 元模型生成器生成的元模型类型(请参见Hibernate 实现的文档示例)。因此,表达式_Customer.createdAt假定Customer具有类型DatecreatedAt属性。除此之外,我们在业务需求抽象级别上表达了一些标准,并创建了可执行文件Specifications。因此,Client 端可以使用Specification,如下所示:

例子 89.使用一个简单的规范

List<Customer> customers = customerRepository.findAll(isLongTermCustomer());

为什么不为这种数据访问创建查询?与简单的查询声明相比,使用单个Specification不会带来很多好处。当您将它们组合在一起以创建新的Specification对象时,规范的力量 true 发挥了作用。您可以通过提供的Specification默认方法来构建类似于以下内容的表达式来实现此目的:

例子 90.组合规格

MonetaryAmount amount = new MonetaryAmount(200.0, Currencies.DOLLAR);
List<Customer> customers = customerRepository.findAll(
  isLongTermCustomer().or(hasSalesOfMoreThan(amount)));

Specification提供了一些“胶水编码”默认方法来链接和组合Specification个实例。这些方法使您可以通过创建新的Specification实现并将其与现有的实现组合来扩展数据访问层。

5.6. 实例查询

5.6.1. Introduction

本章对“示例查询”进行了介绍,并说明了如何使用它。

示例查询(QBE)是一种具有简单界面的用户友好查询技术。它允许动态查询创建,并且不需要您编写包含字段名称的查询。实际上,“示例查询”根本不需要您使用 Store 特定的查询语言编写查询。

5.6.2. Usage

按示例查询 API 包含三部分:

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

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

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

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

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

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

  • 独立于基础数据存储区 API 进行工作。

按示例查询也有一些限制:

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

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

在开始使用“示例查询”之前,您需要具有一个域对象。首先,为您的存储库创建一个接口,如以下示例所示:

例子 91.samples 人对象

public class Person {

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

  // … getters and setters omitted
}

前面的示例显示了一个简单的域对象。您可以使用它来创建Example。默认情况下,具有null值的字段将被忽略,并且使用 Store 特定的默认值来匹配字符串。可以使用of factory 方法或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界面的摘录:

例子 93. QueryByExampleExecutor

public interface QueryByExampleExecutor<T> {

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

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

  // … more functionality omitted.
}

5.6.3. 示例匹配器

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

例子 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属性路径并包含空值。
  • (6) 构造一个新的ExampleMatcher来忽略lastname属性路径,包括空值并执行后缀字符串匹配。
  • (7) 基于域对象和配置的ExampleMatcher创建一个新的Example

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

您可以为单个属性(例如“名字”和“姓氏”,或者对于嵌套属性,“ address.city”)指定行为。您可以使用匹配选项和区分大小写对其进行调整,如以下示例所示:

例子 95.配置匹配器选项

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

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

例子 96.用 lambdas 配置匹配器选项

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

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

表 4. ExampleMatcher设置的范围

SettingScope
Null-handlingExampleMatcher
String matchingExampleMatcher和属性路径
Ignoring propertiesProperty path
Case sensitivityExampleMatcher和属性路径
Value transformationProperty path

5.6.4. 执行一个例子

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

示例 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));
  }
}

Note

当前,只有SingularAttribute个属性可用于属性匹配。

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

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

表 5. StringMatcher选项

MatchingLogical result
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) + '%'

5.7. Transactionality

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

例子 98. CRUD 的自定义事务配置

public interface UserRepository extends CrudRepository<User, Long> {

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

  // Further query method declarations
}

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

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

例子 99.使用外观定义多个存储库调用的事务

@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才能使立面的基于 Comments 的配置生效。本示例假定您使用组件扫描。

5.7.1. 事务查询方法

要使查询方法具有事务性,请在您定义的存储库界面上使用@Transactional,如以下示例所示:

例子 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()使用@ModifyingComments 并覆盖事务配置。因此,该方法在readOnly标志设置为false的情况下运行。

Note

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

5.8. Locking

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

例子 101.在查询方法上定义锁元数据

interface UserRepository extends Repository<User, Long> {

  // Plain query method
  @Lock(LockModeType.READ)
  List<User> findByLastname(String lastname);
}

此方法声明使被触发的查询配备LockModeTypeREAD。您还可以通过在存储库界面中重新声明 CRUD 方法并添加@LockComments 来定义 CRUD 方法的锁定,如以下示例所示:

例子 102.在 CRUD 方法上定义锁元数据

interface UserRepository extends Repository<User, Long> {

  // Redeclaration of a CRUD method
  @Lock(LockModeType.READ);
  List<User> findAll();
}

5.9. Auditing

5.9.1. Basics

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

基于 Comments 的审核元数据

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

例子 103.被审计实体

class Customer {

  @CreatedBy
  private User user;

  @CreatedDate
  private DateTime createdDate;

  // … further properties omitted
}

如您所见,可以根据要捕获的信息有选择地应用 Comments。进行更改时捕获的 Comments 可以用于 Joda-Time 类型,DateTime,旧版 Java DateCalendar,JDK8 日期和时间类型以及longLong的属性。

基于接口的审核元数据

如果您不想使用 Comments 来定义审核元数据,则可以让您的域类实现Auditable接口。它公开了所有审核属性的设置器方法。

还有一个方便的 Base ClassAbstractAuditable,您可以对其进行扩展,以避免需要手动实现接口方法。这样做会增加您的域类与 Spring Data 的耦合,这可能是您要避免的事情。通常,首选基于 Comments 的方式来定义审计元数据,因为它侵入性较小且更灵活。

AuditorAware

如果您使用@CreatedBy@LastModifiedBy,则审计基础结构需要以某种方式了解当前的主体。为此,我们提供了一个AuditorAware<T> SPI 接口,您必须实现该接口来告知基础结构与应用程序交互的当前用户或系统是谁。通用类型T定义必须使用@CreatedBy@LastModifiedByComments 的属性的类型。

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

例子 104.基于 Spring Security 的 AuditorAware 的实现

class SpringSecurityAuditorAware implements AuditorAware<User> {

  public Optional<User> getCurrentAuditor() {

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

该实现访问 Spring Security 提供的Authentication对象,并查找您在UserDetailsService实现中创建的自定义UserDetails实例。我们在这里假设您是通过UserDetails实现公开域用户的,但是根据找到的Authentication,您还可以从任何地方查找它。 :leveloffset:-1

5.9.2. JPA 审核

常规审核配置

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

例子 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>

您还可以使用@EntityListenersComments 按实体启用AuditingEntityListener,如下所示:

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

}

Note

审核功能要求spring-aspects.jar处于 Classpath 中。

在 Classpath 上适当地修改了orm.xml且在spring-aspects.jar上进行了适当的修改后,激活审核功能只需将 Spring Data JPA auditing名称空间元素添加到您的配置中,如下所示:

例子 106.使用 XML 配置激活审计

<jpa:auditing auditor-aware-ref="yourAuditorAwareBean" />

从 Spring Data JPA 1.5 开始,您可以通过使用@EnableJpaAuditingComments 对配置类进行 Comments 来启用审核。您仍然必须修改orm.xml文件,并在 Classpath 上使用spring-aspects.jar。以下示例显示了如何使用@EnableJpaAuditing注解:

例子 107.用 Java 配置激活审计

@Configuration
@EnableJpaAuditing
class Config {

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

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

5.10. 其他注意事项

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

当使用多个EntityManager实例和定制存储库实现时,您需要将正确的EntityManager连接到存储库实现类中。您可以通过在@PersistenceContextComments 中明确命名EntityManager来实现,或者,如果EntityManager@Autowired,则可以使用@Qualifier来命名。

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

例 108.在自定义存储库实现中使用JpaContext

class UserRepositoryImpl implements UserRepositoryCustom {

  private final EntityManager em;

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

  …
}

这种方法的优点是,如果将域类型分配给其他持久性单元,则无需触摸存储库即可更改对持久性单元的引用。

5.10.2. 合并持久性单元

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

例子 109.使用 MergingPersistenceUnitmanager

<bean class="….LocalContainerEntityManagerFactoryBean">
  <property name="persistenceUnitManager">
    <bean class="….MergingPersistenceUnitManager" />
  </property>
</bean>
@Entity 类和 JPAMap 文件的 Classpath 扫描

普通的 JPA 设置要求所有 CommentsMap 的实体类都在orm.xml中列出。 XMLMap 文件也是如此。 Spring Data JPA 提供了一个ClasspathScanningPersistenceUnitPostProcessor,该ClasspathScanningPersistenceUnitPostProcessor被配置了基本包,并且可以选择采用 Map 文件名模式。然后,它在给定的软件包中扫描以@Entity@MappedSuperclassComments 的类,加载与文件名模式匹配的配置文件,并将其交给 JPA 配置。后处理器必须配置如下:

例子 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>

Note

从 Spring 3.1 开始,可以直接在LocalContainerEntityManagerFactoryBean上配置要扫描的程序包,以启用对实体类的 Classpath 扫描。有关详情,请参见JavaDoc

5.10.3. CDI 整合

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

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

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。

每当容器请求存储库类型的 Bean 时,Spring Data JPA CDI 扩展都将所有可用的EntityManager实例作为 CDI bean 并创建一个 Spring Data 存储库的代理。因此,获取 Spring Data 存储库的实例只需声明@Injected属性即可,如以下示例所示:

class RepositoryClient {

  @Inject
  PersonRepository repository;

  public void businessMethod() {
    List<Person> people = repository.findAll();
  }
}

Appendix

附录 A:命名空间参考

<repositories />元素

<repositories />元素触发 Spring 数据存储库基础结构的设置。最重要的属性是base-package,它定义用于扫描 Spring Data 仓库接口的包。请参阅“ XML configuration”。下表描述了<repositories />元素的属性:

表 6.属性

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

附录 B:填充器名称空间参考

<populator />元素

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

表 7.属性

NameDescription
locations应该在哪里找到文件以从存储库中读取对象。

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

支持的查询关键字

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

表 8.查询关键字

Logical keywordKeyword expressions
ANDAnd
OROr
AFTERAfter , IsAfter
BEFOREBefore , IsBefore
CONTAININGContaining , IsContaining , Contains
BETWEENBetween , IsBetween
ENDING_WITHEndingWith , IsEndingWith , EndsWith
EXISTSExists
FALSEFalse , IsFalse
GREATER_THANGreaterThan , IsGreaterThan
GREATER_THAN_EQUALSGreaterThanEqual , IsGreaterThanEqual
INIn , IsIn
ISIsEquals,(或没有关键字)
IS_EMPTYIsEmpty , Empty
IS_NOT_EMPTYIsNotEmpty , NotEmpty
IS_NOT_NULLNotNull , IsNotNull
IS_NULLNull , IsNull
LESS_THANLessThan , IsLessThan
LESS_THAN_EQUALLessThanEqual , IsLessThanEqual
LIKELike , IsLike
NEARNear , IsNear
NOTNot , IsNot
NOT_INNotIn , IsNotIn
NOT_LIKENotLike , IsNotLike
REGEXRegex , MatchesRegex , Matches
STARTING_WITHStartingWith , IsStartingWith , StartsWith
TRUETrue , IsTrue
WITHINWithin , IsWithin

附录 D:Repositories 查询返回类型

支持的查询返回类型

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

Note

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

表 9.查询返回类型

Return typeDescription
void表示没有返回值。
PrimitivesJava primitives.
Wrapper typesJava 包装器类型。
T唯一实体。期望查询方法最多返回一个结果。如果未找到结果,则返回null。一个以上的结果触发IncorrectResultSizeDataAccessException
Iterator<T>一个Iterator
Collection<T>A Collection
List<T>A List
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>A Future。期望使用@AsyncComments 方法,并且需要启用 Spring 的异步方法执行功能。
CompletableFuture<T>Java 8 CompletableFuture。期望使用@AsyncComments 方法,并且需要启用 Spring 的异步方法执行功能。
ListenableFutureA org.springframework.util.concurrent.ListenableFuture。期望使用@AsyncComments 方法,并且需要启用 Spring 的异步方法执行功能。
Slice一定大小的数据块,用于指示是否有更多可用数据。需要Pageable方法参数。
Page<T>Slice以及其他信息,例如结果总数。需要Pageable方法参数。
GeoResult<T>具有附加信息(例如到参考位置的距离)的结果条目。
GeoResults<T>GeoResult<T>列表以及其他信息,例如到参考位置的平均距离。
GeoPage<T>PageGeoResult<T>,例如到参考位置的平均距离。
Mono<T>使用 Reactive 存储库的 Project Reactor Mono发出零或一个元素。期望查询方法最多返回一个结果。如果未找到结果,则返回Mono.empty()。多个结果触发IncorrectResultSizeDataAccessException
Flux<T>Project Reactor Flux使用 Reactive 存储库发出零,一个或多个元素。返回Flux的查询也可以发出无限数量的元素。
Single<T>使用 Reactive 存储库发出单个元素的 RxJava Single。期望查询方法最多返回一个结果。如果未找到结果,则返回Mono.empty()。多个结果触发IncorrectResultSizeDataAccessException
Maybe<T>使用 Reactive 存储库的 RxJava Maybe发出零或一个元素。期望查询方法最多返回一个结果。如果未找到结果,则返回Mono.empty()。多个结果触发IncorrectResultSizeDataAccessException
Flowable<T>RxJava Flowable使用 Reactive 存储库发出零个,一个或多个元素。返回Flowable的查询也可以发出无限数量的元素。

附录 E:常见问题

Common

  • 我想获得有关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>

Infrastructure

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

您必须将AnnotationSessionFactoryBean替换为HibernateJpaSessionFactoryBean,如下所示:

例子 111.从一个HibernateEntityManagerFactory中查找一个SessionFactory

<bean id="sessionFactory" class="org.springframework.orm.jpa.vendor.HibernateJpaSessionFactoryBean">
  <property name="entityManagerFactory" ref="entityManagerFactory"/>
</bean>

Auditing

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

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

附录 F:词汇表

  • AOP

    • 面向方面的编程
  • Commons DBCP

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

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

    • 数据访问对象-用于将持久逻辑与要持久的对象分离的模式
  • Dependency Injection

  • EclipseLink

  • Hibernate

  • JPA

    • Java 持久性 API
  • Spring