Spring Data JDBC(二)

Spring Data JDBC - Reference Documentation

https://docs.spring.io/spring-data/jdbc/docs/1.0.6.RELEASE/reference/html/

前言

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

项目元数据

  • 版本控制: http://github.com/spring-projects/spring-data-jdbc

  • 错误追踪系统: https://jira.spring.io/browse/DATAJDBC

  • 发布存储库: https://repo.spring.io/libs-release

  • 里程碑存储库: https://repo.spring.io/libs-milestone

  • 快照存储库: https://repo.spring.io/libs-snapshot

1. 新的和值得注意的

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

1.1. Spring Data JDBC 1.0中有什么新功能

  • Basic support for CrudRepository. 对CrudRepository的基本支持。

  • @Query support.@Query支持。

  • MyBatis support.MyBatis支持。

  • Id generation.Id代。

  • Event support. 事件支持。

  • Auditing.审计。

  • CustomConversions.自定义转换。

2. 依赖

由于各个Spring Data模块的创建日期不同,因此大多数模块都带有不同的主要版本号和次要版本号。找到兼容版本的最简单方法是依靠Spring Data Release Train BOM,它是我们为了处理版本兼容而定义的。在Maven项目中,您需要在POM的部分中声明此依赖项,如下所示:

Example 1. Using the Spring Data release train BOM


  
    
      org.springframework.data
      spring-data-releasetrain
      Lovelace-SR6
      import
      pom
    
  

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

  • BUILD-SNAPSHOT: 当前快照

  • M1M2, and so on: 里程碑

  • RC1RC2, and so on: 发布候选版

  • RELEASE: GA发布

  • SR1SR2, 等等:服务发布版

可以在我们的Spring Data示例存储库中找到使用BOM的工作示例。有了这个,您可以在块中声明要使用的Spring Data模块并且不需要指定具体版本,如下所示:

示例2.声明对Spring Data模块的依赖关系


  
    org.springframework.data
    spring-data-jpa
  

2.1. Spring Boot的依赖管理

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

2.2. Spring 框架

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

3. 使用Spring Data Repositories

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

 

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

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

3.1. 核心概念

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

Example 3. CrudRepository 接口

public interface CrudRepository extends Repository {

   S save(S entity);      

  Optional findById(ID primaryKey); 

  Iterable findAll();               

  long count();                        

  void delete(T entity);               

  boolean existsById(ID primaryKey);   

  // … more functionality omitted.
}
  保存给定的实体。
  返回由给定ID标识的实体。
  返回所有实体。
  返回实体数量。
  删除给定的实体。
  指示是否存在具有给定ID的实体。
 

我们还提供特定的持久性技术的抽象,例如JpaRepository或MongoRepository。除了相当通用的持久性技术无关的接口(如CrudRepository)之外,这些接口还扩展了CrudRepository并公开了底层持久性技术的功能。

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

Example 4. PagingAndSortingRepository 接口

public interface PagingAndSortingRepository extends CrudRepository {

  Iterable findAll(Sort sort);

  Page findAll(Pageable pageable);
}

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

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

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

Example 5. 派生计数查询

interface UserRepository extends CrudRepository {

  long countByLastname(String lastname);
}

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

Example 6. 派生删除查询

interface UserRepository extends CrudRepository {

  long deleteByLastname(String lastname);

  List removeByLastname(String lastname);
}

3.2. 查询方法

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

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

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

    interface PersonRepository extends Repository {
      List findByLastname(String lastname);
    }
  3. 设置Spring以使用JavaConfig或XML配置,为这些接口创建代理实例。

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

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

      
      
      
         
      
      

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

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

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

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

以下部分详细说明了每个步骤:

  • 定义存储库接口

  • 定义查询方法

  • 创建存储库实例

  • 自定义实现Spring Data Repositories

3.3. 定义存储卡接口

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

3.3.1. 微调存储库定义

通常,存储库接口扩展了Repository,CrudRepository或PagingAndSortingRepository。另外,如果您不想扩展Spring Data接口,还可以使用@RepositoryDe​​finition注释存储库接口。扩展CrudRepository公开了一整套操作实体的方法。如果您更愿意选择要公开的方法,请将要从CrudRepository公开的方法复制到域存储库中。

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

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

