Spring Data JDBC参考文献附录

原文:https://www.docs4dev.com/docs/zh/spring-data-jdbc/1.0.5.RELEASE/reference/all.html

前言

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

项目元数据

1. 新的和值得注意的

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

1.1. Spring Data JDBC 1.0中的新功能

2. 依赖关系

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

示例1.使用Spring Data版本系列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 。列车名称按字母顺序上升,当前可用的列车列于 here 。版本名称遵循以下模式: ${name}-${release} ,其中release可以是以下之一:

可以在我们的 Spring Data examples repository 中找到使用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进行依赖关系管理

Spring Boot为您选择最新版本的Spring Data模块。如果您仍想升级到较新版本,请将属性 spring-data-releasetrain.version 配置为您要使用的 train name and iteration

2.2. Spring框架

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

3. 使用Spring Data Repositories

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

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

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

3.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的实体。

我们还提供特定于持久性技术的抽象,例如 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,声明这些查询将分为四个步骤:

interface PersonRepository extends Repository<Person, Long> { … }
interface PersonRepository extends Repository<Person, Long> {
  List<Person> findByLastname(String lastname);
}
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;

@EnableJpaRepositories
class Config { … }
<?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变体未显式配置包,因为默认情况下使用带注释的类的包。要自定义要扫描的包,请使用特定于数据存储库的 @Enable${store}Repositories -annotation的 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接口,还可以使用 @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提供的所选 Store 的基本存储库实现中(例如,如果您使用JPA,实现是 SimpleJpaRepository ),因为它们匹配 CrudRepository 中的方法签名。因此 UserRepository 现在可以保存用户,按ID查找单个用户,并触发查询以通过电子邮件地址查找 Users

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

3.3.2. 使用具有多个Spring数据模块的存储库

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

以下示例显示了使用特定于模块的接口的存储库(在本例中为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。

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

示例10.使用带注释的域类的存储库定义

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

@Entity
class Person { … }

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

@Document
class User { … }

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

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

示例11.使用具有混合注释的域类的存储库定义

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不再能够将存储库分开,从而导致未定义的行为。

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

区分存储库的最后一种方法是通过确定存储库基础包的范围。基础包定义了扫描存储库接口定义的起点,这意味着将存储库定义放在相应的包中。默认情况下,注释驱动的配置使用配置类的包。 base package in XML-based configuration 是强制性的。

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

示例12.基础包的注释驱动配置

@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 属性。特定数据存储可能不支持某些策略。

3.4.2. 查询创建

Spring Data存储库基础结构中内置的查询构建器机制对于构建对存储库实体的约束查询非常有用。该机制从方法中剥离前缀 find…By ,_ 83316query…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);
}

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

3.4.3. property 表达

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

List<Person> findByAddressZipCode(ZipCode zipCode);

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

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

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

List<Person> findByAddress_ZipCode(ZipCode zipCode);

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

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 实例所需的其他元数据(反过来,这意味着不会发出必要的附加计数查询)。相反,它限制查询仅查找给定范围的实体。

要了解整个查询的页数,必须触发其他计数查询。默认情况下,此查询是从您实际触发的查询派生的。

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 关键字中。

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

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

3.4.6. 存储库方法返回集合或Iterables

返回多个结果的查询方法可以使用标准Java IterableListSet 。除此之外,我们支持返回Spring Data的 StreamableIterable 的自定义扩展,以及 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允许使用这些包装类型作为查询方法返回类型,如果它们满足以下标准:

示例用例如下所示:

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)
}
1Product 实体,公开API以访问产品的价格。
2Streamable<Product> 的包装类型,可以通过 Products.of(…) (通过Lombok注释创建的工厂方法)构建。
3包装类型公开了在 Streamable<Product> 上计算新值的其他API。
4该包装类型可以直接用作查询方法返回类型。无需返回 Stremable<Product> 并手动将其包装在存储库客户端中。
支持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 (相当于 Iterable 的Vavr),我们从实际返回值派生实现类,即 java.util.List 将变为Vavr List / Seqjava.util.Set 变为Vavr LinkedHashSet / Set 等。

3.4.7. 存储库方法的空处理

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

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

Nullability Annotations

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

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

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

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

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

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

