Preface

©2018-2019 原始作者。

Note

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

Spring Data JDBC 提供了基于 JDBC 的存储库抽象。

Project Metadata

1.新功能

本节涵盖每个版本的重大更改。

1.1. Spring Data JDBC 1.0 的新增功能

  • CrudRepository的基本支持。

  • @Query支持。

  • MyBatis support.

  • Id generation.

  • Event support.

  • Auditing.

  • CustomConversions .

2. 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>

2.1. 使用 Spring Boot 进行依赖 Management

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

2.2. Spring Framework

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

3.使用 Spring 数据存储库

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

Tip

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

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

3.1. 核心概念

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

例子 3. CrudRepository界面

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

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

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

  Iterable<T> findAll();               (3)

  long count();                        (4)

  void delete(T entity);               (5)

  boolean existsById(ID primaryKey);   (6)

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

Note

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

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

例子 4. PagingAndSortingRepository界面

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

  Iterable<T> findAll(Sort sort);

  Page<T> findAll(Pageable pageable);
}

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

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

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

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

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

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

3.3. 定义存储库接口

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

3.3.1. 微调存储库定义

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

Note

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

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

例子 7.有选择地公开 CRUD 方法

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

  Optional<T> findById(ID id);

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

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

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

Note

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

3.3.2. 将存储库与多个 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)的存储库:

例子 8.使用模块特定接口的存储库定义

interface MyRepository extends JpaRepository<User, Long> { }

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

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

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

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

示例 9.使用通用接口的存储库定义

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

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

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

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

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

例子 10.使用带有 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。

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

例子 11.使用带有混合 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 驱动配置:

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

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

3.4. 定义查询方法

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

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

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

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

3.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。它首先查找一个声明的查询,如果找不到声明的查询,它将创建一个基于名称的自定义方法查询。这是默认的查找策略,因此,如果未显式配置任何内容,则使用该策略。它允许通过方法名称快速定义查询,还可以通过根据需要引入已声明的查询来自定义调整这些查询。

3.4.2. 查询创建

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

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

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

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

3.4.4. 特殊参数处理

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

例子 14.在查询方法中使用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

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

3.4.5. 限制查询结果

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

例子 15.用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”的最大元素的查询方法。

3.4.6. 存储库方法返回集合或可迭代对象

返回多个结果的查询方法可以使用标准 Java IterableListSet。除此之外,我们还支持返回 Spring Data 的Streamable,自定义 extensionsIterable以及Vavr提供的集合类型。

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

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

例子 16.使用 Streamable 合并查询方法结果

interface PersonRepository extends Repository<Person, Long> {
  Streamable<Person> findByFirstnameContaining(String firstname);
  Streamable<Person> findByLastnameContaining(String lastname);
}

Streamable<Person> result = repository.findByFirstnameContaining("av")
  .and(repository.findByLastnameContaining("ea"));
返回自定义流式包装器类型

为集合提供专用的包装器类型是一种常用的模式,用于在返回多个元素的查询执行结果上提供 API。通常,这些类型是通过调用存储库方法来返回类似集合的类型并手动创建包装类型的实例来使用的。如果 Spring Data 允许这些包装器类型满足以下条件,则可以避免使用这些包装器类型作为查询方法返回类型:

  • 该类型实现Streamable

  • 该类型公开以Streamable作为参数的名为of(…)valueOf(…)的构造函数或静态工厂方法。

示例用例如下所示:

class Product { (1)
  MonetaryAmount getPrice() { … }
}

@RequiredArgConstructor(staticName = "of")
class Products implements Streamable<Product> { (2)

  private Streamable<Product> streamable;

  public MonetaryAmount getTotal() { (3)
    return streamable.stream() //
      .map(Priced::getPrice)
      .reduce(Money.of(0), MonetaryAmount::add);
  }
}

interface ProductRepository implements Repository<Product, Long> {
  Products findAllByDescriptionContaining(String text); (4)
}
  • (1) Product实体,公开 API 以访问产品价格。
  • (2) Streamable<Product>的包装类型可以通过Products.of(…)构造(通过 Lombok 注解创建的工厂方法)。
  • (3) 包装器类型在Streamable<Product>上公开了计算新值的其他 API。
  • (4) 可以将包装器类型直接用作查询方法返回类型。无需返回Stremable<Product>并将其手动包装在存储库 Client 端中。
