© 2018-2022 原作者。

本文档的副本可以供您自己使用和分发给其他人,前提是您不对此类副本收取任何费用,并且进一步前提是每份副本都包含本版权声明,无论是印刷版还是电子版。

前言

Spring Data JDBC 项目将核心 Spring 概念应用于开发使用符合域驱动设计原则的JDBC 数据库的解决方案。我们提供“模板”作为存储和查询聚合的高级抽象。

本文档是 Spring Data JDBC Support 的参考指南。它解释了概念、语义和语法。

本节提供一些基本介绍。文档的其余部分仅涉及 Spring Data JDBC 特性,并假定用户熟悉 SQL 和 Spring 概念。

1.学习Spring

Spring Data 使用 Spring 框架的核心功能,包括:

虽然您不需要了解 Spring API,但了解它们背​​后的概念很重要。至少,您应该熟悉控制反转 (IoC) 背后的理念,并且您应该熟悉您选择使用的任何 IoC 容器。

JDBC Aggregate 支持的核心功能可以直接使用,无需调用 Spring Container 的 IoC 服务。这很像JdbcTemplate,可以在没有任何其他 Spring 容器服务的情况下“独立”使用。要利用 Spring Data JDBC 的所有特性,例如存储库支持,您需要配置库的某些部分以使用 Spring。

要了解有关 Spring 的更多信息,您可以参考详细解释 Spring 框架的综合文档。有很多关于这个主题的文章、博客条目和书籍。有关更多信息,请参阅 Spring 框架主页

2. 要求

Spring Data JDBC 二进制文件需要 JDK 8.0 及更高级别和Spring Framework 5.3.21 及更高版本。

在数据库方面,Spring Data JDBC 需要一种方言来抽象通用 SQL 功能而不是供应商特定的风格。Spring Data JDBC 包括对以下数据库的直接支持:

  • DB2

  • H2

  • 数据库

  • 玛丽亚数据库

  • 微软 SQL 服务器

  • MySQL

  • 甲骨文

  • Postgres

如果您使用不同的数据库,那么您的应用程序将无法启动。方言部分包含有关在这种情况下如何进行的更多详细信息。

3. 其他帮助资源

学习一个新框架并不总是那么简单。在本节中,我们将尝试提供我们认为易于理解的 Spring Data JDBC 模块入门指南。但是,如果您遇到问题或需要建议,请随时使用以下链接之一:

社区论坛

Stack Overflow上的Spring Data是所有 Spring Data(不仅仅是 Document)用户共享信息和互相帮助的标签。请注意,仅发布时需要注册。

专业支持

Spring Data 和 Spring 背后的公司Pivotal Software, Inc.提供专业的源代码支持和有保证的响应时间。

4. 跟随发展

有关 Spring Data JDBC 源代码存储库、夜间构建和快照工件的信息,请参阅 Spring Data JDBC主页您可以通过Stack Overflow上的社区与开发人员互动,帮助 Spring Data 更好地满足 Spring 社区的需求。如果您遇到错误或想提出改进建议,请在Spring Data 问题跟踪器上创建工单。要及时了解 Spring 生态系统中的最新消息和公告,请订阅 Spring 社区门户。您还可以在 Twitter ( SpringData )上关注 Spring博客或项目团队。

5. 项目元数据

6.新的和值得注意的

本节介绍每个版本的重大更改。

6.1。Spring Data JDBC 2.3 的新特性

  • 支持流式传输结果

  • 支持将投影类型指定为返回类型或使用泛型并为查询方法提供 Class 参数。

6.2. Spring Data JDBC 2.2 的新特性

6.3. Spring Data JDBC 2.1 的新特性

  • Oracle 数据库的方言。

  • 支持@Value持久化构造函数。

6.4. Spring Data JDBC 2.0 的新特性

  • 乐观锁定支持。

  • 支持PagingAndSortingRepository.

  • 查询派生

  • 完全支持 H2。

  • 默认情况下,所有 SQL 标识符都知道被引用。

  • 缺少列不再导致异常。

6.5。Spring Data JDBC 1.1 的新特性

  • @Embedded实体支持。

  • 存储byte[]BINARY.

  • 中的专用insert方法JdbcAggregateTemplate

  • 只读属性支持。

6.6. Spring Data JDBC 1.0 的新特性

  • CrudRepository.

  • @Query支持。

  • MyBatis 支持。

  • 身份生成。

  • 活动支持。

  • 审计。

  • CustomConversions.

7. 依赖

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

示例 1. 使用 Spring Data 发布火车 BOM
<dependencyManagement>
  <dependencies>
    <dependency>
      <groupId>org.springframework.data</groupId>
      <artifactId>spring-data-bom</artifactId>
      <version>2021.2.1</version>
      <scope>import</scope>
      <type>pom</type>
    </dependency>
  </dependencies>
</dependencyManagement>

当前的发布火车版本是2021.2.1. 火车版使用calver和花纹YYYY.MINOR.MICRO${calver}GA 版本和服务版本的版本名称如下,所有其他版本的版本名称如下: ${calver}-${modifier},其中modifier可以是以下之一:

  • SNAPSHOT: 当前快照

  • M1, M2, 等等:里程碑

  • RC1, RC2, 等等:发布候选

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

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

7.1。使用 Spring Boot 进行依赖管理

Spring Boot 会为您选择最新版本的 Spring Data 模块。如果您仍想升级到较新版本,请将spring-data-releasetrain.version属性设置为您想要使用的训练版本和迭代。

7.2. Spring框架

当前版本的 Spring Data 模块需要 Spring Framework 5.3.21 或更高版本。这些模块也可能与该次要版本的旧错误修复版本一起使用。但是,强烈建议使用该代中的最新版本。

8. 使用 Spring 数据存储库

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

Spring Data 存储库文档和您的模块

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

8.1。核心概念