示例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存储库位于我们已定义非空行为的包(或子包)中。
2当执行的查询未产生结果时,抛出 EmptyResultDataAccessException 。当 emailAddress 交给方法 null 时,抛出 IllegalArgumentException
3当执行的查询未产生结果时,返回 null 。同时接受 null 作为 emailAddress 的值。
4当执行的查询未产生结果时返回 Optional.empty() 。当 emailAddress 交给方法 null 时,抛出 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此方法对 firstname 参数接受 null ,如果查询执行未产生结果,则返回 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);

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

示例21.在try-with-resources块中使用 Stream<T> 结果

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

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

3.4.9. 异步查询结果

可以使用 Spring’s asynchronous method execution capability 异步运行存储库查询。这意味着该方法在调用时立即返回,而实际的查询执行发生在已提交给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 Data存储库

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

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

</beans:beans>

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

使用过滤器

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

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

示例23.使用exclude-filter元素

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

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

3.5.2. JavaConfig

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

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

示例24.基于样本注释的存储库配置

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

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

前面的示例使用JPA特定的注释,您可以根据实际使用的 Store 模块进行更改。这同样适用于 EntityManagerFactory bean的定义。请参阅涵盖特定于 Store 的配置的部分。

3.5.3. 独立使用

您还可以在Spring容器之外使用存储库基础结构 - 例如,在CDI环境中。您仍然需要在类路径中使用一些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
  }
}

对应于片段接口的类名最重要的部分是 Impl postfix。

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

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

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

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

  // Declare query methods here
}

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

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
}

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

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

示例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> {
}
配置

如果使用命名空间配置,则存储库基础结构会尝试通过扫描其找到存储库的包下面的类来自动检测自定义实现片段。这些类需要遵循将命名空间元素的 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") 注释 UserRepository 接口,则bean名称加 Impl 将匹配 com.acme.impl.two 中为存储库实现定义的名称,并使用它而不是第一个。

手动接线

如果您的自定义实现仅使用基于注释的配置和自动装配,则前面显示的方法效果很好,因为它被视为任何其他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. 自定义Base Repository

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

示例36.自定义存储库基类

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

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

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

示例37.使用JavaConfig配置自定义存储库基类

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

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

示例38.使用XML配置自定义存储库基类

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

3.7. 从聚合根发布事件

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

示例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发布所有事件后,我们有一个用 @AfterDomainEventPublication 注释的方法。它可用于潜在地清除要发布的事件列表(以及其他用途)。

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

3.8. Spring数据扩展

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

3.8.1. Querydsl Extension

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. 网络支持

本节包含Spring Data Web支持的文档,因为它在Spring Data Commons的当前(及更高版本)版本中实现。由于新引入的支持改变了很多东西,我们在 [web.legacy] 中保留了以前行为的文档。

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

示例42.启用Spring Data Web支持

@Configuration
@EnableWebMvc
@EnableSpringDataWebSupport
class WebConfiguration {}

@EnableSpringDataWebSupport 注释会记录我们稍后将讨论的一些组件。它还将检测类路径上的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" />
基本Web支持

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

目前,存储库必须实现 CrudRepository 才有资格被发现进行转换。

HandlerMethodArgumentResolvers for Pageable和Sort

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

page要检索的页面。 0索引并默认为0.
size要检索的页面大小。默认为20.
sort应按 property,property(,ASC|DESC) 格式排序的属性。默认排序方向是升序。如果要切换方向,请使用多个 sort 参数 - 例如, ?sort=firstname&sort=lastname,asc

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

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

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

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

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

你必须填充 thing1_pagething2_page 等等。

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

对页面的超媒体支持

Spring HATEOAS附带了一个表示模型类( PagedResources ),它允许使用必要的 Page 元数据丰富 Page 实例的内容以及允许客户端轻松浏览页面的链接。将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(…) 具有以下效果:

假设我们在数据库中有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数据绑定支持

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

示例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 。这样,可以轻松地减轻源文档结构的变化,而无需客户端调用公开的方法(通常是基于类的有效负载绑定的缺点)。

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

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

有关更多信息,请参阅规范 Spring Data Examples repository 中的 web projection example

Querydsl网络支持

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

请考虑以下查询字符串:

?firstname=Dave&lastname=Matthews

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

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

在类路径中找到Querydsl时,该功能与 @EnableSpringDataWebSupport 一起自动启用。

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

类型信息通常从方法的返回类型中解析。由于该信息不一定与域类型匹配,因此使用 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将查询字符串参数解析为 Predicate 匹配 User