Example 7. 选择性暴露CRUD方法

@NoRepositoryBean
interface MyBaseRepository extends Repository {

  Optional findById(ID id);

   S save(S entity);
}

interface UserRepository extends MyBaseRepository {
  User findByEmailAddress(EmailAddress emailAddress);
}

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

  中间存储库接口使用@NoRepositoryBean注释。确保将该注释添加到那些在Spring Data运行时无需创建实例的存储库接口上。

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

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

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

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

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

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

interface MyRepository extends JpaRepository { }

@NoRepositoryBean
interface MyBaseRepository extends JpaRepository { … }

interface UserRepository extends MyBaseRepository { … }

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

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

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

interface AmbiguousRepository extends Repository { … }

@NoRepositoryBean
interface MyBaseRepository extends CrudRepository { … }

interface AmbiguousUserRepository extends MyBaseRepository { … }

AmbiguousRepository和AmbiguousUserRepository仅在其类型层次结构中扩展Repository和CrudRepository。虽然这在使用独特的Spring Data模块时非常好,但是多个模块无法区分这些存储库应该绑定到哪个特定的Spring Data。

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

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

interface PersonRepository extends Repository { … }

@Entity
class Person { … }

interface UserRepository extends Repository { … }

@Document
class User { … }

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

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

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

interface JpaPersonRepository extends Repository { … }

interface MongoDBPersonRepository extends Repository { … }

@Entity
@Document
class Person { … }

此示例显示了使用JPA和Spring Data MongoDB注释的域类。它定义了两个存储库,JpaPersonRepository和MongoDBPersonRepository。一个用于JPA,另一个用于MongoDB用法。Spring Data不再能够将存储库分开,从而导致未定义的行为。

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

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

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

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

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

3.4. Defining Query Methods

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

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

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

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

3.4.1. 查询查找策略

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

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

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

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

3.4.2. 创建查询

Spring Data存储库基础结构中内置的查询构建器机制对于构建存储库实体的约束查询非常有用。该机制从方法名称中剥离前缀find ... By,read ... By,query ... By,count ... By,and get ...并开始解析其余部分。introduction子句可以包含更多表达式,例如Distinct在要创建的查询上设置不同的标志。但是,第一个By用作分隔符以指示实际条件的开始。在最基本的层面上,您可以在实体属性上定义条件,并将它们与And和Or连接起来。以下示例显示了如何创建大量查询:

Example 13. 从方法名称创建查询

interface PersonRepository extends Repository {

  List findByEmailAddressAndLastname(EmailAddress emailAddress, String lastname);

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

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

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

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

  • 表达式通常是属性遍历与可以连接的运算符相结合。您可以将属性表达式与AND和OR组合在一起。对于属性表达式,您还可以获得诸如Between,LessThan,GreaterThan和Like之类的运算符。支持的运算符可能因数据存储而异,因此请参阅参考文档的相应部分。

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

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

3.4.3. 属性表达式

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

List findByAddressZipCode(ZipCode zipCode);

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

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

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

List findByAddress_ZipCode(ZipCode zipCode);

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

3.4.4. 特殊参数处理

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

Example 14. 在查询方法中使用Pageable,Slice和Sort

Page findByLastname(String lastname, Pageable pageable);

Slice findByLastname(String lastname, Pageable pageable);

List findByLastname(String lastname, Sort sort);

List findByLastname(String lastname, Pageable pageable);

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

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

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

3.4.5. 限制查询结果

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

Example 15. 使用Top和First限制查询的结果大小

User findFirstByOrderByLastnameAsc();

User findTopByOrderByAgeDesc();

Page queryFirst10ByLastname(String lastname, Pageable pageable);

Slice findTop3ByLastname(String lastname, Pageable pageable);

List findFirst10ByLastname(String lastname, Sort sort);

List findTop10ByLastname(String lastname, Pageable pageable);

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

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