支持 Vavr 集合

Vavr是一个包含 Java 中的函数式编程概念的库。它附带了一组自定义的收集类型,可用作查询方法返回类型。

Vavr 收集类型使用的 Vavr 实现类型有效的 Java 源代码类型
io.vavr.collection.Seqio.vavr.collection.Listjava.util.Iterable
io.vavr.collection.Setio.vavr.collection.LinkedHashSetjava.util.Iterable
io.vavr.collection.Mapio.vavr.collection.LinkedHashMapjava.util.Map

第一列中的类型(或其子类型)可以用作查询方法返回类型,并将根据实际查询结果的 Java 类型(第三列)获取第二列中的类型作为实现类型。或者,可以声明Traversable(Vavr 等于Iterable),然后从实际的返回值派生实现类,即java.util.List将变成 Vavr List/Seqjava.util.Set变成 Vavr LinkedHashSet/Set等。

3.4.7. 存储库方法的空处理

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

  • com.google.common.base.Optional

  • scala.Option

  • io.vavr.control.Option

另外,查询方法可以选择根本不使用包装器类型。然后,通过返回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在包级别激活非空性,如以下示例所示:

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

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

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

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

例子 18.使用不同的可空性约束

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 存储库使用语言机制来定义这些约束以应用相同的运行时检查,如下所示:

例子 19.在 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

3.4.8. 流查询结果

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

例子 20.用 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,如以下示例所示:

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

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

Note

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

3.4.9. 异步查询结果

可以使用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作为返回类型。

3.5. 创建存储库实例

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

3.5.1. XML 配置

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

例子 22.通过 XML 启用 Spring 数据仓库

<?xml version="1.0" encoding="UTF-8"?>
<beans:beans xmlns:beans="http://www.springframework.org/schema/beans"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xmlns="http://www.springframework.org/schema/data/jpa"
  xsi:schemaLocation="http://www.springframework.org/schema/beans
    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,可以使用以下配置:

例子 23.使用 exclude-filter 元素

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

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

3.5.2. JavaConfig

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

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

例子 24.基于 samplesComments 的存储库配置

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

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

Note

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

3.5.3. 独立使用

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

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

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

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

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

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

3.6.1. 自定义单个存储库

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

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

interface CustomizedUserRepository {
  void someCustomMethod(User user);
}

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

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

class CustomizedUserRepositoryImpl implements CustomizedUserRepository {

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

Note

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

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

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

例子 28.对您的存储库界面的更改

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

  // Declare query methods here
}

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

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

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

例子 29.片段及其实现

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的自定义存储库的界面:

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

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

  // Declare query methods here
}

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

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

例子 31.覆盖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
  }
}

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

例子 32.定制的仓库接口

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

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

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

例子 33.配置例子

<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匹配。

例子 34.歧义实现的解决

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 定义,而不是自己创建一个。以下示例显示如何手动连接自定义实现:

例子 35.手工连接定制实现

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

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

3.6.2. 自定义基础存储库

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

例子 36.定制存储库 Base Class

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

  private final EntityManager entityManager;

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

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

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

Warning

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

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

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

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

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

例子 38.使用 XML 配置定制的仓库基础类

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

3.7. 从汇总根发布事件

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

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

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(…)方法之一时,就会调用这些方法。

3.8. Spring 数据扩展

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

3.8.1. Querydsl 扩展

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

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

例子 40. 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,如以下示例所示

例子 41.存储库上的 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. 网路支援

Note

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

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

例子 42.启用 Spring Data Web 支持

@Configuration
@EnableWebMvc
@EnableSpringDataWebSupport
class WebConfiguration {}

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

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

例子 43.在 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 控制器方法签名中使用域类型,因此您无需通过存储库手动查找实例,如以下示例所示:

例子 44.在方法签名中使用域类型的 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作为有效的控制器方法参数,如以下示例所示:

例子 45.使用 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作为控制器方法参数:

例子 46.使用 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]中描述)来绑定传入的请求有效负载,如以下示例所示:

例子 47.使用 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属性。