Spring Data 存储库抽象中的中央接口是Repository. 它需要域类来管理以及域类的 ID 类型作为类型参数。此接口主要用作标记接口,以捕获要使用的类型并帮助您发现扩展此接口的接口。该CrudRepository接口为被管理的实体类提供了复杂的 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 的实体是否存在。
我们还提供了特定于持久性技术的抽象,例如JpaRepositoryMongoRepositoryCrudRepository除了相当通用的与持久性技术无关的接口(例如CrudRepository.

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

示例 4.PagingAndSortingRepository界面
public interface PagingAndSortingRepository<T, ID> extends CrudRepository<T, ID> {

  Iterable<T> findAll(Sort sort);

  Page<T> findAll(Pageable pageable);
}

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

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

8.2. 查询方法

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

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

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

    interface PersonRepository extends Repository<Person, Long> {
      List<Person> findByLastname(String lastname);
    }
  3. 设置 Spring 以使用JavaConfigXML configuration为这些接口创建代理实例。

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

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

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

      此示例中使用了 JPA 命名空间。如果您将存储库抽象用于任何其他存储,则需要将其更改为存储模块的适当命名空间声明。换句话说,您应该交换jpa,例如,mongodb

      另外请注意,JavaConfig 变体没有显式配置包,因为默认使用带注释的类的包。要自定义要扫描的包,请使用特定于数据存储的存储库的-annotation的basePackage…属性之一。@Enable${store}Repositories

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

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

以下部分详细解释了每个步骤:

8.3. 定义存储库接口

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

8.3.1. 微调存储库定义

通常,您的存储库接口会扩展RepositoryCrudRepositoryPagingAndSortingRepository. 或者,如果您不想扩展 Spring Data 接口,也可以使用@RepositoryDefinition. 扩展CrudRepository公开了一整套操作实体的方法。如果您希望对公开的方法有选择性,请将要公开的方法复制CrudRepository到您的域存储库中。

这样做可以让您在提供的 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通过电子邮件地址查找。

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

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

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

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

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

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

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

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

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

MyRepository并在它们的类型层次结构中UserRepository扩展。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> { … }

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

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

示例 10. 使用带注释的域类的存储库定义
interface PersonRepository extends Repository<Person, Long> { … }

@Entity
class Person { … }

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

@Document
class User { … }

PersonRepositoryreferences Person,使用 JPA@Entity注释进行注释,因此这个存储库显然属于 Spring Data JPA。UserRepositoryreferences User,使用 Spring Data MongoDB 的注解进行@Document注解。

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

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

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

@Entity
@Document
class Person { … }

此示例显示了使用 JPA 和 Spring Data MongoDB 注释的域类。它定义了两个存储库,JpaPersonRepository并且MongoDBPersonRepository. 一个用于 JPA,另一个用于 MongoDB。Spring Data 不再能够区分存储库,这会导致未定义的行为。

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

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

以下示例显示了基本包的注释驱动配置:

示例 12. 基本包的注释驱动配置
@EnableJpaRepositories(basePackages = "com.acme.repositories.jpa")
@EnableMongoRepositories(basePackages = "com.acme.repositories.mongo")
class Configuration { … }

8.4. 定义查询方法

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

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

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

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

8.4.1. 查询查找策略

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

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

  • USE_DECLARED_QUERY尝试查找已声明的查询,如果找不到则抛出异常。查询可以由某处的注释定义或通过其他方式声明。请参阅特定商店的文档以查找该商店​​的可用选项。如果存储库基础结构在引导时没有找到该方法的声明查询,它就会失败。

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

8.4.2. 查询创建

Spring Data 存储库基础架构中内置的查询构建器机制对于在存储库的实体上构建约束查询很有用。

以下示例显示了如何创建多个查询:

示例 13. 从方法名称创建查询
interface PersonRepository extends Repository<Person, Long> {

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

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

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

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

解析查询方法名称分为主语和谓语。第一部分 ( find…By, exists…By) 定义查询的主题,第二部分构成谓词。引言从句(主语)可以包含进一步的表达。find(或其他引入关键字)和之间的任何文本By都被认为是描述性的,除非使用结果限制关键字之一,例如在Distinct要创建的查询上设置不同的标志或Top/First来限制查询结果

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

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

  • 表达式通常是结合了可以连接的运算符的属性遍历。您可以将属性表达式与AND和结合使用OR。您还可以获得对运算符(如BetweenLessThan、和 )的支持GreaterThan,以及Like对属性表达式的支持。支持的运算符可能因数据存储而异,因此请参阅参考文档的相应部分。

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

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

8.4.3. 属性表达式

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

List<Person> findByAddressZipCode(ZipCode zipCode);

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

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

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

List<Person> findByAddress_ZipCode(ZipCode zipCode);

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

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

第一种方法允许您将org.springframework.data.domain.Pageable实例传递给查询方法,以动态地将分页添加到静态定义的查询中。APage知道可用元素和页面的总数。它通过基础设施触发计数查询来计算总数来实现这一点。由于这可能很昂贵(取决于使用的商店),您可以改为返回Slice. ASlice只知道下一个Slice是否可用,这在遍历更大的结果集时可能就足够了。

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

要了解整个查询获得了多少页,您必须触发额外的计数查询。默认情况下,此查询派生自您实际触发的查询。
分页和排序

您可以使用属性名称定义简单的排序表达式。您可以连接表达式以将多个条件收集到一个表达式中。

示例 15. 定义排序表达式
Sort sort = Sort.by("firstname").ascending()
  .and(Sort.by("lastname").descending());

要以更安全的方式定义排序表达式,请从要为其定义排序表达式的类型开始,并使用方法引用来定义要排序的属性。

示例 16. 使用类型安全 API 定义排序表达式
TypedSort<Person> person = Sort.sort(Person.class);

Sort sort = person.by(Person::getFirstname).ascending()
  .and(person.by(Person::getLastname).descending());
TypedSort.by(…)通过(通常)使用 CGlib 来使用运行时代理,这可能会在使用 Graal VM Native 等工具时干扰本机映像编译。

如果您的商店实现支持 Querydsl,您还可以使用生成的元模型类型来定义排序表达式:

示例 17. 使用 Querydsl API 定义排序表达式
QSort sort = QSort.by(QPerson.firstname.asc())
  .and(QSort.by(QPerson.lastname.desc()));

8.4.5。限制查询结果

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

Top示例 18. 使用and限制查询的结果大小First
User findFirstByOrderByLastnameAsc();

User findTopByOrderByAgeDesc();

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

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

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

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

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

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

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

8.4.6。返回集合或迭代的存储库方法

返回多个结果的查询方法可以使用标准的 Java IterableListSet. 除此之外,我们还支持返回 Spring Data 的Streamable自定义扩展Iterable,以及Vavr提供的集合类型。请参阅解释所有可能的查询方法返回类型的附录。

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

您可以Streamable用作任何集合类型的替代品Iterable或任何集合类型。它提供了方便的方法来访问非并行Stream(缺少Iterable)以及直接….filter(…)….map(…)覆盖元素并将其连接Streamable到其他元素的能力:

示例 19. 使用 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"));
返回自定义 Streamable Wrapper 类型

为集合提供专用的包装器类型是为返回多个元素的查询结果提供 API 的常用模式。通常,通过调用返回类似集合类型的存储库方法并手动创建包装器类型的实例来使用这些类型。您可以避免该额外步骤,因为 Spring Data 允许您使用这些包装器类型作为查询方法返回类型,如果它们满足以下条件:

  1. 类型实现Streamable.

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

以下清单显示了一个示例:

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

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

  private final Streamable<Product> streamable;

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


  @Override
  public Iterator<Product> iterator() {                 (4)
    return streamable.iterator();
  }
}

interface ProductRepository implements Repository<Product, Long> {
  Products findAllByDescriptionContaining(String text); (5)
}
1 公开 API 以访问产品价格的Product实体。
2 Streamable<Product>可以通过使用Products.of(…)(使用 Lombok 注释创建的工厂方法)构造的包装器类型。采用意志的标准构造函数Streamable<Product>也可以。
3 包装器类型公开了一个额外的 API,用于计算Streamable<Product>.
4 实现Streamable接口并委托给实际结果。
5 该包装器类型Products可以直接用作查询方法返回类型。您不需要Streamable<Product>在存储库客户端中的查询之后返回并手动包装它。
支持 Vavr 集合

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

Vavr 集合类型 使用的 Vavr 实现类型 有效的 Java 源类型

io.vavr.collection.Seq

io.vavr.collection.List

java.util.Iterable

io.vavr.collection.Set

io.vavr.collection.LinkedHashSet

java.util.Iterable

io.vavr.collection.Map

io.vavr.collection.LinkedHashMap

java.util.Map

您可以使用第一列中的类型(或其子类型)作为查询方法返回类型,并获取第二列中的类型作为实现类型,具体取决于实际查询结果的 Java 类型(第三列)。或者,您可以声明Traversable(VavrIterable等效项),然后我们从实际返回值派生实现类。也就是说,ajava.util.List变成 VavrListSeq,ajava.util.Set变成 Vavr LinkedHashSet Set,依此类推。

8.4.7。存储库方法的空处理

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

  • com.google.common.base.Optional

  • scala.Option

  • io.vavr.control.Option

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

可空性注释

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

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

  • @NonNull: 用于不能使用的参数或返回值null(在适用的情况下不需要用于参数和返回值@NonNullApi)。

  • @Nullable: 用在参数或返回值上即可null

Spring 注释使用JSR 305注释(一种休眠但广泛使用的 JSR)进行元注释。JSR 305 元注释让工具供应商(例如IDEAEclipseKotlin)以通用方式提供空安全支持,而无需对 Spring 注释进行硬编码支持。要为查询方法启用可空性约束的运行时检查,您需要使用 Spring 的@NonNullApiin在包级别激活不可空性package-info.java,如下例所示:

示例 20. 在package-info.java
@org.springframework.lang.NonNullApi
package com.acme;

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

以下示例显示了刚刚描述的一些技术:

示例 21. 使用不同的可空性约束
package com.acme;                                                       (1)

import org.springframework.lang.Nullable;

interface UserRepository extends Repository<User, Long> {

  User getByEmailAddress(EmailAddress emailAddress);                    (2)

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

  Optional<User> findOptionalByEmailAddress(EmailAddress emailAddress); (4)
}
1 存储库位于我们为其定义了非空行为的包(或子包)中。
2 EmptyResultDataAccessException当查询不产生结果时抛出一个。IllegalArgumentExceptionemailAddress交给方法时抛出一个null
3 null当查询没有产生结果时返回。也接受null作为 的值emailAddress
4 Optional.empty()当查询没有产生结果时返回。IllegalArgumentExceptionemailAddress交给方法时抛出一个null
基于 Kotlin 的存储库中的可空性

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

示例 22. 在 Kotlin 存储库上使用可空性约束
interface UserRepository : Repository<User, String> {

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

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

8.4.8。流式查询结果

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

示例 23. 使用 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);
AStream可能包装底层数据存储特定的资源,因此必须在使用后关闭。Stream您可以使用该close()方法或使用 Java 7块手动关闭try-with-resources,如以下示例所示:
示例 24. 处理块Stream<T>中的结果try-with-resources
try (Stream<User> stream = repository.findAllByCustomQueryAndStream()) {
  stream.forEach(…);
}
并非所有 Spring Data 模块当前都支持Stream<T>作为返回类型。

8.4.9。异步查询结果

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

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

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

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

8.5。创建存储库实例

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

8.5.1。XML 配置

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

示例 25. 通过 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
    https://www.springframework.org/schema/beans/spring-beans.xsd
    http://www.springframework.org/schema/data/jpa
    https://www.springframework.org/schema/data/jpa/spring-jpa.xsd">

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

</beans:beans>

在前面的示例中,指示 Spring 扫描com.acme.repositories其所有子包以查找扩展接口Repository或其子接口之一。对于找到的每个接口,基础设施都会注册特定的持久性技术,FactoryBean以创建适当的代理来处理查询方法的调用。每个 bean 都在从接口名称派生的 bean 名称下注册,因此接口UserRepository将在userRepository. 嵌套存储库接口的 Bean 名称以它们的封闭类型名称为前缀。该base-package属性允许使用通配符,以便您可以定义扫描包的模式。

使用过滤器

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

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

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

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

8.5.2. Java 配置

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

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

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

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

8.5.3. 独立使用

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

示例 28. 存储库工厂的独立使用
RepositoryFactorySupport factory = … // Instantiate factory here
UserRepository repository = factory.getRepository(UserRepository.class);

8.6. Spring Data Repository 的自定义实现

Spring Data 提供了各种选项来创建几乎不需要编码的查询方法。但是当这些选项不能满足您的需求时,您还可以为存储库方法提供您自己的自定义实现。本节介绍如何执行此操作。

8.6.1. 自定义单个存储库

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

示例 29. 自定义存储库功能的接口
interface CustomizedUserRepository {
  void someCustomMethod(User user);
}
示例 30. 自定义存储库功能的实现
class CustomizedUserRepositoryImpl implements CustomizedUserRepository {

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

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

然后可以让你的repository接口扩展fragment接口,如下:

示例 31. 对存储库界面的更改
interface UserRepository extends CrudRepository<User, Long>, CustomizedUserRepository {

  // Declare query methods here
}

使用存储库接口扩展片段接口结合了 CRUD 和自定义功能,并使其可供客户端使用。

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

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

示例 32. 片段及其实现
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

示例 33. 对存储库界面的更改
interface UserRepository extends CrudRepository<User, Long>, HumanRepository, ContactRepository {

  // Declare query methods here
}

存储库可能由多个按其声明顺序导入的自定义实现组成。自定义实现具有比基本实现和存储库方面更高的优先级。此排序允许您覆盖基本存储库和方面方法,并在两个片段贡献相同的方法签名时解决歧义。存储库片段不限于在单个存储库界面中使用。多个存储库可以使用一个片段接口,让您可以在不同的存储库中重用自定义。

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

示例 34. 片段覆盖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
  }
}

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

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

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

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

示例 36. 配置示例
<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与片段 interface( CustomizedUserRepository) 加上后缀的名称相匹配Impl

示例 37. 歧义实现的解决
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
}

如果您使用 注释UserRepository接口@Component("specialCustom"),则 bean 名称加上Impl然后匹配为存储库实现定义的名称com.acme.impl.two,并且使用它而不是第一个。

手动接线

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

示例 38. 自定义实现的手动连接
<repositories base-package="com.acme.repository" />

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

8.6.2. 自定义基础存储库

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