  通过使用Sort参数将结果与动态排序结合,让您实现“K”个最小元素以及“K”个最大元素的查询方法。

3.4.6. 存储库方法返回集合或迭代类型

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

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

Streamable可以用作Iterable或任何集合类型的替代品。它提供了方便的方法来访问非并行Stream(从Iterable中丢失),直接... .filter(...)和... .map(...)在元素上的能力,并将Streamable连接到其他人:

Example 16. 使用Streamable组合查询方法结果

interface PersonRepository extends Repository {
  Streamable findByFirstnameContaining(String firstname);
  Streamable findByLastnameContaining(String lastname);
}

Streamable result = repository.findByFirstnameContaining("av")
  .and(repository.findByLastnameContaining("ea"));

返回自定义流式包装类型

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

  1. 该类型实现了Streamable。

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

示例用例如下所示:

class Product { 
  MonetaryAmount getPrice() { … }
}

@RequiredArgConstructor(staticName = "of")
class Products implements Streamable { 

  private Streamable streamable;

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

interface ProductRepository implements Repository {
  Products findAllByDescriptionContaining(String text); 
}
  公开API以访问产品价格的产品实体。
  Streamable 的包装类型,可以通过Products.of(...)构建(通过Lombok注释创建的工厂方法)。
  包装器类型在Streamable 上公开了额外的API计算新值。
 

该包装类型可以直接用作查询方法返回类型。无需返回Stremable 并手动将其包装在存储库客户端中。

支持Vavr集合

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

Vavr collection type Used Vavr implementation type Valid Java source types

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(Vavr,Iterable等价物),并从实际返回值派生实现类,即java.util.List将变为Vavr List / Seq,java.util.Set变为VavrLinkedHashSet / Set等。

3.4.7. 存储库方法的NULL处理

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

  • com.google.common.base.Optional

  • scala.Option

  • io.vavr.control.Option

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

可空性注释

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

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

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

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

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

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

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

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

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

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

package com.acme;                                                       

import org.springframework.lang.Nullable;

interface UserRepository extends Repository {

  User getByEmailAddress(EmailAddress emailAddress);                    

  @Nullable
  User findByEmailAddress(@Nullable EmailAddress emailAdress);          

  Optional findOptionalByEmailAddress(EmailAddress emailAddress); 
}
  存储库驻留在我们已定义非空行为的包(或子包)中。
  当执行的查询未产生结果时,抛出EmptyResultDataAccessException。当传递给方法的emailAddress为null时,抛出IllegalArgumentException。
  当执行的查询未产生结果时返回null。同时接受null作为emailAddress的值。
  当执行的查询未产生结果时,返回Optional.empty()。当传递给方法的emailAddress为null时,抛出IllegalArgumentException。

基于Kotlin的存储库中的可空性

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

Example 19. 对Kotlin存储库使用可空性约束

interface UserRepository : Repository {

  fun findByUsername(username: String): User     

  fun findByFirstname(firstname: String?): User? 
}
 

该方法将参数和结果都定义为非可空(Kotlin默认值)。Kotlin编译器拒绝将null传递给方法的方法调用。如果查询执行产生空结果,则抛出EmptyResultDataAccessException。

  此方法对firstname参数接受null,如果查询执行不生成结果,则返回null。

3.4.8. 流式查询结果

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

Example 20. 使用Java 8 Stream 流式传输查询结果

@Query("select u from User u")
Stream findAllByCustomQueryAndStream();

Stream readAllByFirstnameNotNull();

@Query("select u from User u")
Stream streamAllPaged(Pageable pageable);
  Stream可能会包装基础数据存储特定的资源,因此必须在使用后关闭。您可以使用close()方法或使用Java 7 try-with-resources块手动关闭Stream,如以下示例所示:

Example 21. 使用Stream 会导致try-with-resources块

try (Stream stream = repository.findAllByCustomQueryAndStream()) {
  stream.forEach(…);
}
  并非所有Spring Data模块当前都支持Stream 作为返回类型。

3.4.9. 异步查询结果

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

@Async
Future findByFirstname(String firstname);               

@Async
CompletableFuture findOneByFirstname(String firstname); 

@Async
ListenableFuture findOneByLastname(String lastname);    
  Use java.util.concurrent.Future 作为返回类型。
  Use a Java 8 java.util.concurrent.CompletableFuture 作为返回类型。
  Use a org.springframework.util.concurrent.ListenableFuture 作为返回类型。

3.5. 创建存储库实例

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

3.5.1. XML配置

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

Example 22. 通过XML启用Spring Data存储库




  

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

使用过滤器

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

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

Example 23. 使用exclude-filter元素


  

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

3.5.2. Java配置

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

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

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

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

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

3.5.3. 独立使用

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

Example 25. 存储库工厂的独立使用

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

3.6. Spring Data 存储库的自定义实现

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

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

3.6.1. 自定义单个存储库

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

Example 26. 自定义存储库功能的接口

interface CustomizedUserRepository {
  void someCustomMethod(User user);
}

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

Example 27. 自定义存储库功能的实现

class CustomizedUserRepositoryImpl implements CustomizedUserRepository {

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

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

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

Example 28. 对存储库界面的更改

interface UserRepository extends CrudRepository, CustomizedUserRepository {