3.8.3. 存储库填充器

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

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

例子 48.用 JSON 定义的数据

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

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

例子 49.声明一个 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 解组存储库填充器:

例子 50.声明一个解组存储库填充器(使用 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

4. JDBC 存储库

本章指出了 JDBC 存储库支持的特殊性。这构建在使用 Spring 数据存储库中解释的核心存储库支持的基础上。您应该对这里介绍的基本概念有一个很好的了解。

4.1. 为什么选择 Spring Data JDBC?

Java 世界中用于关系数据库的主要持久性 API 当然是 JPA,它具有自己的 Spring Data 模块。为什么还有另一个?

JPA 为了帮助开发人员做了很多事情。除其他外,它跟踪对实体的更改。它为您完成了延迟加载。它使您可以将各种各样的对象构造 Map 到同样广泛的数据库设计中。

这很棒,而且使很多事情变得非常容易。只需看一下基本的 JPA 教程即可。但是,为什么 JPA 会做某件事常常使人感到困惑。此外,使用 JPA 在概念上 true 简单的事情变得相当困难。

通过包含以下设计决策,Spring Data JDBC 的目标是从概念上简化得多:

  • 如果加载实体,则会执行 SQL 语句。完成此操作后,您将拥有一个完全加载的实体。不会进行延迟加载或缓存。

  • 如果保存实体,则将保存它。如果您不这样做,则不会。没有肮脏的跟踪,也没有会话。

  • 有一个简单的模型可以将实体 Map 到表。它可能仅适用于相当简单的情况。如果您不喜欢这样做,则应编写自己的策略。 Spring Data JDBC 仅提供非常有限的支持,以使用 Comments 自定义策略。

4.2. 域驱动的设计和关系数据库。

所有 Spring Data 模块均受 Domain Driven Design 中“存储库”,“聚合”和“聚合根”概念的启发。这些对于 Spring Data JDBC 可能甚至更为重要,因为它们在某种程度上与使用关系数据库时的常规做法背道而驰。

集合是一组实体,可以保证在对其进行原子更改之间保持一致。一个经典的例子是OrderOrderItemsOrder上的属性(例如numberOfItemsOrderItems的实际数量一致)在进行更改时保持一致。

跨集合的引用不能保证始终保持一致。他们保证最终会变得一致。

每个集合都只有一个集合根,这是集合的实体之一。聚合只能通过该聚合根上的方法进行操作。这些是前面提到的原子变化。

存储库是对持久性存储的抽象,它看起来像某种特定类型的所有聚合的集合。通常,对于 Spring Data,这意味着您希望每个聚合根有一个Repository。另外,对于 Spring Data JDBC,这意味着从聚合根可访问的所有实体均被视为该聚合根的一部分。 Spring Data JDBC 假定只有聚合对存储聚合的非根实体的表具有外键,并且没有其他实体指向非根实体。

Warning

在当前实现中,Spring Data JDBC 删除并重新创建了从聚合根引用的实体。

您可以使用与您的数据库工作和设计风格相匹配的实现来覆盖存储库方法。

4.3. 基于 Comments 的配置

可以通过 Java 配置中的 Comments 来激活 Spring Data JDBC 存储库支持,如以下示例所示:

例子 51.使用 Java 配置的 Spring Data JDBC 存储库

@Configuration
@EnableJdbcRepositories
class ApplicationConfig {

  @Bean
  public DataSource dataSource() {

    EmbeddedDatabaseBuilder builder = new EmbeddedDatabaseBuilder();
    return builder.setType(EmbeddedDatabaseType.HSQL).build();
  }

}

上一示例中的配置类使用spring-jdbcEmbeddedDatabaseBuilder API 设置了嵌入式 HSQL 数据库。我们通过使用@EnableJdbcRepositories激活 Spring Data JDBC 存储库。如果未配置任何基本程序包,它将使用配置类所在的程序包。

4.4. 持久实体

可以使用CrudRepository.save(…)方法执行保存聚合。如果聚合是新的,则会为聚合根生成一个插入,然后是所有直接或间接引用的实体的插入语句。

如果聚合根不是新的,则将删除所有引用的实体,更新聚合根,并再次插入所有引用的实体。请注意,实例是否为新实例是实例状态的一部分。

Note

这种方法有一些明显的缺点。如果只有很少的引用实体已实际更改,则删除和插入是浪费的。尽管可以并且可能会改进此过程,但是 Spring Data JDBC 可以提供的功能存在某些限制。它不知道聚合的先前状态。因此,任何更新过程都必须获取在数据库中找到的所有内容,并确保将其转换为传递给 save 方法的实体的状态。

4.4.1. 对象 Map 基础

本节介绍了 Spring Data 对象 Map,对象创建,字段和属性访问,可变性和不可变性的基础。请注意,本部分仅适用于不使用基础数据存储(例如 JPA)的对象 Map 的 Spring Data 模块。另外,请确保参考 Store 特定的部分以获取 Store 特定的对象 Map,例如索引,自定义列或字段名称等。

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

  • 使用公开的构造函数之一创建实例。

  • 实例填充以实现所有暴露的属性。

Object creation

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

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

  • 如果只有一个构造函数接受参数,则将使用它。

  • 如果有多个构造函数采用参数,则必须由@PersistenceConstructorCommentsSpring Data 要使用的一个。

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

可以使用特定于 Store 的 SpEL 表达式使用 Spring Framework 的@Value值 Comments 来自定义值分辨率。请参阅有关 Store 特定 Map 的部分以获取更多详细信息。

对象创建内部

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

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

我们将在运行时创建一个在语义上等效于该类的工厂类:

class PersonObjectInstantiator implements ObjectInstantiator {

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

与反射相比,这使我们的性能提高了约 10%。为了使域类有资格进行此类优化,它需要遵守一组约束:

  • 它不能是私人类

  • 它不能是非静态内部类

  • 它不能是 CGLib 代理类

  • Spring Data 使用的构造函数不能为私有

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

Property population

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

  • 如果属性是不可变的,但是公开了一个凋零方法(请参见下文),我们将使用凋零来创建具有新属性值的新实体实例。

  • 如果定义了属性访问(即通过 getter 和 setter 的访问),那么我们正在调用 setter 方法。

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

房地产人口内部

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

class Person {

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

  Person() {
    this.id = null;
  }

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

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

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

例子 52.生成的属性访问器

class PersonPropertyAccessor implements PersistentPropertyAccessor {

  private static final MethodHandle firstname;              (2)

  private Person person;                                    (1)

  public void setProperty(PersistentProperty property, Object value) {

    String name = property.getName();

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

这使我们的反射性能提高了约 25%。为了使域类有资格进行此类优化,它需要遵守一组约束:

  • 类型不得位于默认值或java包下。

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

  • 内部类的类型必须为static

  • 使用的 Java 运行时必须允许在原始ClassLoader中声明类。 Java 9 和更高版本强加了某些限制。

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

让我们看一下以下实体:

例子 53.一个 samples 实体

class Person {

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

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

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

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

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

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

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

  void setRemarks(String remarks) {                                         (5)
    this.remarks = remarks;
  }
}
  • (1) 标识符属性是 final,但在构造函数中设置为null。该类公开了用于设置标识符的withId(…)方法,例如将实例插入数据存储区并已生成标识符时。创建新实例时,原始Person实例保持不变。通常将相同的模式应用于由存储 Management 的其他属性,但可能需要为持久性操作进行更改。
  • (2) firstnamelastname属性是可能通过 getter 公开的普通不可变属性。
  • (3) age属性是不可变的,但是从birthday属性派生的。按照所示的设计,由于 Spring Data 使用唯一声明的构造函数,因此数据库值将胜过默认值。即使意图是首选计算,此构造函数也必须将age作为参数(可能会忽略它),这很重要,否则属性填充步骤将尝试设置 age 字段并由于其不可变而失败,并且没有凋谢。
  • (4) 通过直接设置其comment属性是可变的。
  • (5) remarks属性是可变的,可通过直接设置comment字段或通过调用 setter 方法来填充
  • (6) 该类公开了一个工厂方法和一个用于创建对象的构造函数。这里的核心思想是使用工厂方法而不是其他构造函数,以避免通过@PersistenceConstructor消除构造函数歧义的需要。相反,属性的默认设置是在工厂方法中处理的。
General recommendations
  • 尝试坚持不可变的对象-不可变的对象很容易创建,因为实现一个对象只需调用其构造函数即可。同样,这避免了用允许 Client 端代码操纵对象状态的 setter 方法乱扔您的域对象。如果需要它们,则最好使它们受到程序包保护,以便只能由有限数量的同一位置类型调用它们。仅限构造函数的实现比属性填充快 30%。

  • 提供一个全参数的构造函数-即使您无法或不希望将实体建模为不可变值,提供构造函数仍具有价值,该构造函数将实体的所有属性作为参数,包括可变的允许对象 Map 跳过属性填充以获得最佳性能。

  • *使用工厂方法而不是重载的构造函数来避免@PersistenceConstructor * —为了获得最佳性能,需要使用全参数构造函数,我们通常希望公开更多针对应用程序用例的特定构造函数,这些构造函数会忽略诸如自动生成的标识符等内容。而是使用静态工厂方法公开 all-args 构造函数的这些变体。

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

  • 对于要生成的标识符,请结合使用 final 字段和凋灵方法

  • 使用 Lombok 避免样板代码-持久化操作通常需要构造函数接受所有参数,因此它们的声明成为对字段分配的样板参数的繁琐重复,最好使用 Lombok 的@AllArgsConstructor来避免。

Kotlin support

Spring Data 修改了 Kotlin 的细节以允许对象创建和变异。

Kotlin 对象创建

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

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

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

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

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

Kotlin 通过允许在未提供参数的情况下使用默认值来支持参数可选性。当 Spring Data 检测到带有参数默认值的构造函数时,如果数据存储区不提供值(或仅返回null),它将保留这些参数不存在,因此 Kotlin 可以应用参数默认值。考虑下面的类,该类对name应用参数默认值

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

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

Kotlin 数据类的属性人口

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

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

该类实际上是不可变的。当 Kotlin 生成copy(…)方法时,它可以创建新实例,该方法创建新对象实例,该对象实例从现有对象复制所有属性值,并将作为参数提供的属性值应用于该方法。

4.4.2. 实体中支持的类型

当前支持以下类型的属性:

  • 所有基本类型及其装箱的类型(intfloatIntegerFloat等)

  • 枚举被 Map 到其名称。

  • String

  • java.util.Datejava.time.LocalDatejava.time.LocalDateTimejava.time.LocalTime

  • 您的数据库驱动程序接受的任何内容。

  • 对其他实体的引用。他们被认为是一对一的关系。此类实体具有id属性是可选的。所引用实体的表应具有与引用实体表相同名称的附加列。您可以通过实现NamingStrategy.getReverseColumnName(RelationalPersistentProperty property)来更改此名称。

  • Set<some entity>被认为是一对多关系。所引用实体的表应具有与引用实体表相同名称的附加列。您可以通过实现NamingStrategy.getReverseColumnName(RelationalPersistentProperty property)来更改此名称。

  • Map<simple type, some entity>被认为是合格的一对多关系。所引用实体的表应具有两列:一列与外键的引用实体表相同,一列具有相同名称,而图键具有后缀_key。您可以通过分别实现NamingStrategy.getReverseColumnName(RelationalPersistentProperty property)NamingStrategy.getKeyColumn(RelationalPersistentProperty property)来更改此行为。或者,您可以用@Column(value="your_column_name", keyColumn="your_key_column_name")Comments 属性

  • List<some entity>被 Map 为Map<Integer, some entity>

引用实体的处理受到限制。这基于如上所述的聚合根的思想。如果您引用另一个实体,那么根据定义,该实体就是集合的一部分。因此,如果删除引用,则先前引用的实体将被删除。这也意味着引用是 1-1 或 1-n,但不是 n-1 或 n-m。

如果您具有 n-1 或 n-m 引用,那么根据定义,您将处理两个单独的聚合。它们之间的引用应编码为简单的id值,应与 Spring Data JDBC 正确 Map。

4.4.3. 定制转换器

通过从JdbcConfiguration继承配置并覆盖方法jdbcCustomConversions(),可以为默认不支持的类型注册自定义转换器。

@Configuration
public class DataJdbcConfiguration extends JdbcConfiguration {

    @Override
    protected JdbcCustomConversions jdbcCustomConversions() {

      return new JdbcCustomConversions(Collections.singletonList(TimestampTzToDateConverter.INSTANCE));

    }

    @ReadingConverter
    enum TimestampTzToDateConverter implements Converter<TIMESTAMPTZ, Date> {

        INSTANCE;

        @Override
        public Date convert(TIMESTAMPTZ source) {
            //...
        }
    }
}

JdbcCustomConversions的构造函数接受org.springframework.core.convert.converter.Converter的列表。

转换器应带有@ReadingConverter@WritingConverterComments,以控制它们的适用性,使其仅用于读取或写入数据库。

该示例中的TIMESTAMPTZ是特定于数据库的数据类型,需要转换为更适合域模型的数据类型。

4.4.4. NamingStrategy

当您使用 Spring Data JDBC 提供的CrudRepository的标准实现时,他们期望使用某种表结构。您可以通过在应用程序上下文中提供NamingStrategy来进行调整。

4.4.5. 自定义表格名称

当 NamingStrategy 与数据库表名称不匹配时,可以使用@Table注解自定义名称。此注解的元素value提供了自定义表名称。下面的示例将MyEntity类 Map 到数据库中的CUSTOM_TABLE_NAME表:

@Table("CUSTOM_TABLE_NAME")
public class MyEntity {
    @Id
    Integer id;

    String name;
}

4.4.6. 自定义列名称

当 NamingStrategy 与数据库列名称不匹配时,可以使用@Column注解自定义名称。此注解的元素value提供了自定义列名称。下面的示例将MyEntity类的name属性 Map 到数据库中的CUSTOM_COLUMN_NAME列:

public class MyEntity {
    @Id
    Integer id;

    @Column("CUSTOM_COLUMN_NAME")
    String name;
}

@ColumnComments 还可以用于引用类型(一对一关系)或集合,列表和 Map(一对多关系)上。在所有这些类型上,Comments 的value元素用于提供引用另一个表中的 id 列的外键列的自定义名称。在以下示例中,由于关系原因,MySubEntity类的对应表具有名称列和MyEntity id 的 id 列。 MySubEntity类的 id 列的名称也可以使用@Column注解的value元素进行自定义:

public class MyEntity {
    @Id
    Integer id;

    @Column("CUSTOM_COLUMN_NAME")
    Set<MySubEntity> name;
}

public class MySubEntity {
    String name;
}

当使用ListMap时,必须在List中的数据集位置或在Map中的实体的键值上有附加的列。可以使用@Column注解的keyColumn元素自定义此附加列名:

public class MyEntity {
    @Id
    Integer id;

    @Column(value = "CUSTOM_COLUMN_NAME", keyColumn = "CUSTOM_KEY_COLUMN_NAME")
    List<MySubEntity> name;
}

public class MySubEntity {
    String name;
}

4.4.7. 实体状态检测策略

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

*表 2.用于在 Spring Data JDBC 中检测实体是否为新实体的选项

身份检查(默认)默认情况下,Spring Data JDBC 检查给定实体的标识符属性。如果标识符属性为null,则假定该实体为新实体。否则,假定它不是新的。
实施Persistable如果一个实体实现Persistable,Spring Data JDBC 会将新的检测委托给该实体的isNew(…)方法。有关详情,请参见Javadoc
实施EntityInformation您可以通过创建JdbcRepositoryFactory的子类并覆盖getEntityInformation(…)方法来自定义SimpleJdbcRepository实现中使用的EntityInformation抽象。然后,您必须将JdbcRepositoryFactory的自定义实现注册为 Spring bean。请注意,这几乎没有必要。有关详情,请参见Javadoc

4.4.8. ID 生成

Spring Data JDBC 使用该 ID 来标识实体。实体的 ID 必须使用 Spring Data 的@IdComments 进行 Comments。

当数据库的 ID 列具有自动递增列时,将生成的值插入到数据库中后即可在实体中设置该值。

一个重要的约束条件是,在保存实体之后,该实体就不能再是新的。请注意,实体是否为新实体是该实体状态的一部分。对于自动增量列,这是自动发生的,因为 Spring 会使用 ID 列中的值来设置 ID。如果不使用自动增量列,则可以使用BeforeSave侦听器,该侦听器设置实体的 ID(在本文档的后面介绍)。

4.5. 查询方法

本节提供有关 Spring Data JDBC 的实现和使用的一些特定信息。

4.5.1. 查询查询策略

JDBC 模块仅支持将查询手动定义为@QueryComments 中的字符串。当前不支持从方法名称派生查询。

4.5.2. 使用@Query

下面的示例演示如何使用@Query声明查询方法:

例子 54.使用@Query 声明一个查询方法

public interface UserRepository extends CrudRepository<User, Long> {

  @Query("select firstName, lastName from User u where u.emailAddress = :email")
  User findByEmailAddress(@Param("email") String email);
}

Note

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

Note

Spring Data JDBC 仅支持命名参数。

Custom RowMapper

您可以通过使用@Query(rowMapperClass = ….)或注册RowMapperMap bean 并为每个方法返回类型注册RowMapper来配置要使用的RowMapper。以下示例显示了如何注册RowMappers

@Bean
RowMapperMap rowMappers() {
	return new ConfigurableRowMapperMap() //
		.register(Person.class, new PersonRowMapper()) //
		.register(Address.class, new AddressRowMapper());
}

确定方法使用哪个RowMapper时,将根据方法的返回类型执行以下步骤:

  • 如果类型是简单类型,则不使用RowMapper

取而代之的是,查询应返回具有单列的单行,并将对该返回类型的转换应用于该值。

  • 迭代RowMapperMap中的实体类,直到找到一个属于所讨论的返回类型的超类或接口。使用为该类注册的RowMapper

迭代按注册 Sequences 进行,因此请确保在特定类型之后注册更多通用类型。

如果适用,将解开包装类型,例如集合或Optional。因此,返回类型Optional<Person>在前面的过程中使用Person类型。

Modifying Query

您可以使用@Modifying on 查询方法将查询标记为修改查询,如以下示例所示:

@Modifying
@Query("UPDATE DUMMYENTITY SET name = :name WHERE id = :id")
boolean updateName(@Param("id") Long id, @Param("name") String name);

您可以指定以下返回类型:

  • void

  • int(更新的记录数)

  • boolean(记录是否已更新)

4.6. MyBatis 整合

对于CrudRepository中的每个操作,Spring Data JDBC 运行多个语句。如果应用程序上下文中有一个SqlSessionFactory,Spring Data 将针对每个步骤检查SessionFactory是否提供了一条语句。如果找到一个,则使用该语句(包括其配置的到实体的 Map)。

通过将实体类型的标准名称与Mapper.String串联起来确定语句的类型,来构造语句的名称。例如,如果要插入org.example.User的实例,Spring Data JDBC 将查找名为org.example.UserMapper.insert的语句。

语句运行时,[+845+]的实例作为参数传递,这使该语句可以使用各种参数。

下表描述了可用的 MyBatis 语句:

NamePurpose可能触发此语句的 CrudRepository 方法MyBatisContext中可用的属性
insert插入单个实体。这也适用于由聚合根引用的实体。save , saveAll .getInstance:要保存的实例


getDomainType:要保存的实体的类型。
get(<key>):引用实体的 ID,其中<key>NamingStrategy提供的后向引用列的名称。
| update |更新单个实体。这也适用于由聚合根引用的实体。 savesaveAll。| getInstance:要保存的实例
getDomainType:要保存的实体的类型。
| delete |删除单个实体。 deletedeleteById。| getId:要删除的实例的 ID
getDomainType:要删除的实体的类型。
| deleteAll-<propertyPath> |删除该类型的任何聚合根所引用的所有实体,该类型的根用作给定属性路径的前缀。请注意,为语句名称加上前缀的类型是聚合根的名称,而不是要删除的实体之一。 deleteAll。| getDomainType:要删除的实体的类型。
| deleteAll |删除用作前缀类型的所有聚合根| deleteAll。| getDomainType:要删除的实体的类型。
| delete-<propertyPath> |删除具有给定 propertyPath 的聚集根引用的所有实体| deleteById。| getId:要删除引用实体的聚合根的 ID。
getDomainType:要删除的实体的类型。
| findById |通过 ID 选择聚合根| findById。| getId:要加载的实体的 ID。
getDomainType:要加载的实体的类型。
| findAll |选择所有聚合根| findAll。| getDomainType:要加载的实体的类型。
| findAllById |通过 ID 值选择一组聚合根| findAllById。| getId:要加载的实体的 ID 值列表。
getDomainType:要加载的实体的类型。
| findAllByProperty-<propertyName> |选择另一个实体引用的一组实体。引用实体的类型用于前缀。 |所有find*方法。|被引用的实体类型用作后缀。 getId:引用要加载的实体的实体的 ID。
getDomainType:要加载的实体的类型。
| count |计算用作前缀的类型的聚合根的数量| count | getDomainType:要计数的聚合根的类型。

4.7. Events

Spring Data JDBC 触发事件,这些事件将发布到应用程序上下文中任何匹配的ApplicationListener。例如,在保存聚合之前,将调用以下侦听器:

@Bean
public ApplicationListener<BeforeSave> timeStampingSaveTime() {

	return event -> {

		Object entity = event.getEntity();
		if (entity instanceof Category) {
			Category category = (Category) entity;
			category.timeStamp();
		}
	};
}

下表描述了可用事件:

表 3.可用事件

Event何时出版
BeforeDeleteEvent在删除聚合根之前。
AfterDeleteEvent删除聚合根之后。
BeforeSaveEvent在保存聚合根之前(即,插入或更新根之前,但在决定是否更新或删除聚合根之前)。该事件引用了AggregateChange实例。可以通过添加或删除DbAction个实例来修改该实例。
AfterSaveEvent保存聚合根后(即插入或更新)。
AfterLoadEvent从数据库ResultSet创建聚合根后,其所有属性都将设置。

4.8. Logging

Spring Data JDBC 本身很少执行日志记录甚至不执行日志记录。相反,JdbcTemplate发出 SQL 语句的机制提供了日志记录。因此,如果要检查执行了哪些 SQL 语句,请激活 Spring 的NamedParameterJdbcTemplateMyBatis的日志记录。

4.9. Transactionality

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

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

public interface UserRepository extends CrudRepository<User, Long> {

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

  // Further query method declarations
}

前面的代码导致findAll()方法以 10 秒的超时时间执行,并且没有readOnly标志。

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

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

@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 的外墙工作配置。请注意,以上示例假定您使用组件扫描。

4.9.1. 事务查询方法

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

例子 57.在查询方法上使用@Transactional

@Transactional(readOnly = true)
public interface UserRepository extends CrudRepository<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 驱动程序,以进行性能优化。

4.10. Auditing

4.10.1. Basics

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

基于 Comments 的审核元数据

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

例子 58.被审计实体

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对象的接口的实现:

例子 59.基于 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,您还可以从任何地方查找它。

4.11. JDBC 审核

为了激活审核,请将@EnableJdbcAuditing添加到您的配置中,如以下示例所示:

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

@Configuration
@EnableJdbcAuditing
class Config {

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

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

Appendix

附录 A:常见问题

抱歉。到目前为止,我们没有常见问题。

附录 B:词汇表

附录 C:命名空间参考

<repositories />元素

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

表 4.属性

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

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

<populator />元素

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

表 5.属性

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

附录 E:Repositories 查询关键字

支持的查询关键字

Spring Data JDBC 目前还不支持查询派生。

附录 F:Repositories 查询返回类型

支持的查询返回类型

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

Note

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

表 6.查询返回类型

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 或 Vavr Option类型。语义上与前面描述的 Java 8 的Optional相同。
Stream<T>Java 8 Stream
Streamable<T>Iterable的便捷扩展,直接将方法公开以流式处理,Map 和过滤结果,将其串联等。
实现Streamable并接受Streamable构造函数或工厂方法参数的类型公开构造函数或以Streamable作为参数的….of(…)/….valueOf(…)工厂方法的类型。有关详情,请参见返回自定义流式包装器类型
SeqListMapSetVavr 集合类型。有关详情,请参见支持 Vavr 收藏
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的查询也可以发出无限数量的元素。