默认绑定如下:

可以通过 @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)
  }
}
1QuerydslPredicateExecutor 提供对 Predicate 的特定查找程序方法的访问。
存储库界面上定义的2QuerydslBinderCustomizer 会自动获取并快捷方式 @QuerydslPredicate(bindings=…)
3username 属性的绑定定义为简单的 contains 绑定。
4String 属性的默认绑定定义为不区分大小写的 contains 匹配。
5Predicate 分辨率中排除 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,请声明类似于以下内容的populator:

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

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

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

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

示例50.声明一个解组存储库populator(使用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>

4. JDBC存储库

本章指出了JDBC的存储库支持的特性。这基于 Working with Spring Data Repositories 中解释的核心存储库支持。你应该对那里解释的基本概念有充分的理解。

4.1. 为什么选择Spring Data JDBC?

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

为了帮助开发人员,JPA做了很多事情。除此之外,它还跟踪实体的变化。它为您提供延迟加载。它允许您将各种对象构造映射到同样广泛的数据库设计。

这很棒,让很多事情变得非常简单。只需看一下基本的JPA教程。但是,为什么JPA会做某件事常常让人感到困惑。而且,JPA在概念上非常简单。

Spring Data JDBC通过采用以下设计决策,旨在简化概念:

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

所有Spring Data模块都受到Domain Driven Design中“repository”,“aggregate”和“aggregate root”概念的启发。对于Spring Data JDBC来说,这些可能更为重要,因为在使用关系数据库时,它们在某种程度上与正常做法相反。

聚合是一组实体,它们保证在原子更改之间保持一致。一个典型的例子是带有 OrderItemsOrderOrder 上的属性(例如, numberOfItemsOrderItems 的实际数量一致)在进行更改时保持一致。

跨聚合的引用不保证始终保持一致。它们最终会保持一致。

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

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

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

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

4.3. 基于注释的配置

可以通过Java配置通过注释激活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(…) 方法执行保存聚合。如果聚合是新的,则会导致聚合根的插入,然后是所有直接或间接引用的实体的插入语句。

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

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

4.4.1. 对象映射基础知识

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

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

对象创建

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

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

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

对象创建内部

为了避免反射的开销,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%。要使域类符合此类优化的条件,它需要遵守一组约束:

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

property 人口

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

property 人口内部

与我们的 optimizations in object construction 类似,我们还使用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)
    }
  }
}
1PropertyAccessor持有底层对象的可变实例。这是为了实现其他不可变属性的突变。
2默认情况下,Spring Data使用字段访问来读取和写入属性值。根据 private 字段的可见性规则, MethodHandles 用于与字段交互。
3该类公开了一个用于设置标识符的 withId(…) 方法,例如将实例插入数据存储区并生成标识符时。调用 withId(…) 会创建一个新的 Person 对象。所有后续突变都将在新实例中发生,而前一个突变不会发生。
4使用property-access允许直接方法调用而不使用 MethodHandles

这使我们在反射方面的性能提升了25%。要使域类符合此类优化的条件,它需要遵守一组约束:

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

我们来看看以下实体:

例53.一个样本实体

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 实例在创建新实例时保持不变。相同的模式通常应用于存储管理的其他属性,但可能必须更改以进行持久性操作。
2firstnamelastname 属性是可能通过getter公开的普通不可变属性。
3age 属性是 birthday 属性中的不可变但派生的属性。在显示设计的情况下,数据库值将胜过默认值,因为Spring Data使用唯一声明的构造函数。即使意图是计算应该是首选,重要的是这个构造函数也将 age 作为参数(可能忽略它),否则属性填充步骤将尝试设置age字段并因为它是不可变的而失败。枯萎的存在。
4comment 属性是可变的,通过直接设置其字段来填充。
5remarks 属性是可变的,并通过直接设置 comment 字段或通过调用的setter方法来填充
6该类公开了一个工厂方法和一个用于创建对象的构造函数。这里的核心思想是使用工厂方法而不是其他构造函数来避免构造函数通过 @PersistenceConstructor 消除歧义。相反,在工厂方法中处理属性的默认值。
一般建议
Kotlin的支持

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

Kotlin对象创建

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

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

上面的类使用显式构造函数编译为典型的类。我们可以通过添加另一个构造函数来自定义此类,并使用 @PersistenceConstructor 注释它以指示构造函数首选项:

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中,默认情况下所有类都是不可变的,并且需要显式属性声明来定义可变属性。考虑以下 data class Person

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

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