  // Declare query methods here
}

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

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

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

Example 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的自定义存储库的接口:

Example 30. 对存储库界面的更改

interface UserRepository extends CrudRepository, HumanRepository, ContactRepository {

  // Declare query methods here
}

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

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

Example 31. 碎片压倒一切 save(…)

interface CustomizedSave {
   S save(S entity);
}

class CustomizedSaveImpl implements CustomizedSave {

  public  S save(S entity) {
    // Your custom implementation
  }
}

以下示例显示了使用前面的存储库片段的存储库:

Example 32. 自定义存储库接口

interface UserRepository extends CrudRepository, CustomizedSave {
}

interface PersonRepository extends CrudRepository, CustomizedSave {
}

配置

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

Example 33. 配置示例



前面示例中的第一个配置尝试查找名为com.acme.repository.CustomizedUserRepositoryImpl的类,以充当自定义存储库实现。第二个示例尝试查找com.acme.repository.CustomizedUserRepositoryMyPostfix。

解决歧义

如果在不同的包中找到具有匹配类名的多个实现,则Spring Data使用bean名称来标识要使用的名称。

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

Example 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并根据前一节中描述的约定命名它。然后,基础结构按名称引用手动定义的bean定义,而不是自己创建一个。以下示例显示如何手动连接自定义实现:

Example 35. 自定义实现的手动连接




  

3.6.2. 自定义Base Repository

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

Example 36. 自定义存储库基类

class MyRepositoryImpl
  extends SimpleJpaRepository {

  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 save(S entity) {
    // implementation goes here
  }
}
 

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

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

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

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

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

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

3.7. 从聚合根发布事件

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

Example 39. 从聚合根公开事件域事件

class AnAggregateRoot {

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

    @AfterDomainEventPublication 
    void callbackMethod() {
       // … potentially clean up domain events list
    }
} 
  
  使用@DomainEvents的方法可以返回单个事件实例或事件集合。它不能带任何参数。
 

在所有事件发布后,我们有一个使用@AfterDomainEventPublication注释的方法。它可用于潜在地清除要发布的事件列表(以及其他用途)。

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

3.8. Spring Data 扩展

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

3.8.1. Querydsl扩展

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

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

Example 40. QuerydslPredicateExecutor接口

public interface QuerydslPredicateExecutor {

  Optional findById(Predicate predicate);  

  Iterable findAll(Predicate predicate);   

  long count(Predicate predicate);            

  boolean exists(Predicate predicate);        

  // … more functionality omitted.
}
 

查找并返回与Predicate匹配的单个实体。

  查找并返回与Predicate匹配的所有实体。
  返回与Predicate匹配的实体数。
  返回是否存在与Predicate匹配的实体。

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

Example 41. Querydsl集成在存储库中

interface UserRepository extends CrudRepository, QuerydslPredicateExecutor {
}

前面的示例允许您使用Querydsl Predicate实例编写类型安全查询,如以下示例所示:

Predicate predicate = user.firstname.equalsIgnoreCase("dave")
	.and(user.lastname.startsWithIgnoreCase("mathews"));

userRepository.findAll(predicate);

3.8.2. Web 支持

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

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

Example 42. 启用Spring Data Web支持

@Configuration
@EnableWebMvc
@EnableSpringDataWebSupport
class WebConfiguration {}

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

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

Example 43. 在XML中启用Spring Data Web支持




基本Web支持

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

  • 一个DomainClassConverter让Spring MVC从请求参数或路径变量中解析存储库管理的域类的实例。