示例 39. 自定义存储库基类
class MyRepositoryImpl<T, ID>
  extends SimpleJpaRepository<T, ID> {

  private final EntityManager entityManager;

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

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

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

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

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

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

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

8.7. 从聚合根发布事件

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

示例 42. 从聚合根中公开域事件
class AnAggregateRoot {

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

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

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

8.8. Spring 数据扩展

本节记录了一组 Spring Data 扩展,这些扩展支持 Spring Data 在各种上下文中的使用。目前,大部分集成都是针对 Spring MVC 的。

8.8.1。Querydsl 扩展

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

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

示例 43.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请在存储库接口上进行扩展,如以下示例所示:

示例 44. 存储库上的 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);

8.8.2. 网络支持

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

示例 45. 启用 Spring Data web 支持
@Configuration
@EnableWebMvc
@EnableSpringDataWebSupport
class WebConfiguration {}

@EnableSpringDataWebSupport注释注册了一些组件。我们将在本节后面讨论这些内容。它还检测类路径上的 Spring HATEOAS 并为其注册集成组件(如果存在)。

或者,如果您使用 XML 配置,则注册为 Spring beanSpringDataWebConfiguration或注册HateoasAwareSpringDataWebConfiguration为 Spring beans,如以下示例所示 (for SpringDataWebConfiguration):

示例 46. 在 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" />
基本网络支持

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

使用DomainClassConverter

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

示例 47. 在方法签名中使用域类型的 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 MVCid先将path变量转换为域类的类型,最终通过调用findById(…)为域类型注册的repository实例来访问实例。

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

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

示例 48. 使用 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使用以下默认配置从请求参数中派生一个实例:

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

page

您要检索的页面。0 索引,默认为 0。

size

您要检索的页面的大小。默认为 20。

sort

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

要自定义此行为,请分别注册一个实现PageableHandlerMethodArgumentResolverCustomizer接口或SortHandlerMethodArgumentResolverCustomizer接口的 bean。它的customize()方法被调用,让您更改设置,如以下示例所示:

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

如果设置现有的属性MethodArgumentResolver不足以满足您的目的,请扩展其中一个SpringDataWebConfiguration或启用 HATEOAS 的等效项,覆盖pageableResolver()orsortResolver()方法,并导入您的自定义配置文件而不是使用@Enable注释。

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

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

您必须填充thing1_pagething2_page等。

Pageable传入方法的默认值相当于 a PageRequest.of(0, 20),但您可以使用参数@PageableDefault上的注解来自定义它Pageable

Pageables 的超媒体支持

PagedResourcesSpring HATEOAS附带了一个表示模型PagePagePagea到 a的转换PagedResources是由 Spring HATEOASResourceAssembler接口的实现完成的,称为PagedResourcesAssembler. 以下示例显示了如何使用 aPagedResourcesAssembler作为控制器方法参数:

示例 49. 使用 PagedResourcesAssembler 作为控制器方法参数
@Controller
class PersonController {

  @Autowired PersonRepository repository;

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

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

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

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

  • PagedResources对象PageMetadata附加了一个实例,并填充了来自Page和底层的信息PageRequest

  • 可能会PagedResources获取prevnext附加链接,具体取决于页面的状态。链接指向方法映射到的 URI。添加到方法中的分页参数与设置相匹配,PageableHandlerMethodArgumentResolver以确保稍后可以解析链接。

假设我们Person在数据库中有 30 个实例。您现在可以触发请求 ( ) 并查看类似于以下内容的输出: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(…)方法来自定义它。

Spring Data Jackson 模块

核心模块和一些特定于商店的模块附带一组杰克逊模块,用于 Spring Data 域使用的类型,如org.springframework.data.geo.Distance和。一旦启用Web 支持并可用,就会 导入这些模块。org.springframework.data.geo.Point
com.fasterxml.jackson.databind.ObjectMapper

在初始化期间SpringDataJacksonModules,像SpringDataJacksonConfiguration, 被基础设施拾取,以便声明com.fasterxml.jackson.databind.Module的 s 可供 Jackson 使用ObjectMapper

以下域类型的数据绑定混合由公共基础设施注册。

org.springframework.data.geo.Distance
org.springframework.data.geo.Point
org.springframework.data.geo.Box
org.springframework.data.geo.Circle
org.springframework.data.geo.Polygon

单个模块可能会提供额外的SpringDataJacksonModules.
有关详细信息,请参阅商店特定部分。

Web 数据绑定支持

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

示例 50. 使用 JSONPath 或 XPath 表达式的 HTTP 有效负载绑定
@ProjectedPayload
public interface UserPayload {

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

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

您可以将前面示例中显示的类型用作 Spring MVC 处理程序方法参数,也可以ParameterizedTypeReference使用RestTemplate. 前面的方法声明将尝试firstname在给定文档中查找任何位置。XML 查找在传入文档的lastname顶层执行。它的 JSON 变体首先尝试顶级,但如果前者不返回值lastname,也会尝试lastname嵌套在子文档中。user这样,源文档结构的变化可以很容易地缓解,而无需客户端调用公开的方法(通常是基于类的有效负载绑定的缺点)。

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

对于 Spring MVC,必要的转换器在@EnableSpringDataWebSupport活动时会自动注册,并且所需的依赖项在类路径中可用。对于使用RestTemplate,注册一个ProjectingJackson2HttpMessageConverter(JSON)或XmlBeamHttpMessageConverter手动。

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

Querydsl 网络支持

对于那些具有QueryDSLRequest集成的商店,您可以从查询字符串中包含的属性派生查询。

考虑以下查询字符串:

?firstname=Dave&lastname=Matthews

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

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

向方法签名添加 a@QuerydslPredicate提供了一个即用型Predicate,您可以使用QuerydslPredicateExecutor.

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

以下示例显示了如何@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 将查询字符串参数解析为匹配Predicatefor User

默认绑定如下:

  • Object在简单的属性上eq

  • Object像属性一样的集合contains

  • Collection在简单的属性上in

bindings您可以通过属性@QuerydslPredicate或使用 Java 8default methods并将方法添加到存储库接口来自定义这些绑定QuerydslBinderCustomizer,如下所示:

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

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

    bindings.bind(user.username).first((path, value) -> path.contains(value))    (3)
    bindings.bind(String.class)
      .first((StringPath path, String value) -> path.containsIgnoreCase(value)); (4)
    bindings.excluding(user.password);                                           (5)
  }
}
1 QuerydslPredicateExecutor提供对Predicate.
2 QuerydslBinderCustomizer存储库界面上定义的自动拾取和快捷方式@QuerydslPredicate(bindings=…​)
3 将属性的绑定定义为username简单contains绑定。
4 将属性的默认绑定定义为String不区分大小写的contains匹配。
5 从解析中排除该password属性。Predicate
您可以QuerydslBinderCustomizerDefaults在应用来自存储库或@QuerydslPredicate.

8.8.3. 存储库填充器

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

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

示例 51. JSON 中定义的数据
[ { "_class" : "com.acme.Person",
 "firstname" : "Dave",
  "lastname" : "Matthews" },
  { "_class" : "com.acme.Person",
 "firstname" : "Carter",
  "lastname" : "Beauford" } ]

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

示例 52. 声明 Jackson 存储库填充器
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xmlns:repository="http://www.springframework.org/schema/data/repository"
  xsi:schemaLocation="http://www.springframework.org/schema/beans
    https://www.springframework.org/schema/beans/spring-beans.xsd
    http://www.springframework.org/schema/data/repository
    https://www.springframework.org/schema/data/repository/spring-repository.xsd">

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

</beans>

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

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

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

示例 53. 声明一个解组存储库填充器(使用 JAXB)
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xmlns:repository="http://www.springframework.org/schema/data/repository"
  xmlns:oxm="http://www.springframework.org/schema/oxm"
  xsi:schemaLocation="http://www.springframework.org/schema/beans
    https://www.springframework.org/schema/beans/spring-beans.xsd
    http://www.springframework.org/schema/data/repository
    https://www.springframework.org/schema/data/repository/spring-repository.xsd
    http://www.springframework.org/schema/oxm
    https://www.springframework.org/schema/oxm/spring-oxm.xsd">

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

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

</beans>

参考文档

9. JDBC 存储库

本章指出了 JDBC 存储库支持的特点。这建立在使用 Spring Data Repositories中解释的核心存储库支持之上。您应该对那里解释的基本概念有充分的理解。

9.1。为什么选择 Spring Data JDBC?

Java 世界中关系型数据库的主要持久化 API 肯定是 JPA,它有自己的 Spring Data 模块。为什么还有一个?

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

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

Spring Data JDBC 旨在通过采用以下设计决策在概念上更加简单:

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

  • 如果你保存一个实体,它就会被保存。如果你不这样做,它就不会。没有脏跟踪,也没有会话。

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

9.2. 领域驱动设计和关系数据库。

所有 Spring Data 模块都受到域驱动设计中“存储库”、“聚合”和“聚合根”概念的启发。这些对于 Spring Data JDBC 可能更为重要,因为它们在某种程度上与使用关系数据库时的常规做法相反。

聚合是一组实体,它们保证在对它的原子更改之间保持一致。一个经典的例子是Orderwith OrderItems。上的属性Order(例如,numberOfItems与 的实际数量OrderItems一致)在进行更改时保持一致。

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

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

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

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

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

9.3. 入门

引导设置工作环境的一种简单方法是在STS中或从Spring Initializr创建一个基于 Spring 的项目。

首先,您需要设置一个正在运行的数据库服务器。请参阅供应商文档,了解如何配置数据库以进行 JDBC 访问。

在 STS 中创建 Spring 项目:

  1. 转到 File → New → Spring Template Project → Simple Spring Utility Project,并在出现提示时按 Yes。然后输入项目和包名,例如org.spring.jdbc.example.

  2. 将以下内容添加到pom.xml文件dependencies元素:

    <dependencies>
    
      <!-- other dependency elements omitted -->
    
      <dependency>
        <groupId>org.springframework.data</groupId>
        <artifactId>spring-data-jdbc</artifactId>
        <version>2.4.1</version>
      </dependency>
    
    </dependencies>
  3. 将 pom.xml 中的 Spring 版本更改为

    <spring.framework.version>5.3.21</spring.framework.version>
  4. 将 Maven 的 Spring Milestone 存储库的以下位置添加到您的位置,使其与您的元素pom.xml处于同一级别:<dependencies/>

    <repositories>
      <repository>
        <id>spring-milestone</id>
        <name>Spring Maven MILESTONE Repository</name>
        <url>https://repo.spring.io/libs-milestone</url>
      </repository>
    </repositories>

该存储库也可在此处浏览

9.4。示例存储库

有一个GitHub 存储库,其中包含几个示例,您可以下载并试用这些示例,以了解该库的工作原理。

9.5。基于注解的配置

Spring Data JDBC 存储库支持可以通过 Java 配置由注解激活,如以下示例所示:

示例 54. 使用 Java 配置的 Spring Data JDBC 存储库
@Configuration
@EnableJdbcRepositories                                                                (1)
class ApplicationConfig extends AbstractJdbcConfiguration {                            (2)

    @Bean
    DataSource dataSource() {                                                         (3)

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

    @Bean
    NamedParameterJdbcOperations namedParameterJdbcOperations(DataSource dataSource) { (4)
        return new NamedParameterJdbcTemplate(dataSource);
    }

    @Bean
    TransactionManager transactionManager(DataSource dataSource) {                     (5)
        return new DataSourceTransactionManager(dataSource);
    }
}
1 @EnableJdbcRepositories为派生自的接口创建实现Repository
2 AbstractJdbcConfiguration提供 Spring Data JDBC 所需的各种默认 bean
3 创建DataSource到数据库的连接。这是以下两个 bean 方法所必需的。
4 创建NamedParameterJdbcOperationsSpring Data JDBC 用于访问数据库的对象。
5 Spring Data JDBC 利用 Spring JDBC 提供的事务管理。

上例中的配置类EmbeddedDatabaseBuilder使用spring-jdbc. 然后DataSource用于设置NamedParameterJdbcOperationsTransactionManager. 我们最终通过使用@EnableJdbcRepositories. 如果没有配置基本包,则使用配置类所在的包。扩展AbstractJdbcConfiguration可确保注册各种 bean。覆盖其方法可用于自定义设置(见下文)。

使用 Spring Boot 可以进一步简化此配置。一旦启动器包含在依赖项中,使用 Spring BootDataSource就足够了。spring-boot-starter-data-jdbc其他一切都由 Spring Boot 完成。

在此设置中可能需要自定义几件事。

9.5.1。方言

Spring Data JDBC 使用接口的实现Dialect来封装特定于数据库或其 JDBC 驱动程序的行为。默认情况下,AbstractJdbcConfiguration尝试确定正在使用的数据库并注册正确的Dialect. 这种行为可以通过覆盖来改变jdbcDialect(NamedParameterJdbcOperations)

如果您使用没有方言可用的数据库,那么您的应用程序将无法启动。在这种情况下,您必须要求您的供应商提供Dialect实施。或者,您可以:

  1. 实现你自己的Dialect.

  2. 实现一个JdbcDialectProvider返回Dialect.

  3. 通过在下创建spring.factories资源来注册提供者META-INF并通过添加一行来执行注册
    org.springframework.data.jdbc.repository.config.DialectResolver$JdbcDialectProvider=<fully qualified name of your JdbcDialectProvider>

9.6。持久实体

可以使用该CrudRepository.save(…)方法保存聚合。如果聚合是新的,这将导致聚合根的插入,然后是所有直接或间接引用的实体的插入语句。

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

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

9.6.1。对象映射基础

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

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

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

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

对象创建

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

  1. 如果有一个带有注释的静态工厂方法,@PersistenceCreator则使用它。

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

  3. 如果有多个构造函数并且恰好一个用 注释@PersistenceCreator,则使用它。

  4. 如果有无参数构造函数,则使用它。其他构造函数将被忽略。

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

可以使用 Spring Framework 的@Valuevalue annotation 使用 store-specific SpEL 表达式来自定义 value 解析。请参阅有关商店特定映射的部分以获取更多详细信息。

对象创建内部

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

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

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

class PersonObjectInstantiator implements ObjectInstantiator {

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

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

  • 它不能是私人课程

  • 它不能是非静态内部类

  • 它不能是 CGLib 代理类

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

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

物业人口

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

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

  2. 如果定义了属性访问(即通过getter 和setter 访问),我们将调用setter 方法。

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

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

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

财产人口内部

与我们在对象构造中的优化类似,我们还使用 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;
  }
}
示例 55. 生成的属性访问器
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 使用字段访问来读取和写入属性值。根据字段的可见性规则privateMethodHandles用于与字段交互。
3 该类公开了一个withId(…)用于设置标识符的方法,例如,当将实例插入数据存储并生成标识符时。调用withId(…)会创建一个新Person对象。所有后续的突变都将发生在新的实例中,而前一个不变。
4 使用属性访问允许直接方法调用而不使用MethodHandles.

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

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

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

  • 作为内部类的类型必须是static.

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

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

让我们看一下以下实体:

示例 56. 示例实体
class Person {

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

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

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

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

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

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

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

  void setRemarks(String remarks) {                                         (5)
    this.remarks = remarks;
  }
}
1 标识符属性是最终的,但null在构造函数中设置为。该类公开了一个withId(…)用于设置标识符的方法,例如,当将实例插入数据存储并生成标识符时。Person创建新实例时,原始实例保持不变。相同的模式通常适用于存储管理但可能必须更改以进行持久性操作的其他属性。wither 方法是可选的,因为持久性构造函数(参见 6)实际上是一个复制构造函数,设置属性将被转换为创建一个应用了新标识符值的新实例。
2 firstnamelastname属性是可能通过 getter 暴露的普通不可变属性。
3 age属性是不可变的,但从该birthday属性派生而来。使用所示的设计,数据库值将胜过默认值,因为 Spring Data 使用唯一声明的构造函数。即使意图是应该首选计算,重要的是此构造函数也将age其作为参数(可能会忽略它),否则属性填充步骤将尝试设置年龄字段并由于它是不可变的且没有with…方法而失败在场。
4 comment属性是可变的,通过直接设置其字段来填充。
5 remarks属性是可变的,并且可以通过直接设置字段comment或调用 setter 方法来填充
6 该类公开了一个工厂方法和一个用于创建对象的构造函数。这里的核心思想是使用工厂方法而不是额外的构造函数来避免构造函数通过@PersistenceCreator. 相反,属性的默认设置是在工厂方法中处理的。如果您希望 Spring Data 使用工厂方法进行对象实例化,请使用@PersistenceCreator.
一般建议
  • 尝试坚持使用不可变对象 ——不可变对象很容易创建,因为实现对象只需调用其构造函数即可。此外,这可以避免您的域对象被允许客户端代码操纵对象状态的 setter 方法乱扔垃圾。如果您需要这些,最好将它们包保护起来,以便它们只能被有限数量的并置类型调用。仅构造函数实现比属性填充快 30%。

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

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

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

  • 对于要生成的标识符,仍将 final 字段与全参数持久性构造函数(首选)或with…方法结合使用 —— 

  • 使用 Lombok 避免样板代码 - 由于持久性操作通常需要一个构造函数来获取所有参数,因此它们的声明变成了对字段分配的样板参数的繁琐重复,使用 Lombok 可以最好地避免这种情况@AllArgsConstructor

覆盖属性

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

public class SuperType {

   private CharSequence field;

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

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

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

public class SubType extends SuperType {

   private String field;

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

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

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

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

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

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

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

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

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

Kotlin 支持

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

Kotlin 对象创建

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

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

上面的类编译成一个带有显式构造函数的典型类。我们可以通过添加另一个构造函数来自定义这个类,并用注释@PersistenceCreator来指示构造函数的偏好:

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

    @PersistenceCreator
    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(…)创建新对象实例的方法,该方法从现有对象复制所有属性值并将作为参数提供的属性值应用到该方法。

Kotlin 覆盖属性

Kotlin 允许声明属性覆盖以更改子类中的属性。

open class SuperType(open var field: Int)

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

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

public class SuperType {

   private int field;

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

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

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

public final class SubType extends SuperType {

   private int field;

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

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

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

getter 和 setterSubType仅在 set上SubType.field,而不在SuperType.field. 在这种安排中,使用构造函数是 set 的唯一默认方法SuperType.field。添加一个方法来SubType设置SuperType.fieldviathis.SuperType.field = …是可能的,但不属于支持的约定。属性覆盖在某种程度上会产生冲突,因为属性共享相同的名称但可能代表两个不同的值。我们通常建议使用不同的属性名称。

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

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

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

  3. @AccessType(PROPERTY)由于无法设置超属性,因此无法使用 using 。

9.6.2. 您的实体中支持的类型

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

  • 所有原始类型及其装箱类型(intfloatIntegerFloat等)

  • 枚举被映射到他们的名字。

  • String

  • java.util.Date, java.time.LocalDate,java.time.LocalDateTimejava.time.LocalTime

  • 如果您的数据库支持,上述类型的数组和集合可以映射到数组类型的列。

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

  • 对其他实体的引用。它们被认为是一对一的关系或嵌入式类型。一对一关系实体具有id属性是可选的。被引用实体的表应该有一个与引用实体的表同名的附加列。您可以通过实施来更改此名称NamingStrategy.getReverseColumnName(PersistentPropertyPathExtension path)。嵌入式实体不需要id. 如果存在一个,它将被忽略。

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

  • Map<simple type, some entity>被认为是合格的一对多关系。被引用实体的表应该有两个额外的列:一个与引用实体的表命名相同的外键,另一个具有相同的名称和一个附加_key后缀的映射键。NamingStrategy.getReverseColumnName(PersistentPropertyPathExtension path)您可以通过分别实现和来更改此行为NamingStrategy.getKeyColumn(RelationalPersistentProperty property)。或者,您可以使用注释属性@MappedCollection(idColumn="your_column_name", keyColumn="your_key_column_name")

  • List<some entity>映射为Map<Integer, some entity>.

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

如果您有 n-1 或 nm 引用,根据定义,您将处理两个单独的聚合。它们之间的引用可以编码为简单的id值,这些值与 Spring Data JDBC 正确映射。对这些进行编码的更好方法是使它们成为AggregateReference. AnAggregateReference是一个 id 值的包装器,它将该值标记为对不同聚合的引用。此外,该聚合的类型被编码在类型参数中。

示例 57. 声明和设置AggregateReference
class Person {
	@Id long id;
	AggregateReference<Person, Long> bestFriend;
}

// ...

Person p1, p2 = // some initialization

p1.bestFriend = AggregateReference.to(p2.id);
  • 您注册的适合的类型.

9.6.3。NamingStrategy

CrudRepository当您使用Spring Data JDBC 提供的标准实现时,它们需要特定的表结构。NamingStrategy您可以通过在应用程序上下文中提供 a 来调整它。

9.6.4。Custom table names

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

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

    String name;
}

9.6.5。Custom column names

当 NamingStrategy 与您的数据库列名称不匹配时,您可以使用@Column注释自定义名称。此注释的元素value提供自定义列名称。以下示例将类的name属性映射到数据库中的列:MyEntityCUSTOM_COLUMN_NAME

class MyEntity {
    @Id
    Integer id;

    @Column("CUSTOM_COLUMN_NAME")
    String name;
}

注释可@MappedCollection 用于引用类型(一对一关系)或 Sets、Lists 和 Maps(一对多关系)。 idColumn注释的元素为引用另一个表中的 id 列的外键列提供自定义名称。在以下示例中,MySubEntity该类的对应表有一个NAME列,并且出于关系原因,该CUSTOM_MY_ENTITY_ID_COLUMN_NAME列的id 列:MyEntity

class MyEntity {
    @Id
    Integer id;

    @MappedCollection(idColumn = "CUSTOM_MY_ENTITY_ID_COLUMN_NAME")
    Set<MySubEntity> subEntities;
}

class MySubEntity {
    String name;
}

使用时ListMap您必须为数据集在 中的位置List或 中实体的键值添加一列Map。这个额外的列名可以用注解的keyColumn元素来定制:@MappedCollection

class MyEntity {
    @Id
    Integer id;

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

class MySubEntity {
    String name;
}

9.6.6。嵌入式实体

嵌入式实体用于在您的 java 数据模型中具有值对象,即使您的数据库中只有一个表。在下面的示例中,您会看到它MyEntity@Embedded注释映射。这样做的结果是,在数据库中需要一个my_entity包含两列idname(来自EmbeddedEntity类)的表。

但是,如果该name列实际上null在结果集中,则整个属性embeddedEntity将根据onEmptyof设置为 null,当所有嵌套属性都是 时@Embedded,它是对象。 与此行为相反,尝试使用默认构造函数或从结果集中接受可为空参数值的构造函数来创建新实例。nullnull
USE_EMPTY

示例 58. 嵌入对象的示例代码
class MyEntity {

    @Id
    Integer id;

    @Embedded(onEmpty = USE_NULL) (1)
    EmbeddedEntity embeddedEntity;
}

class EmbeddedEntity {
    String name;
}
1 NullembeddedEntity如果namenull. _ 用于使用属性的潜在值进行USE_EMPTY实例化。embeddedEntitynullname

如果您在一个实体中多次需要一个值对象,这可以通过注解的可选prefix元素来实现。@Embedded此元素表示一个前缀,并为嵌入对象中的每个列名添加前缀。

使用快捷方式@Embedded.Nullable& @Embedded.Emptyfor@Embedded(onEmpty = USE_NULL)@Embedded(onEmpty = USE_EMPTY)来减少冗长,同时@javax.annotation.Nonnull相应地设置 JSR-305。

class MyEntity {

    @Id
    Integer id;

    @Embedded.Nullable (1)
    EmbeddedEntity embeddedEntity;
}
1 的快捷方式@Embedded(onEmpty = USE_NULL)

Collection包含 a或 a 的嵌入式实体Map将始终被视为非空,因为它们至少包含空集合或映射。null因此,即使使用@Embedded(onEmpty = USE_NULL),这样的实体也永远不会存在。

9.6.7。实体状态检测策略

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

表 2. Spring Data 中检测实体是否为新实体的选项

@Id-物业检查(默认)

默认情况下,Spring Data 检查给定实体的标识符属性。如果标识符属性是null0在原始类型的情况下,则假定实体是新的。否则,假定它不是新的。

@Version-物业检查

@Version如果存在用 和注释的属性null,或者在原始类型0的版本属性的情况下,实体被认为是新的。如果版本属性存在但具有不同的值,则该实体被认为不是新的。如果不存在版本属性,则 Spring Data 回退到对标识符属性的检查。

实施Persistable

如果实体实现Persistable,Spring Data 将新的检测委托给isNew(…)实体的方法。有关详细信息,请参阅Javadoc

注意:Persistable如果您使用AccessType.PROPERTY. 为避免这种情况,请使用@Transient.

提供自定义EntityInformation实现

您可以EntityInformation通过创建模块特定存储库工厂的子类并覆盖该getEntityInformation(…)方法来自定义存储库基础实现中使用的抽象。然后,您必须将模块特定存储库工厂的自定义实现注册为 Spring bean。请注意,这应该很少需要。

9.6.8。身份生成

Spring Data JDBC 使用 ID 来识别实体。实体的 ID 必须使用 Spring Data 的注解进行@Id注解。

当您的数据库具有 ID 列的自动增量列时,生成的值会在将其插入数据库后在实体中设置。

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

9.6.9。乐观锁定

@VersionSpring Data JDBC 通过在聚合根上注释的数字属性支持乐观锁定 。每当 Spring Data JDBC 保存具有此类版本属性的聚合时,会发生两件事:聚合根的更新语句将包含 where 子句,检查存储在数据库中的版本是否实际未更改。如果不是这种情况,OptimisticLockingFailureException将被抛出。此外,实体和数据库中的版本属性都会增加,因此并发操作会注意到更改并OptimisticLockingFailureException在适用的情况下抛出如上所述的。

此过程也适用于插入新聚合,其中 anull0version 表示新实例,然后增加的实例将实例标记为不再是新的,这使得在对象构造期间生成 id 的情况下(例如当 UUID 是用过的。

在删除期间,版本检查也适用,但不增加版本。

9.7。查询方法

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

您通常在存储库上触发的大多数数据访问操作都会导致针对数据库运行查询。定义这样的查询是在存储库接口上声明一个方法的问题,如以下示例所示:

示例 59. 带有查询方法的 PersonRepository
interface PersonRepository extends PagingAndSortingRepository<Person, String> {

  List<Person> findByFirstname(String firstname);                                   (1)

  List<Person> findByFirstnameOrderByLastname(String firstname, Pageable pageable); (2)

  Slice<Person> findByLastname(String lastname, Pageable pageable);                 (3)

  Page<Person> findByLastname(String lastname, Pageable pageable);                  (4)

  Person findByFirstnameAndLastname(String firstname, String lastname);             (5)

  Person findFirstByLastname(String lastname);                                      (6)

  @Query("SELECT * FROM person WHERE lastname = :lastname")
  List<Person> findByLastname(String lastname);                                     (7)
  @Query("SELECT * FROM person WHERE lastname = :lastname")
  Stream<Person> streamByLastname(String lastname);                                     (8)
}
1 该方法显示对所有具有给定firstname. 查询是通过解析可以与And和连接的约束的方法名称来派生的Or。因此,方法名称导致查询表达式为SELECT … FROM person WHERE firstname = :firstname.
2 用于Pageable将偏移量和排序参数传递给数据库。
3 返回一个Slice<Person>。选择LIMIT+1行以确定是否有更多数据要使用。ResultSetExtractor不支持自定义。
4 运行分页查询返回Page<Person>. 仅选择给定页面范围内的数据,并可能通过计数查询来确定总计数。ResultSetExtractor不支持自定义。
5 查找给定条件的单个实体。它以IncorrectResultSizeDataAccessException非唯一结果完成。
6 与 <3> 相比,即使查询产生更多结果文档,也始终发出第一个实体。
7 findByLastname方法显示对所有具有给定lastname.
8 streamByLastname方法返回 a Stream,这使得值一旦从数据库返回就成为可能。

下表显示了查询方法支持的关键字:

表 3. 查询方法支持的关键字
关键词 样本 逻辑结果

After

findByBirthdateAfter(Date date)

birthdate > date

GreaterThan

findByAgeGreaterThan(int age)

age > age

GreaterThanEqual

findByAgeGreaterThanEqual(int age)

age >= age

Before

findByBirthdateBefore(Date date)

birthdate < date

LessThan

findByAgeLessThan(int age)

age < age

LessThanEqual

findByAgeLessThanEqual(int age)

age <= age

Between

findByAgeBetween(int from, int to)

age BETWEEN from AND to

NotBetween

findByAgeNotBetween(int from, int to)

age NOT BETWEEN from AND to

In

findByAgeIn(Collection<Integer> ages)

age IN (age1, age2, ageN)

NotIn

findByAgeNotIn(Collection ages)

age NOT IN (age1, age2, ageN)

IsNotNull,NotNull

findByFirstnameNotNull()

firstname IS NOT NULL

IsNull,Null

findByFirstnameNull()

firstname IS NULL

Like, StartingWith,EndingWith

findByFirstnameLike(String name)

firstname LIKE name

NotLike,IsNotLike

findByFirstnameNotLike(String name)

firstname NOT LIKE name

Containing在字符串上

findByFirstnameContaining(String name)

firstname LIKE '%' + name + '%'

NotContaining在字符串上

findByFirstnameNotContaining(String name)

firstname NOT LIKE '%' + name + '%'

(No keyword)

findByFirstname(String name)

firstname = name

Not

findByFirstnameNot(String name)

firstname != name

IsTrue,True

findByActiveIsTrue()

active IS TRUE

IsFalse,False

findByActiveIsFalse()

active IS FALSE

查询派生仅限于可以在WHERE不使用连接的子句中使用的属性。

9.7.1。查询查找策略

JDBC 模块支持手动将查询定义为@Query注释中的字符串或属性文件中的命名查询。

从方法名称派生查询目前仅限于简单属性,这意味着属性直接存在于聚合根中。此外,此方法仅支持选择查询。

9.7.2。使用@Query

下面的例子展示了如何使用@Query来声明一个查询方法:

例 60. 使用 @Query 声明一个查询方法
interface UserRepository extends CrudRepository<User, Long> {

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

对于将查询结果转换为实体RowMapper,默认情况下使用与 Spring Data JDBC 自身生成的查询相同。您提供的查询必须与RowMapper预期的格式相匹配。必须提供在实体的构造函数中使用的所有属性的列。通过 setter、wither 或字段访问设置的属性列是可选的。结果中没有匹配列的属性将不会被设置。该查询用于填充聚合根、嵌入实体和一对一关系,包括作为 SQL 数组类型存储和加载的原始类型数组。为地图、列表、集合和实体数组生成单独的查询。

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

9.7.3。命名查询

如果上一节中描述的注释中没有给出查询,Spring Data JDBC 将尝试定位命名查询。有两种方法可以确定查询的名称。默认采用查询的域类,即存储库的聚合根,采用其简单名称并附加方法名称,并用 . 分隔.。或者,@Query注释有一个name属性,可用于指定要查找的查询的名称。

命名查询应该在META-INF/jdbc-named-queries.properties类路径的属性文件中提供。

可以通过将值设置为 来更改该文件的位置@EnableJdbcRepositories.namedQueriesLocation

流式传输结果

当您将 Stream 指定为查询方法的返回类型时,Spring Data JDBC 会在元素可用时立即返回。在处理大量数据时,这适用于减少延迟和内存需求。

该流包含与数据库的打开连接。为了避免内存泄漏,最终需要通过关闭流来关闭该连接。推荐的方法是使用try-with-resource clause. 这也意味着,一旦关闭与数据库的连接,流就无法获取更多元素并可能引发异常。

风俗RowMapper

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

@Bean
QueryMappingConfiguration rowMappers() {
  return new DefaultQueryMappingConfiguration()
    .register(Person.class, new PersonRowMapper())
    .register(Address.class, new AddressRowMapper());
}

在确定RowMapper方法使用哪个时,根据方法的返回类型,遵循以下步骤:

  1. 如果类型是简单类型,RowMapper则使用 no。

    相反,查询应该返回单行和单列,并且对返回类型的转换应用于该值。

  2. 迭代中的实体类,QueryMappingConfiguration直到找到一个是所讨论的返回类型的超类或接口。使用为该类RowMapper注册的。

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

如果适用,诸如集合之类的包装器类型Optional将被解包。因此,返回类型Optional<Person>使用Person前面过程中的类型。

使用自定义RowMapper通过QueryMappingConfiguration@Query(rowMapperClass=…)或自定义ResultSetExtractor禁用实体回调和生命周期事件,因为结果映射可以在需要时发出自己的事件/回调。
修改查询

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

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

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

  • void

  • int(更新记录数)

  • boolean(记录是否更新)

9.7.4。预测

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

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

示例 61. 示例聚合和存储库
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 提供了哪些方法来实现这一目标?本章的其余部分回答了这个问题。

基于界面的投影

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

示例 62. 检索属性子集的投影接口
interface NamesOnly {

  String getFirstname();
  String getLastname();
}

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

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

  Collection<NamesOnly> findByLastname(String lastname);
}

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

在您的方法中声明一个Repository覆盖基本方法的方法(例如,在 中声明CrudRepository,特定于存储的存储库接口,或Simple…Repository)导致调用基本方法,而不管声明的返回类型如何。确保使用兼容的返回类型,因为基本方法不能用于投影。一些商店模块支持@Query注释将覆盖的基本方法转换为查询方法,然后可用于返回投影。

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

示例 64. 检索属性子集的投影接口
interface PersonSummary {

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

  interface AddressSummary {
    String getCity();
  }
}

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

封闭投影

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

示例 65. 封闭投影
interface NamesOnly {

  String getFirstname();
  String getLastname();
}

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

打开投影

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

示例 66. 开放投影
interface NamesOnly {

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

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

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

示例 67. 使用自定义逻辑的默认方法的投影界面
interface NamesOnly {

  String getFirstname();
  String getLastname();

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

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

示例 68. 示例 Person 对象
@Component
class MyBean {

  String getFullName(Person person) {
    …
  }
}

interface NamesOnly {

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

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

示例 69. 示例 Person 对象
interface NamesOnly {

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

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

可空包装器

投影接口中的 getter 可以使用可为空的包装器来提高空安全性。当前支持的包装器类型有:

  • java.util.Optional

  • com.google.common.base.Optional

  • scala.Option

  • io.vavr.control.Option

示例 70. 使用可空包装器的投影接口
interface NamesOnly {

  Optional<String> getFirstname();
}

如果基础投影值不是null,则使用包装器类型的当前表示返回值。如果支持值是null,则 getter 方法返回所用包装器类型的空表示。

基于类的投影 (DTO)

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

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

以下示例显示了一个投影 DTO:

示例 71. 投影 DTO
class NamesOnly {

  private final String firstname, lastname;

  NamesOnly(String firstname, String lastname) {

    this.firstname = firstname;
    this.lastname = lastname;
  }

  String getFirstname() {
    return this.firstname;
  }

  String getLastname() {
    return this.lastname;
  }

  // equals(…) and hashCode() implementations
}
避免投影 DTO 的样板代码

您可以使用提供注释的Project Lombok显着简化 DTO 的代码(不要与前面接口示例中显示的@ValueSpring 注释混淆)。@Value如果您使用 Project Lombok 的@Value注释,前面显示的示例 DTO 将变为以下内容:

@Value
class NamesOnly {
	String firstname, lastname;
}

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

动态投影

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

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

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

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

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

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

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

9.8。MyBatis 集成

CRUD 操作和查询方法可以委托给 MyBatis。本节介绍如何配置 Spring Data JDBC 以与 MyBatis 集成,以及遵循哪些约定来移交查询的运行以及到库的映射。

9.8.1。配置

将 MyBatis 正确插入 Spring Data JDBC 的最简单方法是导入MyBatisJdbcConfiguration应用程序配置:

@Configuration
@EnableJdbcRepositories
@Import(MyBatisJdbcConfiguration.class)
class Application {

  @Bean
  SqlSessionFactoryBean sqlSessionFactoryBean() {
    // Configure MyBatis here
  }
}

如您所见,您需要声明的只是SqlSessionFactoryBeanasMyBatisJdbcConfiguration依赖于最终SqlSession可用的 bean 。ApplicationContext

9.8.2. 使用约定

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

语句的名称是通过将实体类型的完全限定名称与确定语句的类型连接起来而构造Mapper.String。例如,如果org.example.User要插入 的实例,Spring Data JDBC 会查找名为org.example.UserMapper.insert.

当语句运行时,[ MyBatisContext] 的一个实例作为参数传递,这使得语句可以使用各种参数。

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

姓名 目的 可能触发此语句的 CrudRepository 方法 中可用的属性MyBatisContext

insert

插入单个实体。这也适用于聚合根引用的实体。

save, saveAll.

getInstance:要保存的实例

getDomainType:要保存的实体的类型。

get(<key>):引用实体的ID,这里<key>是提供的反向引用列的名称NamingStrategy

update

更新单个实体。这也适用于聚合根引用的实体。

save, saveAll.

getInstance:要保存的实例

getDomainType:要保存的实体的类型。

delete

删除单个实体。

delete, deleteById.

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>

选择一组被另一个实体引用的实体。引用实体的类型用于前缀。引用的实体类型用作后缀。此方法已弃用。findAllByPath改为使用

所有find*方法。如果没有定义查询findAllByPath

getId: 引用要加载的实体的实体的 ID。

getDomainType:要加载的实体的类型。

findAllByPath-<propertyPath>

选择由另一个实体通过属性路径引用的一组实体。

所有find*方法。

getIdentifier:Identifier持有聚合根的 id 加上所有路径元素的键和列表索引。

getDomainType:要加载的实体的类型。

findAllSorted

选择所有聚合根,排序

findAll(Sort).

getSort: 排序规范。

findAllPaged

选择聚合根的页面,可选择排序

findAll(Page).

getPageable: 分页规范。

count

计算用作前缀的类型的聚合根数

count

getDomainType:要计算的聚合根的类型。

9.9。生命周期事件

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

@Bean
ApplicationListener<BeforeSaveEvent<Object>> loggingSaves() {

	return event -> {

		Object entity = event.getEntity();
		LOG.info("{} is getting saved.", entity);
	};
}

如果您只想处理特定域类型的事件,您可以从其中派生侦听器AbstractRelationalEventListener并覆盖一个或多个onXXX方法,其中XXX代表事件类型。回调方法只会被与域类型及其子类型相关的事件调用,因此您不需要进一步转换。

class PersonLoadListener extends AbstractRelationalEventListener<Person> {

	@Override
	protected void onAfterLoad(AfterLoadEvent<Person> personLoad) {
		LOG.info(personLoad.getEntity());
	}
}

下表描述了可用的事件:

表 4. 可用事件
事件 什么时候发布

BeforeDeleteEvent

在聚合根被删除之前。

AfterDeleteEvent

在聚合根被删除后。

BeforeConvertEvent

在聚合根被转换为执行 SQL 语句的计划之前,但在决定聚合是否是新的之后,即是否按顺序进行更新或插入。如果您想以编程方式设置 id,这是正确的事件。

BeforeSaveEvent

在保存聚合根之前(即插入或更新,但在决定是否插入或更新之后)。

AfterSaveEvent

在保存聚合根之后(即插入或更新)。

AfterLoadEvent

在从数据库创建聚合根ResultSet并设置其所有属性之后。注意:这已被弃用。AfterConvert改为使用

AfterConvertEvent

在从数据库创建聚合根ResultSet并设置其所有属性之后。

生命周期事件依赖于 a ApplicationEventMulticaster,在这种情况下SimpleApplicationEventMulticaster可以配置 a TaskExecutor,因此不保证何时处理事件。

9.9.1。特定于商店的 EntityCallbacks

Spring Data JDBC 使用EntityCallbackAPI 来提供审计支持,并对以下回调做出反应:

表 5. 可用回调
EntityCallback 什么时候发布

BeforeDeleteCallback

在聚合根被删除之前。

AfterDeleteCallback

在聚合根被删除后。

BeforeConvertCallback

在聚合根被转换为执行 SQL 语句的计划之前,但在决定聚合是否是新的之后,即是否按顺序进行更新或插入。如果您想以编程方式设置 id,这是正确的回调。

BeforeSaveCallback

在保存聚合根之前(即插入或更新,但在决定是否插入或更新之后)。

在 Spring Data JDBC 的 2.x 版本中,您可以使用它来为实体创建 id。将来唯一正确使用回调的首选方式是BeforeConvertCallback.

AfterSaveCallback

在保存聚合根之后(即插入或更新)。

AfterLoadCallback

在从数据库创建聚合根ResultSet并设置其所有属性之后。已弃用,请AfterConvertCallback改用

AfterConvertCallback

在从数据库创建聚合根ResultSet并设置其所有属性之后。

9.10。实体回调

Spring Data 基础设施提供了在调用某些方法之前和之后修改实体的钩子。那些所谓EntityCallback的实例提供了一种方便的方式来检查和潜在地修改回调风格的实体。
AnEntityCallback看起来很像专门的ApplicationListener. BeforeSaveEvent一些 Spring Data 模块发布允许修改给定实体的存储特定事件(例如)。在某些情况下,例如使用不可变类型时,这些事件可能会导致麻烦。此外,事件发布依赖于ApplicationEventMulticaster. 如果使用异步配置TaskExecutor它可能会导致不可预测的结果,因为事件处理可以分叉到线程上。

实体回调提供具有同步和反应式 API 的集成点,以保证在处理链中定义明确的检查点按顺序执行,返回可能修改的实体或反应式包装器类型。

实体回调通常按 API 类型分隔。这种分离意味着同步 API 只考虑同步实体回调,而反应式实现只考虑反应式实体回调。

Spring Data Commons 2.2 引入了实体回调 API。这是应用实体修改的推荐方式。在调用可能已注册的实例之前ApplicationEvents,仍会发布特定于现有商店的现有商店。EntityCallback

9.10.1。实现实体回调

AnEntityCallback通过其泛型类型参数直接与其域类型相关联。每个 Spring Data 模块通常附带一组EntityCallback涵盖实体生命周期的预定义接口。

示例 74. 解剖学EntityCallback
@FunctionalInterface
public interface BeforeSaveCallback<T> extends EntityCallback<T> {

	/**
	 * Entity callback method invoked before a domain object is saved.
	 * Can return either the same or a modified instance.
	 *
	 * @return the domain object to be persisted.
	 */
	T onBeforeSave(T entity <2>, String collection <3>); (1)
}
1 BeforeSaveCallback在保存实体之前调用的特定方法。返回一个可能修改过的实例。
2 在持久化之前的实体。
3 许多存储特定参数,例如实体持久保存到的集合。
示例 75. 反应性剖析EntityCallback
@FunctionalInterface
public interface ReactiveBeforeSaveCallback<T> extends EntityCallback<T> {

	/**
	 * Entity callback method invoked on subscription, before a domain object is saved.
	 * The returned Publisher can emit either the same or a modified instance.
	 *
	 * @return Publisher emitting the domain object to be persisted.
	 */
	Publisher<T> onBeforeSave(T entity <2>, String collection <3>); (1)
}
1 BeforeSaveCallback在保存实体之前,在订阅时调用的特定方法。发出一个可能修改过的实例。
2 在持久化之前的实体。
3 许多存储特定参数,例如实体持久保存到的集合。
可选的实体回调参数由实现的 Spring Data 模块定义,并从EntityCallback.callback().

实现适合您的应用程序需求的接口,如下例所示:

示例 76. 示例BeforeSaveCallback
class DefaultingEntityCallback implements BeforeSaveCallback<Person>, Ordered {      (2)

	@Override
	public Object onBeforeSave(Person entity, String collection) {                   (1)

		if(collection == "user") {
		    return // ...
		}

		return // ...
	}

	@Override
	public int getOrder() {
		return 100;                                                                  (2)
	}
}
1 根据您的要求实现回调。
2 如果存在多个相同域类型的实体回调,则可能会订购实体回调。排序遵循最低优先级。

9.10.2。注册实体回调

EntityCallbackbean 被商店特定的实现拾取,以防它们在ApplicationContext. 大多数模板 API 已经实现ApplicationContextAware,因此可以访问ApplicationContext

以下示例说明了一组有效的实体回调注册:

示例 77. 示例EntityCallbackBean 注册
@Order(1)                                                           (1)
@Component
class First implements BeforeSaveCallback<Person> {

	@Override
	public Person onBeforeSave(Person person) {
		return // ...
	}
}

@Component
class DefaultingEntityCallback implements BeforeSaveCallback<Person>,
                                                           Ordered { (2)

	@Override
	public Object onBeforeSave(Person entity, String collection) {
		// ...
	}

	@Override
	public int getOrder() {
		return 100;                                                  (2)
	}
}

@Configuration
public class EntityCallbackConfiguration {

    @Bean
    BeforeSaveCallback<Person> unorderedLambdaReceiverCallback() {   (3)
        return (BeforeSaveCallback<Person>) it -> // ...
    }
}

@Component
class UserCallbacks implements BeforeConvertCallback<User>,
                                        BeforeSaveCallback<User> {   (4)

	@Override
	public Person onBeforeConvert(User user) {
		return // ...
	}

	@Override
	public Person onBeforeSave(User user) {
		return // ...
	}
}
1 BeforeSaveCallback@Order注释中接收它的命令。
2 BeforeSaveCallbackOrdered通过接口实现接收其订单。
3 BeforeSaveCallback使用 lambda 表达式。默认无序,最后调用。请注意,由 lambda 表达式实现的回调不会公开输入信息,因此使用不可分配的实体调用这些会影响回调吞吐量。使用classorenum为回调 bean 启用类型过滤。
4 在单个实现类中组合多个实体回调接口。

9.11。自定义转化

Spring Data JDBC 允许注册自定义转换器以影响值在数据库中的映射方式。目前,转换器仅应用于属性级别。

9.11.1。使用已注册的 Spring 转换器编写属性

以下示例显示了ConverterBoolean对象转换为String值的 a 的实现:

import org.springframework.core.convert.converter.Converter;

@WritingConverter
public class BooleanToStringConverter implements Converter<Boolean, String> {

    @Override
    public String convert(Boolean source) {
        return source != null && source ? "T" : "F";
    }
}

这里有几件事需要注意:Boolean并且String都是简单类型,因此 Spring Data 需要提示该转换器应该应用的方向(读取或写入)。通过注释这个转换器,你可以指示 Spring Data 像在数据库中一样@WritingConverter写入每个Boolean属性。String

9.11.2。使用 Spring 转换器读取

以下示例显示了Converter从 a 转换StringBoolean值的 a 的实现:

@ReadingConverter
public class StringToBooleanConverter implements Converter<String, Boolean> {

    @Override
    public Boolean convert(String source) {
        return source != null && source.equalsIgnoreCase("T") ? Boolean.TRUE : Boolean.FALSE;
    }
}

这里有几件事需要注意:String并且Boolean都是简单类型,因此 Spring Data 需要提示该转换器应该应用的方向(读取或写入)。通过注释这个转换器,@ReadingConverter你可以指示 Spring Data 转换String数据库中应该分配给Boolean属性的每个值。

9.11.3。注册 Spring 转换器JdbcConverter

class MyJdbcConfiguration extends AbstractJdbcConfiguration {

    // …

	@Override
    protected List<?> userConverters() {
		return Arrays.asList(new BooleanToStringConverter(), new StringToBooleanConverter());
	}

}
在以前版本的 Spring Data JDBC 中,建议直接覆盖AbstractJdbcConfiguration.jdbcCustomConversions(). 这不再是必要的,甚至是不推荐的,因为该方法组合了用于所有数据库的转换Dialect、用户注册的转换和用户注册的转换。如果您正在从旧版本的 Spring Data JDBC 迁移并且已经AbstractJdbcConfiguration.jdbcCustomConversions()覆盖了您的转换,Dialect则不会注册。

9.11.4。数据库值

值转换用于JdbcValue丰富传播到具有java.sql.Types类型的 JDBC 操作的值。如果您需要指定特定于 JDBC 的类型而不是使用类型派生,请注册自定义写入转换器。此转换器应将值转换为JdbcValue具有值和实际字段的值JDBCType

下面的 SpringConverter实现示例将 a 转换String为自定义Email值对象:

@ReadingConverter
public class EmailReadConverter implements Converter<String, Email> {

  public Email convert(String source) {
    return Email.valueOf(source);
  }
}

如果您编写Converter的源类型和目标类型都是本机类型,我们无法确定应该将其视为读取转换器还是写入转换器。将转换器实例注册为两者可能会导致不需要的结果。例如,a是模棱两可的,尽管在编写时尝试将所有实例转换为实例Converter<String, Long>可能没有意义。为了让您强制基础架构仅以一种方式注册转换器,我们提供了要在转换器实现中使用的注释。StringLong@ReadingConverter@WritingConverter

转换器需要进行显式注册,因为不会从类路径或容器扫描中提取实例,以避免不必要的转换服务注册以及此类注册产生的副作用。转换器注册CustomConversions为中央设施,允许根据源和目标类型注册和查询已注册的转换器。

CustomConversions附带一组预定义的转换器注册:

  • JSR-310 用于在和类型之间java.time进行转换的转换器。java.util.DateString

  • 已弃用:用于在org.joda.time、JSR-310 和java.util.Date.

  • 已弃用:ThreeTenBackport 转换器,用于在org.joda.time、JSR-310 和java.util.Date.

本地时间类型(例如LocalDateTimeto java.util.Date)的默认转换器依赖于系统默认时区设置在这些类型之间进行转换。您可以通过注册自己的转换器来覆盖默认转换器。
转换器消歧

通常,我们检查Converter它们转换的源和目标类型的实现。根据其中一个是否是底层数据访问 API 可以本地处理的类型,我们将转换器实例注册为读取或写入转换器。以下示例显示了写入和读取转换器(请注意,区别在于 限定词的顺序Converter):

// Write converter as only the target type is one that can be handled natively
class MyConverter implements Converter<Person, String> { … }

// Read converter as only the source type is one that can be handled natively
class MyConverter implements Converter<String, Person> { … }

9.12。日志记录

Spring Data JDBC 本身几乎没有日志记录。相反,JdbcTemplate发出 SQL 语句的机制提供了日志记录。因此,如果您想检查运行了哪些 SQL 语句,请激活 SpringNamedParameterJdbcTemplateMyBatis的日志记录。

9.13。交易性

CrudRepository实例的方法默认是事务性的。对于读取操作,事务配置readOnly标志设置为true。所有其他人都配置了一个简单的@Transactional注释,以便应用默认的事务配置。有关详细信息,请参阅SimpleJdbcRepository. 如果您需要调整存储库中声明的方法之一的事务配置,请在存储库接口中重新声明该方法,如下所示:

示例 78. CRUD 的自定义事务配置
interface UserRepository extends CrudRepository<User, Long> {

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

  // Further query method declarations
}

上述导致该findAll()方法以 10 秒的超时时间运行并且没有readOnly标志。

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

示例 79. 使用外观为多个存储库调用定义事务
@Service
public class UserManagementImpl implements UserManagement {

  private final UserRepository userRepository;
  private final RoleRepository roleRepository;

  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才能获得基于注释的配置以使外观正常工作。请注意,前面的示例假设您使用组件扫描。

9.13.1。事务查询方法

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

示例 80. 在查询方法中使用 @Transactional
@Transactional(readOnly = true)
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()使用@Modifying注释并覆盖事务配置。因此,该方法的readOnly标志设置为false

强烈建议使查询方法具有事务性。这些方法可能会执行一个以上的查询以填充实体。没有公共事务,Spring Data JDBC 在不同的连接中执行查询。这可能会给连接池带来过大的压力,甚至可能在多个方法在保持一个连接的同时请求新连接时导致死锁。
通过设置标志来标记只读查询绝对是合理的readOnly。但是,这并不能作为您不触发操纵查询的检查(尽管某些数据库拒绝INSERTUPDATE声明在只读事务中)。相反,该readOnly标志作为提示传播到底层 JDBC 驱动程序以进行性能优化。

9.14。审计

9.14.1。基本

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

仅跟踪创建和修改日期的应用程序不需要指定AuditorAware.

基于注释的审计元数据

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

示例 81. 被审计实体
class Customer {

  @CreatedBy
  private User user;

  @CreatedDate
  private Instant createdDate;

  // … further properties omitted
}

如您所见,可以有选择地应用注释,具体取决于您要捕获的信息。DateTime可以在 Joda-Time、 、legacy JavaDateCalendar、JDK8 日期和时间类型以及long或类型的属性上使用捕获更改时捕获的注释Long

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

示例 82. 嵌入实体中的审计元数据
class Customer {

  private AuditMetadata auditingMetadata;

  // … further properties omitted
}

class AuditMetadata {

  @CreatedBy
  private User user;

  @CreatedDate
  private Instant createdDate;

}
基于接口的审计元数据

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

AuditorAware

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

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

示例 83.AuditorAware基于 Spring Security的实现
class SpringSecurityAuditorAware implements AuditorAware<User> {

  @Override
  public Optional<User> getCurrentAuditor() {

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

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

ReactiveAuditorAware

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

以下示例显示了使用响应式 Spring SecurityAuthentication对象的接口实现:

示例 84.ReactiveAuditorAware基于 Spring Security的实现
class SpringSecurityAuditorAware implements ReactiveAuditorAware<User> {

  @Override
  public Mono<User> getCurrentAuditor() {

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

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

9.15。JDBC 审计

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

示例 85. 使用 Java 配置激活审计
@Configuration
@EnableJdbcAuditing
class Config {

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

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

9.16。JDBC 锁定

Spring Data JDBC 支持锁定派生查询方法。要启用对存储库中给定派生查询方法的锁定,请使用@Lock. type 的 required 值LockMode提供了两个值:PESSIMISTIC_READ一个保证你正在读取的数据不会被修改,PESSIMISTIC_WRITE另一个是获取一个锁来修改数据。一些数据库没有进行这种区分。在这种情况下,两种模式都等效于PESSIMISTIC_WRITE

示例 86. 在派生查询方法上使用 @Lock
interface UserRepository extends CrudRepository<User, Long> {

  @Lock(LockMode.PESSIMISTIC_READ)
  List<User> findByLastname(String lastname);
}

正如您在上面看到的,该方法findByLastname(String lastname)将使用悲观读锁执行。如果您使用 MySQL 方言的数据库,这将导致例如以下查询:

示例 87. MySQL 方言的结果 Sql 查询
Select * from user u where u.lastname = lastname LOCK IN SHARE MODE

替代LockMode.PESSIMISTIC_READ你可以使用LockMode.PESSIMISTIC_WRITE.

附录

附录 A:词汇表

AOP

面向方面的编程

CRUD

创建、读取、更新、删除 - 基本的持久化操作

依赖注入

将组件的依赖项从外部传递给组件的模式,释放组件以查找依赖项本身。有关详细信息,请参阅https://en.wikipedia.org/wiki/Dependency_Injection

JPA

Java 持久性 API

Spring

Java应用框架—— https://projects.spring.io/spring-framework

附录 B:命名空间参考

<repositories />元素_

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

表 6. 属性
姓名 描述

base-package

定义要扫描的包以*Repository自动检测模式扩展的存储库接口(实际接口由特定的 Spring Data 模块确定)。配置包下的所有包也会被扫描。允许使用通配符。

repository-impl-postfix

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

query-lookup-strategy

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

named-queries-location

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

consider-nested-repositories

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

附录 C:Populators 命名空间参考

<populator /> 元素

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

表 7. 属性
姓名 描述

locations

在哪里可以找到从存储库中读取对象的文件。

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

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

下表列出了 Spring Data repository 查询派生机制通常支持的主题关键字来表达谓词。有关受支持关键字的确切列表,请参阅特定于商店的文档,因为此处列出的某些关键字可能在特定商店中不受支持。

表 8. 查询主题关键字
关键词 描述

find…By, read…By, get…By, query…By, search…By,stream…By

一般查询方法通常返回存储库类型、CollectionStreamable子类型或结果包装器,例如PageGeoResults或任何其他特定于存储的结果包装器。可用作findBy…findMyDomainTypeBy…或与其他关键字组合使用。

exists…By

存在投影,通常返回boolean结果。

count…By

计数投影返回数字结果。

delete…By,remove…By

删除查询方法返回无结果 ( void) 或删除计数。

…First<number>…,…Top<number>…

将查询结果限制为第一个<number>结果。此关键字可以出现在find(和其他关键字)和之间的主题的任何位置by

…Distinct…

使用不同的查询仅返回唯一的结果。请查阅特定于商店的文档是否支持该功能。此关键字可以出现在find(和其他关键字)和之间的主题的任何位置by

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

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

表 9. 查询谓词关键字
逻辑关键字 关键字表达式

AND

And

OR

Or

AFTER

After,IsAfter

BEFORE

Before,IsBefore

CONTAINING

Containing, IsContaining,Contains

BETWEEN

Between,IsBetween

ENDING_WITH

EndingWith, IsEndingWith,EndsWith

EXISTS

Exists

FALSE

False,IsFalse

GREATER_THAN

GreaterThan,IsGreaterThan

GREATER_THAN_EQUALS

GreaterThanEqual,IsGreaterThanEqual

IN

In,IsIn

IS

Is, Equals, (或没有关键字)

IS_EMPTY

IsEmpty,Empty

IS_NOT_EMPTY

IsNotEmpty,NotEmpty

IS_NOT_NULL

NotNull,IsNotNull

IS_NULL

Null,IsNull

LESS_THAN

LessThan,IsLessThan

LESS_THAN_EQUAL

LessThanEqual,IsLessThanEqual

LIKE

Like,IsLike

NEAR

Near,IsNear

NOT

Not,IsNot

NOT_IN

NotIn,IsNotIn

NOT_LIKE

NotLike,IsNotLike

REGEX

Regex, MatchesRegex,Matches

STARTING_WITH

StartingWith, IsStartingWith,StartsWith

TRUE

True,IsTrue

WITHIN

Within,IsWithin

除了过滤谓词之外,还支持以下修饰符列表:

表 10. 查询谓词修饰符关键字
关键词 描述

IgnoreCase,IgnoringCase

与谓词关键字一起用于不区分大小写的比较。

AllIgnoreCase,AllIgnoringCase

忽略所有合适属性的大小写。在查询方法谓词的某处使用。

OrderBy…

指定静态排序顺序,后跟属性路径和方向(例如OrderByFirstnameAscLastnameDesc)。

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

支持的查询返回类型

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

地理空间类型(例如GeoResultGeoResultsGeoPage)仅适用于支持地理空间查询的数据存储。一些商店模块可能会定义自己的结果包装器类型。
表 11. 查询返回类型
返回类型 描述

void

表示没有返回值。

原语

Java 原语。

包装器类型

Java 包装器类型。

T

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

Iterator<T>

一个Iterator

Collection<T>

一个Collection

List<T>

一个List

Optional<T>

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

Option<T>

Scala 或 VavrOption类型。语义上与 Java 8 相同的行为Optional,如前所述。

Stream<T>

一个 Java 8 Stream

Streamable<T>

该直接的便利扩展Iterable公开了流式处理、映射和过滤结果、连接它们等的方法。

实现Streamable并采用Streamable构造函数或工厂方法参数的类型

公开构造函数或….of(…)/….valueOf(…)工厂方法的类型,Streamable作为参数。有关详细信息,请参阅返回自定义 Streamable Wrapper 类型

Vavr Seq, List, Map,Set

Vavr 集合类型。有关详细信息,请参阅对 Vavr 集合的支持。

Future<T>

一个Future。期望一个方法被注释@Async并且需要启用 Spring 的异步方法执行能力。

CompletableFuture<T>

一个 Java 8 CompletableFuture。期望一个方法被注释@Async并且需要启用 Spring 的异步方法执行能力。

ListenableFuture

一个org.springframework.util.concurrent.ListenableFuture。期望一个方法被注释@Async并且需要启用 Spring 的异步方法执行能力。

Slice<T>

一定大小的数据块,指示是否有更多可用数据。需要Pageable方法参数。

Page<T>

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

GeoResult<T>

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

GeoResults<T>

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

GeoPage<T>

APageGeoResult<T>,例如到参考位置的平均距离。

Mono<T>

Mono使用反应式存储库发射零个或一个元素的项目反应器。期望查询方法最多返回一个结果。如果没有找到结果,Mono.empty()则返回。多个结果会触发IncorrectResultSizeDataAccessException.

Flux<T>

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

Single<T>

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

Maybe<T>

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

Flowable<T>

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


1. see XML Configuration