4.4.2. 实体中支持的类型

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

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

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

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@WritingConverter 进行注释,以便控制它们仅适用于读取或写入数据库的适用性。

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

4.4.4. NamingStrategy

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

4.4.5. 自定义表名

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

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

    String name;
}

4.4.6. 自定义列名称

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

public class MyEntity {
    @Id
    Integer id;

    @Column("CUSTOM_COLUMN_NAME")
    String name;
}

@Column 注释也可用于引用类型(一对一关系)或集合,列表和映射(一对多关系)在所有这些类型上,注释的 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提供的用于检测实体是否为新的策略:

Id-属性检查(缺省值)默认情况下,Spring Data JDBC检查给定实体的identifier属性。如果identifier属性为 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来标识实体。必须使用Spring Data的 @Id 注释注释实体的ID。

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

一个重要的限制是,在保存实体后,该实体不能再是新的。请注意,实体是否是新实体是实体状态的一部分。随着自动增量列,这会自动发生,因为Spring将使用ID列中的值设置ID。如果您没有使用自动增量列,则可以使用 BeforeSave 侦听器,该侦听器设置实体的ID(本文档后面会介绍)。

4.5. 查询方法

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

4.5.1. 查询查询策略

JDBC模块仅支持将查询手动定义为 @Query 注释中的String。目前不支持从方法名称派生查询。

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

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

Spring Data JDBC仅支持命名参数。

自定义RowMapper

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

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

在确定要用于方法的 RowMapper 时,将根据方法的返回类型执行以下步骤:

相反,该查询应返回具有单个列的单个行,并且将返回类型的转换应用于该值。

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

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

修改查询

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

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

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

4.6. MyBatis集成

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

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

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

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

名称用途可能触发此语句的CrudRepository方法891790中可用的属性
insert插入单个实体。这也适用于聚合根引用的实体。savesaveAllgetInstance :要保存的实例 getDomainType :要保存的实体的类型。 get(<key>) :引用实体的ID,其中 <key>NamingStrategy 提供的后引用列的名称。
update更新单个实体。这也适用于聚合根引用的实体。savesaveAllgetInstance :要保存的实例 getDomainType :要保存的实体的类型。
delete删除单个实体。deletedeleteByIdgetId :要删除的实例的ID getDomainType :要删除的实体的类型。
deleteAll-<propertyPath>删除使用给定属性路径作为前缀的类型的任何聚合根引用的所有实体。请注意,用于为语句名称添加前缀的类型是聚合根的名称,而不是要删除的实体的名称。deleteAllgetDomainType :要删除的实体的类型。
deleteAll删除用作前缀deleteAll 的类型的所有聚合根。getDomainType :要删除的实体的类型。
delete-<propertyPath>删除具有给定propertyPathdeleteById 的聚合根引用的所有实体。getId :的ID要删除引用实体的聚合根。 getDomainType :要删除的实体的类型。
findById按IDfindById 选择聚合根。getId :要加载的实体的ID。 getDomainType :要加载的实体的类型。
findAll选择所有聚合根findAllgetDomainType :要加载的实体的类型。
findAllById按ID值findAllById 选择一组聚合根。getId :要加载的实体的ID值列表。 getDomainType :要加载的实体的类型。
findAllByProperty-<propertyName>选择另一个实体引用的一组实体。引用实体的类型用于前缀。引用的实体类型用作后缀。所有 find* 方法。getId :引用要加载的实体的实体的ID。 getDomainType :要加载的实体的类型。
count计算用作前缀的类型的聚合根数countgetDomainType :要计算的聚合根的类型。

4.7. 活动

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

下表描述了可用事件:

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

4.8. 记录

Spring Data JDBC几乎没有自己的日志记录。相反, JdbcTemplate 发布SQL语句的机制提供了日志记录。因此,如果要检查执行的SQL语句,请激活Spring的 NamedParameterJdbcTemplateMyBatis 的日志记录。

4.9. Transaction 性

默认情况下,存储库实例上的CRUD方法是事务性的。对于读取操作,事务配置 readOnly 标志设置为 true 。所有其他配置都使用普通的 @Transactional 注释,以便应用默认事务配置。有关详细信息,请参阅 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 标志。

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

示例56.使用Facade定义多个存储库调用的事务