  • HandlerMethodArgumentResolver实现让Spring MVC从请求参数中解析Pageable和Sort实例。

DomainClassConverter

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

Example 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 and Sort

上一节中显示的配置代码段还注册了PageableHandlerMethodArgumentResolver以及SortHandlerMethodArgumentResolver的实例。注册启用Pageable和Sort作为有效的控制器方法参数,如以下示例所示:示例45.

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

Table 1. 为Pageable实例评估的请求参数页您要检索的页面

page

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

size

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

sort

排序格式property,property(,ASC | DESC)中排序的属性。默认排序方向是升序。如果要切换方向,请使用多个排序参数 - 例如,?sort = firstname&sort = lastname,asc。

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

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

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

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

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

你必须填充thing1_page和thing2_page等。

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

对Pageables的超媒体支持

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

Example 46. 使用PagedResourcesAssembler作为控制器方法参数

@Controller
class PersonController {

  @Autowired PersonRepository repository;

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

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

如上例所示启用配置,可以将PagedResourcesAssembler用作控制器方法参数。在其上调用资源(...)具有以下效果:

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

  • PagedResources对象获取一个附加的PageMetadata实例,并使用来自Page和底层PageRequest的信息填充它。

  • 根据页面的状态,PagedResources可能会显示并附加上一个或下一个链接。链接指向方法映射到的URI。添加到方法的分页参数与PageableHandlerMethodArgumentResolver的设置相匹配,以确保稍后可以解析链接。

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

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

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

Web数据绑定支持

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

Example 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。前面的方法声明将尝试在给定文档中的任何位置查找firstname。lastname XML查找在传入文档的顶级执行。其中JSON变体首先尝试顶级lastname,但如果前者未返回值,则还尝试嵌套在用户子文档中的lastname。这样,可以轻松地减轻源文档结构的变化,而无需客户端调用公开的方法(通常是基于类的有效负载绑定的缺点)。

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

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

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

Querydsl Web支持

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

请考虑以下查询字符串:

?firstname=Dave&lastname=Matthews

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

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

将@QuerydslPredicate添加到方法签名提供了一个可立即使用的谓词,可以使用QuerydslPredicateExecutor运行。

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

以下示例显示如何在方法签名中使用@QuerydslPredicate:

@Controller
class UserController {

  @Autowired UserRepository repository;

  @RequestMapping(value = "/", method = RequestMethod.GET)
  String index(Model model, @QuerydslPredicate(root = User.class) Predicate predicate,    
          Pageable pageable, @RequestParam MultiValueMap parameters) {

    model.addAttribute("users", repository.findAll(predicate, pageable));

    return "index";
  }
}
  将查询字符串参数解析为匹配Predicate for User。

默认绑定如下:

  • 简单属性上的对象为eq。

  • 集合上的对象,如包含的属性。

  • 关于简单属性的集合,如 in。

可以通过@QuerydslPredicate的bindings属性或通过使用Java 8默认方法并将QuerydslBinderCustomizer方法添加到存储库接口来自定义这些绑定。

interface UserRepository extends CrudRepository,
                                 QuerydslPredicateExecutor,                
                                 QuerydslBinderCustomizer {               

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

    bindings.bind(user.username).first((path, value) -> path.contains(value))    
    bindings.bind(String.class)
      .first((StringPath path, String value) -> path.containsIgnoreCase(value)); 
    bindings.excluding(user.password);                                           
  }
}
  QuerydslPredicateExecutor提供对Predicate的特定查找器方法的访问。
  存储库接口上定义的QuerydslBinderCustomizer会自动获取并快捷方式@QuerydslPredicate(bindings = ...)。
  将username属性的绑定定义为简单包含绑定。
  将String属性的默认绑定定义为不区分大小写的包含匹配项。
 

从谓词解析中排除密码属性。

3.8.3. 存储库填充程序

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

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

Example 48. 在JSON中定义的数据

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

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

Example 49. 声明Jackson存储库填充程序




  

前面的声明导致由Jackson ObjectMapper读取和反序列化data.json文件。通过检查JSON文档的_class属性来确定解组JSON对象的类型。基础结构最终选择适当的存储库来处理反序列化的对象。

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

Example 50. 声明一个解组存储库populator(使用JAXB)




  

  

参考文档

4. JDBC 存储库

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

4.1. 为什么选择Spring Data JDBC?

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

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

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

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

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

  • 如果您保存实体,它将被保存。如果你不这样做,那就没有。没有脏跟踪和没有会话。

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

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

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

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

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

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

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

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

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

4.3. 基于注释的配置

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

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

@Configuration
@EnableJdbcRepositories
class ApplicationConfig {

  @Bean
  public DataSource dataSource() {

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

}

上例中的配置类使用spring-jdbc的EmbeddedDatabaseBuilder API设置嵌入式HSQL数据库。我们使用@EnableJdbcRepositories激活Spring Data JDBC存储库。如果未配置基本软件包,则使用配置类所在的软件包。

4.4. 持久化实体

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

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

 

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

4.4.1. 对象映射基础

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

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

  1. 通过使用其中一个公开的构造函数创建实例。

  2. 实例填充实现所有暴露的属性。

对象创建

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

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

  2. 如果有一单个构造函数接受参数,它将被使用。

  3. 如果有多个构造函数使用参数,则Spring Data要使用的构造函数必须使用@PersistenceConstructor进行注释。

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

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

对象内部创建

为了避免反射的开销,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就会填充该类的所有剩余持久属性。除非已经由实体的构造函数填充(即通过其构造函数参数列表使用),否则将首先填充identifier属性以允许循环对象引用的解析。之后,在实体实例上设置尚未由构造函数填充的所有非瞬态属性。为此,我们使用以下算法:

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

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

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

属性内部填充

与我们在对象构造中的优化类似,我们还使用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;
  }
}

Example 52. 生成的属性访问器

class PersonPropertyAccessor implements PersistentPropertyAccessor {

  private static final MethodHandle firstname;              

  private Person person;                                    