@Service
class UserManagementImpl implements UserManagement {

  private final UserRepository userRepository;
  private final RoleRepository roleRepository;

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

  @Transactional
  public void addRoleToAllUsers(String roleName) {

    Role role = roleRepository.findByName(roleName);

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

前面的示例导致调用 addRoleToAllUsers(…) 在事务中运行(参与现有事务或创建新事务(如果没有已运行))。由于外部事务配置确定要使用的实际存储库,因此忽略了存储库的事务配置。请注意,您必须显式激活 <tx:annotation-driven /> 或使用 @EnableTransactionManagement 来获取外观工作的基于注释的配置。请注意,前面的示例假定您使用组件扫描。

4.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() 使用 @Modifying 注释并覆盖事务配置。因此,该方法将 readOnly 标志设置为 false

将事务用于只读查询是绝对合理的,我们可以通过设置 readOnly 标志来标记它们。但是,这不会检查您是否触发了操作查询(尽管某些数据库会拒绝只读事务中的 INSERTUPDATE 语句)。相反, readOnly 标志作为提示传播到底层JDBC性能优化的驱动程序。

4.10. 审计

4.10.1. 基础知识

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

基于注释的审计元数据

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

例子58.一个被审计的实体

class Customer {

  @CreatedBy
  private User user;

  @CreatedDate
  private DateTime createdDate;

  // … further properties omitted
}

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

基于接口的审计元数据

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

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

AuditorAware

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

以下示例显示了使用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 属性来选择要使用的实现。

附录A:常见问题解答

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

附录B:术语表

附录C:命名空间参考

<repositories />元素

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

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

附录D:Populators命名空间参考

<populator />元素

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

姓名说明
locations应在哪里找到要从存储库中读取对象的文件。

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

支持的查询关键字

Spring Data JDBC尚不支持查询派生。

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

支持的查询返回类型

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

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

返回类型说明
void表示没有返回值。
PrimitivesJava原语。
包装器类型Java包装器类型。
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 的便利扩展,直接将方法暴露给流,映射和过滤结果,连接它们等.
实现 Streamable 并采用 Streamable 构造函数或工厂方法参数的类型使用 Streamable 作为参数公开构造函数或 ….of(…) / ….valueOf(…) 工厂方法的类型。有关详细信息,请参阅 Returning Custom Streamable Wrapper Types
Vavr SeqListMapSetVavr集合类型。有关详细信息,请参阅 Support for Vavr Collections
Future<T>A Future 。期望使用 @Async 注释的方法,并且需要启用Spring的异步方法执行功能。
CompletableFuture<T>Java 8 CompletableFuture 。期望使用 @Async 注释的方法,并且需要启用Spring的异步方法执行功能。
ListenableFutureA org.springframework.util.concurrent.ListenableFuture 。期望使用 @Async 注释的方法,并且需要启用Spring的异步方法执行功能。
Slice一大块数据,表明是否有更多可用数据。需要 Pageable 方法参数。
Page<T>A Slice 包含其他信息,例如结果总数。需要 Pageable 方法参数。
GeoResult<T>包含附加信息的结果条目,例如到参考位置的距离。
GeoResults<T>包含附加信息的 GeoResult<T> 列表,例如到参考位置的平均距离。
GeoPage<T>带有 GeoResult<T>Page ,例如到参考位置的平均距离。
Mono<T>项目反应堆 Mono 使用被动存储库发出零个或一个元素。期望查询方法最多返回一个结果。如果未找到结果,则返回 Mono.empty() 。多个结果会触发 IncorrectResultSizeDataAccessException
Flux<T>项目反应堆 Flux 使用被动存储库发出零个,一个或多个元素。返回 Flux 的查询也可以发出无限数量的元素。
Single<T>使用被动存储库发出单个元素的RxJava Single 。期望查询方法最多返回一个结果。如果未找到结果,则返回 Mono.empty() 。多个结果会触发 IncorrectResultSizeDataAccessException
Maybe<T>一个RxJava Maybe 使用被动存储库发出零个或一个元素。期望查询方法最多返回一个结果。如果未找到结果,则返回 Mono.empty() 。多个结果会触发 IncorrectResultSizeDataAccessException
Flowable<T>一个RxJava Flowable 使用被动存储库发出零个,一个或多个元素。查询返回 Flowable 也可以发出无限数量的元素。