  public void setProperty(PersistentProperty property, Object value) {

    String name = property.getName();

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

使用property-access允许直接方法调用而不使用MethodHandles。

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

  • 类型不能驻留在默认值或java包下。

  • 类型及其构造函数必须是公共的。

  • 内部类的类型必须是静态的。

  • 使用的Java Runtime必须允许在原始ClassLoader中声明类。Java 9和更新版本施加了某些限制。

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

我们来看看以下实体:

Example 53.一个样本实体

class Person {

  private final @Id Long id;                                                
  private final String firstname, lastname;                                 
  private final LocalDate birthday;
  private final int age; 

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

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

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

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

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

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

  void setRemarks(String remarks) {                                         
    this.remarks = remarks;
  }
}
  identifier属性是final,但在构造函数中设置为null。该类公开了一个withId(...)方法,该方法用于设置标识符,例如将实例插入数据存储区并生成标识符时。原始Person实例在创建新实例时保持不变。相同的模式通常应用于存储管理的其他属性,但可能必须更改以进行持久性操作。
  firstname和lastname属性是可能通过getter公开的普通不可变属性。age属性是一个不可变的,但是来自生日财产。如图所示,当Spring Data使用唯一声明的构造函数时,数据库值将胜过默认值。即使意图是计算
  应该是首选,重要的是这个构造函数也将age作为参数(可能忽略它),否则属性填充步骤将尝试设置age字段并因为它是不可变的而失败并且不存在枯萎。
  注释属性是可变的,通过直接设置其字段来填充。
  通过直接设置注释字段或通过调用setter方法,remarks属性是可变的和填充的
  该类公开了一个工厂方法和一个用于创建对象的构造函数。这里的核心思想是使用工厂方法而不是其他构造函数来避免通过@PersistenceConstructor进行构造函数消歧。相反,在工厂方法中处理属性的默认值。

一般建议

  • 尝试维持不可变对象 - 不可变对象很容易创建,因为实现对象只是调用它的构造函数。此外,这可以避免使用允许客户端代码操纵对象状态的setter方法来填充域对象。如果您需要这些,请更喜欢使它们受到保护,以便只能通过有限数量的共存类型来调用它们。仅构造函数的实现比属性总体快30%。

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

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

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

  • 对于要生成的标识符,仍然使用最终字段和枯萎方法— 

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

Kotlin 支持

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

Kotlin 对象创建

支持实例化Kotlin类,默认情况下所有类都是不可变的,并且需要显式属性声明来定义可变属性。考虑以下数据类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时,名称默认为unknown。

Kotlin数据类的属性数量

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

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

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

4.4.2. 您实体中支持的类型

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

  • 所有原始类型及其盒装类型 (intfloatIntegerFloat, and so on)

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

  • String

  • java.util.Datejava.time.LocalDatejava.time.LocalDateTime, and java.time.LocalTime

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

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

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

  • Map 被认为是合格的一对多关系。引用实体的表应该有两个附加列:一个名称与外键的引用实体的表相同,另一个名称相同,并且映射键的附加_key后缀。您可以分别通过实现NamingStrategy.getReverseColumnName(RelationalPersistentProperty属性)和NamingStrategy.getKeyColumn(RelationalPersistentProperty属性)来更改此行为。或者,您可以使用@Column注释属性(value =“your_column_name”,keyColumn =“your_key_column_name”)

  • List 被映射为Map

引用实体的处理是有限的。这是基于如上所述的聚合根的想法。如果您引用另一个实体,则根据定义,该实体是您的聚合的一部分。因此,如果删除引用,则先前引用的实体将被删除。这也意味着参考是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 {

        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批注自定义名称。此批注的元素值提供自定义表名。以下示例将MyEntity类映射到数据库中的CUSTOM_TABLE_NAME表:

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

    String name;
}

4.4.6. 自定义列名

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

public class MyEntity {
    @Id
    Integer id;

    @Column("CUSTOM_COLUMN_NAME")
    String name;
}

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

public class MyEntity {
    @Id
    Integer id;

    @Column("CUSTOM_COLUMN_NAME")
    Set name;
}

public class MySubEntity {
    String name;
}

使用列表和映射时,您必须有一个额外的列,用于列表中数据集的位置或映射中实体的键值。可以使用@Column注释的keyColumn元素自定义此附加列名:

public class MyEntity {
    @Id
    Integer id;

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

public class MySubEntity {
    String name;
}

4.4.7. 实体状态检测策略

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

Table 2. 用于检测实体是否是Spring Data JDBC中的新实例的选项
Id-Property检查(默认)

默认情况下,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注解中手动定义查询。目前不支持从方法名称派生查询。

4.5.2. 使用@Query

以下示例显示如何使用@Query声明查询方法:

Example 54.使用@Query声明查询方法

public interface UserRepository extends CrudRepository {

  @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

You can configure which RowMapper to use, either by using the @Query(rowMapperClass = …​.) or by registering a RowMapperMapbean and registering a RowMapper per method return type. The following example shows how to register RowMappers:

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

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

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

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

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

  2. 对RowMapperMap中的实体类进行迭代,直到找到一个有问题的返回类型的超类或接口。使用为该类注册的RowMapper。

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

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

修改查询

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

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

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

  • void

  • int (更新记录数)

  • boolean(记录是否已更新)

4.6. MyBatis 集成

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

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

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

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

Name Purpose CrudRepository methods that might trigger this statement Attributes available in the MyBatisContext

insert

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

savesaveAll.

getInstance:要保存的实例getDomainType:要保存的实体的类型。get():引用实体的ID,其中是NamingStrategy提供的后引用列的名称。

update

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

savesaveAll.

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

delete

删除单个实体。

deletedeleteById.

getId:要删除的实例的IDgetDomainType:要删除的实体的类型。

deleteAll-

删除由用作给定属性路径的前缀的类型的任何聚合根引用的所有实体。请注意,用于为语句名称添加前缀的类型是聚合根的名称,而不是要删除的实体的名称。

deleteAll.

getDomainType:要删除的实体的类型。

deleteAll

删除用作前缀的类型的所有聚合根

deleteAll.

getDomainType:要删除的实体的类型。

delete-

使用给定的propertyPath删除聚合根引用的所有实体

deleteById.

getId:要删除引用实体的聚合根的ID。getDomainType:要删除的实体的类型。

findById

按ID选择聚合根

findById.

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

findAll

选择所有聚合根

findAll.

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

findAllById

按ID值选择一组聚合根

findAllById.

getId:要加载的实体的ID值列表。getDomainType:要加载的实体的类型。

findAllByProperty-

选择另一个实体引用的一组实体。引用实体的类型用于前缀。引用的实体类型用作后缀。

All find* methods.

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

count

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

count

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

4.7. Events

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

@Bean
public ApplicationListener timeStampingSaveTime() {

	return event -> {

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

下表描述了可用事件:

Table 3. 可用事件
Event When It Is Published

BeforeDeleteEvent

在聚合根被删除之前。

AfterDeleteEvent

在聚合根被删除之后。

BeforeSaveEvent

在保存聚合根之前(即插入或更新,但在决定是否更新或删除之后)。该事件引用了AggregateChange实例。可以通过添加或删除DbAction实例来修改实例。

AfterSaveEvent

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

AfterLoadEvent

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

4.8. 日志

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

4.9. 事务性

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

Example 55. CRUD的自定义事务配置

public interface UserRepository extends CrudRepository {

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

  // Further query method declarations
}

前面的内容导致findAll()方法执行时超时为10秒且没有readOnly标志。

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

Example 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(...)在事务内部运行(参与现有事务或创建新事务(如果没有已运行))。由于外部事务配置确定要使用的实际存储库,因此忽略了存储库的事务配置。请注意,您必须显式激活或使用@EnableTransactionManagement来获取外观工作的基于注解的配置。请注意,前面的示例假定您使用组件扫描。

4.9.1. 事务查询方法

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

Example 57.在查询方法中使用@Transactional

@Transactional(readOnly = true)
public interface UserRepository extends CrudRepository {

  List findByLastname(String lastname);

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

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

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

4.10. 审计

4.10.1. 基本

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

Annotation-based Auditing Metadata

基于注解的审计元数据

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

Example 58. 一个被审计的实体

class Customer {

  @CreatedBy
  private User user;

  @CreatedDate
  private DateTime createdDate;

  // … further properties omitted
}

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

基于接口的审计元数据

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

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

AuditorAware

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

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

Example 59.基于Spring Security的AuditorAware的实现

class SpringSecurityAuditorAware implements AuditorAware {

  public Optional getCurrentAuditor() {

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

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

4.11. JDBC 审计

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

Example 60.使用Java配置激活审计

@Configuration
@EnableJdbcAuditing
class Config {

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

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

附录

附录A: 经常问的问题

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

附录B: 词汇表

AOP

面向方面编程

CRUD

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

Dependency Injection

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

JPA

Java 持久化 API

Spring

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

附录C: 命名空间参考

 元素

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

Table 4. 属性
Name Description

base-package

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

repository-impl-postfix

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

query-lookup-strategy

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

named-queries-location

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

consider-nested-repositories

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

附录D: Populators命名空间引用

元素

元素允许通过Spring Data存储库基础结构填充数据存储.[1]

Table 5. 属性
Name Description

locations

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

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

支持的查询关键字

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

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

支持的查询返回类型

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

  地理空间类型(例如GeoResult,GeoResults和GeoPage)仅适用于支持地理空间查询的数据存储。
Table 6. 查询返回类型
Return type Description

void

表示没有返回值。

Primitives

Java原语。

Wrapper types

Java包装器类型。

T

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

Iterator

迭代器

Collection

集合

List

列表

Optional

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

Option

Scala或Vavr Option类型。语义上与前面描述的Java 8的Optional相同。

Stream

Java 8 流对象。

Streamable

Iterable的便利扩展,直接暴露方法流,映射和过滤结果,连接它们等。

Types that implement Streamable and take a Streamable constructor or factory method argument

暴露构造函数的类型或以.Streamable为参数的... .of(...)/....valueOf(...)工厂方法。有关详细信息,请参阅返回自定义可流式包装类型。

Vavr SeqListMapSet

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

Future

 Future。期望使用@Async注释的方法,并且需要启用Spring的异步方法执行功能。

CompletableFuture

Java 8 CompletableFuture。期望使用@Async注释的方法,并且需要启用Spring的异步方法执行功能。

ListenableFuture

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

Slice

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

Page

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

GeoResult

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

GeoResults

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

GeoPage

具有GeoResult 的页面,例如到参考位置的平均距离。

Mono

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

Flux

使用反应性存储库反射零个,一个或多个元素的Project Reactor Flux。返回Flux的查询也会发出无数个元素。

Single

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

Maybe

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

Flowable

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

 

你可能感兴趣的:(Spring,Data,Spring,Data,JDBC)