3.使用Spring数据仓库


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




Spring数据存储库文档和你的模块





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




3.1。核心概念


Spring数据存储库抽象中的中心接口Repository(可能并不令人吃惊)。它需要管理域类以及域类的id类型作为类型参数。这个接口主要作为一个标记接口来捕获类型的工作,并帮助你发现扩展这个接口的接口。该CrudRepository规定对于正在管理的实体类复杂的CRUD功能。


例子3. CrudRepository接口



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

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

  Optional<T> findById(ID primaryKey); 

  Iterable<T> findAll();               

  long count();                        

  void delete(T entity);               

  boolean existsById(ID primaryKey);   

  // … more functionality omitted.
}




保存给定的实体。


返回由给定ID标识的实体。


返回所有实体。


返回实体的数量。


删除给定的实体。


指示是否存在具有给定标识的实体。



我们还提供持久性技术特定的抽象,如eg JpaRepositoryMongoRepository。这些接口扩展CrudRepository和揭示了基础持久化技术的功能,以及相当通用的持久化技术无关接口,如CrudRepository。


最重要的是CrudRepository有一个PagingAndSortingRepository抽象,增加了额外的方法来简化分页访问实体:


例子4. PagingAndSortingRepository



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

  Iterable<T> findAll(Sort sort);

  Page<T> findAll(Pageable pageable);
}


访问User页面大小为20 的第二页,你可以简单地做这样的事情:


PagingAndSortingRepository<User, Long> repository = // … get access to a bean
Page<User> users = repository.findAll(new PageRequest(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,声明这些查询变成了一个四步骤的过程:



  1. 声明扩展Repository或其子接口之一的接口,并将其输入到它将处理的域类和ID类型。
interface PersonRepository extends Repository<Person, Long> { … }
  1. 在接口上声明查询方法。
interface PersonRepository extends Repository<Person, Long> {
  List<Person> findByLastname(String lastname);
}
  1. 设置Spring为这些接口创建代理实例。通过JavaConfig
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;

@EnableJpaRepositories
class Config {}

或通过XML配置

<?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命名空间。如果您使用的是存储库抽象任何其他商店,你需要更改为您储存模块,它应该被交换的适当的命名空间声明jpa赞成的,例如,mongodb

另请注意,JavaConfig变体不会明确地配置包,因为缺省情况下使用了注释类的包。要自定义要扫描的软件包,请使用basePackage…数据存储库特定存储库@Enable…-annotation的某个属性。

  1. 获取存储库实例注入并使用它。
class SomeClient {

  private final PersonRepository repository;

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

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



下面的部分将详细解释每一步。



3.3。定义存储库接口


作为第一步,您需要定义一个域类特定的存储库接口。该接口必须扩展存储库并键入到域类和ID类型。如果您想要公开该域类型的CRUD方法,请扩展CrudRepository而不是Repository


3.3.1。微调存储库定义


通常,您的存储库接口将扩展RepositoryCrudRepositoryPagingAndSortingRepository。另外,如果你不想扩展Spring Data接口,你也可以用你的库接口注释@RepositoryDefinition。扩展CrudRepository公开了一套完整的方法来操纵你的实体。如果您希望对所公开的方法进行选择,只需将要公开的方法复制CrudRepository到您的域存储库中即可。



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



示例7.选择性地暴露CRUD方法



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

  Optional<T> findById(ID id);

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

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


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



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



3.3.2。存储库方法的空处理


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

com.google.common.base.Optionalscala.Optionio.vavr.control.Optionjavaslang.control.Option

  •  (不推荐使用Javaslang)


或者查询方法可以选择不使用包装类型。查询结果的缺失将通过返回来指示null。存储库方法返回的集合,集合备选方案,包装和流保证永远不会返回null,而是相应的空表示。有关详细信息,请参阅Repository查询返回类型


可空性注释


您可以使用Spring框架的可空性注释来表示存储库方法的可空约束。他们提供了一个工具友好的方法,并null在运行时选择加入检查:


  • @NonNullApi- 在包级别使用来声明参数和返回值的默认行为是不接受或产生null值。
  • @NonNull- 在参数或返回值上使用,不能是null (不需要参数和返回值,如果@NonNullApi适用)。
  • @Nullable- 用于可以是参数或返回值null


Spring注释使用JSR 305注释进行元注释(一种休眠但广泛传播的JSR)。JSR 305元注释允许IDEAEclipseKotlin等工具供应商以通用方式提供空安全支持,而无需对Spring注释进行硬编码支持。要启用的查询方法空约束运行时检查,你需要使用Spring的激活在封装级非空性@NonNullApipackage-info.java

例子8.声明不可为空 

package-info.java


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


一旦非空默认就绪,存储库查询方法调用将在运行时验证可空性约束。如果查询执行结果违反了定义的约束,例如,null由于某种原因,该方法将返回,但被声明为非空(存储库所在包中定义的注释的默认值),将抛出异常。如果你想再次选择可空的结果,有选择地使用@Nullable该方法。使用上述的结果包装类型将继续按预期工作,即空的结果将被转换为表示不存在的值。


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



package com.acme;                                                       

import org.springframework.lang.Nullable;

interface UserRepository extends Repository<User, Long> {

  User getByEmailAddress(EmailAddress emailAddress);                    

  @Nullable
  User findByEmailAddress(@Nullable EmailAddress emailAdress);          

  Optional<User> findOptionalByEmailAddress(EmailAddress emailAddress); 
}




存储库驻留在我们为其定义非空行为的包(或子包)中(请参阅上文)。


将会抛出一个EmptyResultDataAccessExceptioncase来执行查询不会产生结果。将抛出IllegalArgumentException的情况下,emailAddress交给该方法null


null如果执行的查询不会产生结果,将会返回。也接受null作为价值emailAddress


Optional.empty()如果执行的查询不会产生结果,将会返回。将抛出IllegalArgumentException的情况下,emailAddress交给该方法null



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


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


示例10.在Kotlin存储库上使用可空性约束



interface UserRepository : Repository<User, String> {

  fun findByUsername(username: String): User     

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




该方法将两个参数都定义为非空(Kotlin默认值)以及结果。Kotlin编译器已经拒绝方法调用,试图把null这个方法放到手中。如果查询执行产生空的结果,EmptyResultDataAccessException则会抛出一个。


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



3.3.3。将存储库与多个Spring Data模块一起使用



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



  1. 如果存储库定义扩展了特定于模块的存储库,那么它是特定的Spring Data模块的有效候选者。
  2. 如果域类使用特定于模块的类型注释进行注释,则它是特定的Spring Data模块的有效候选者。Spring Data模块接受第三方注释(比如JPA的@Entity)或者提供自己的注解,例如@DocumentSpring Data MongoDB / Spring Data Elasticsearch。



示例11.使用模块特定接口的存储库定义



interface MyRepository extends JpaRepository<User, Long> { }

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

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


MyRepository并在其类型层次中进行UserRepository扩展JpaRepository。他们是Spring Data JPA模块的有效候选者。


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



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

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

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


AmbiguousRepositoryAmbiguousUserRepository只扩展RepositoryCrudRepository在他们的类型层次结构。虽然使用独特的Spring Data模块这非常好,但多个模块无法区分这些存储库应绑定到哪个特定的Spring Data。


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



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使用UserSpring Data MongoDB的@Document注解进行注释。


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



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

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

@Entity
@Document
class Person {
  …
}


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


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



区分存储库的最后一种方法是确定存储库基础包。基本包定义了扫描存储库接口定义的起点,这意味着存储库定义位于相应的包中。默认情况下,注释驱动的配置使用配置类的包。基于XML的配置中基础包是强制性的。



示例15.基本包的注释驱动配置



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



3.4。定义查询方法



存储库代理有两种方法可以从方法名称派生特定于存储的查询。它可以直接从方法名称派生查询,也可以使用手动定义的查询。可用的选项取决于实际的商店。但是,必须有一个策略来决定创建什么样的实际查询。让我们来看看可用的选项。



3.4.1。查询查询策略


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

CREATE

  • 尝试从查询方法名称构造特定于商店的查询。一般的方法是从方法名称中移除一组已知的前缀,然后解析方法的其余部分。在查询创建中详细了解查询构建。

USE_DECLARED_QUERY

  • 试图找到一个声明的查询,并会抛出一个异常,以防万一找不到。查询可以通过某个注释来定义,也可以通过其他方式声明。查阅特定商店的文档以查找该商店的可用选项。如果存储库基础结构在引导时未找到该方法的已声明查询,则会失败。

CREATE_IF_NOT_FOUND

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



3.4.2。查询创建


Spring数据存储库基础架构中内置的查询构建器机制对构建存储库实体的约束查询非常有用。该机制条前缀find…Byread…Byquery…Bycount…By,和get…By从所述方法和开始分析它的其余部分。引入子句可以包含进一步的表达式,例如Distinct在要创建的查询上设置不同的标志。但是,第一个By作为分隔符来指示实际标准的开始。在一个非常基本的层次上,您可以定义实体属性的条件,并将它们与And和连接起来Or


示例16.从方法名称创建查询



interface PersonRepository extends Repository<User, Long> {

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

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

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

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



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



  • 表达式通常是属性遍历和可以连接的运算符的组合。您可以使用组合属性表达式ANDOR。您还可以得到这样的运营商为支撑BetweenLessThanGreaterThanLike为属性表达式。受支持的操作符可能因数据存储而异,因此请参阅参考文档的相应部分。
  • 方法解析器支持IgnoreCase为单个属性(例如,findByLastnameIgnoreCase(…))或支持忽略大小写的类型(String例如,通常为实例)的所有属性设置标志findByLastnameAndFirstnameAllIgnoreCase(…)。是否支持忽略情况可能因商店而异,因此请参阅参考文档中的相关部分以获取特定于商店的查询方法。
  • 您可以通过OrderBy向引用属性的查询方法附加子句并提供排序方向(AscDesc)来应用静态排序。要创建支持动态排序的查询方法,请参阅特殊参数处理



3.4.3。属性表达式


属性表达式只能引用被管实体的直接属性,如上例所示。在查询创建时,您已经确保解析的属性是托管域类的一个属性。但是,您也可以通过遍历嵌套属性来定义约束。假设Person有一个AddressZipCode。在这种情况下,一个方法名称


List<Person> findByAddressZipCode(ZipCode zipCode);


创建属性遍历x.address.zipCode。解析算法首先将整个part(AddressZipCode)作为属性进行解释,然后检查具有该名称属性的域类(未包含大小)。如果算法成功,则使用该属性。如果不是,则算法拆分了从右侧的驼峰部分的信号源到头部和尾部,并试图找出相应的属性,在我们的例子,AddressZipCode。如果算法找到一个头部的属性,它将采取尾巴,并继续从那里建立树,按照刚刚描述的方式分割尾部。如果第一个分割不匹配,则算法将分割点移到左侧(AddressZipCode)并继续。

尽管这在大多数情况下都适用,但算法可能会选择错误的属性。假设这个Person类也有一个addressZip属性。该算法将在第一轮拆分中相匹配,并基本上选择错误的属性,最后失败(因为addressZip可能没有code属性的类型)。

为了解决这个不明确的问题,你可以_在方法名中使用手动定义遍历点。所以我们的方法名称会像这样结束:


List<Person> findByAddress_ZipCode(ZipCode zipCode);



当我们把下划线看作一个保留字符时,我们强烈建议遵循标准的Java命名约定(即,不要在属性名称中使用下划线,而应该使用驼峰大小写)。



3.4.4。特殊参数处理


要处理查询中的参数,只需定义上述示例中已经看到的方法参数即可。除此之外,基础设施将识别某些特定类型PageableSort并动态地对查询应用分页和排序。


示例17.使用Pageable,Slice和Sort in查询方法



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实例传递给查询方法,以动态地将分页添加到静态定义的查询中。A Page知道可用元素和页面的总数。它通过基础设施触发计数查询来计算总数。由于这可能是昂贵的,取决于所使用的商店,Slice可以用作退货。A Slice只知道是否有下一个Slice可用的数据,当走过一个更大的结果集时可能就足够了。

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



要找出完整查询得到的页数,您必须触发一个附加的计数查询。默认情况下,这个查询将从您实际触发的查询中派生。



3.4.5。限制查询结果


查询方法的结果可以通过关键字来限制,first或者top可以互换使用。一个可选的数字值可以被附加到top / first来指定要返回的最大结果大小。如果该号码被遗漏,则假定结果大小为1。

示例18.使用

Top

和限制查询的结果大小 First


User findFirstByOrderByLastnameAsc();

User findTopByOrderByAgeDesc();

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

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

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

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


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


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




请注意,通过Sort参数限制结果与动态排序结合使用,可以表示“K”最小以及“K”最大元素的查询方法。



3.4.6。流式查询结果


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

示例19.使用Java 8流式传输查询的结果 

Stream<T>


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

Stream<User> readAllByFirstnameNotNull();

@Query("select u from User u")
Stream<User> streamAllPaged(Pageable pageable);




Stream潜在封装底层数据存储特定资源和使用后必须因此被关闭。您可以手动关闭Stream使用该close()方法或使用Java 7 try-with-resources块。


例20. 

Stream<T>

在try-with-resources块中处理结果


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




并不是所有的Spring Data模块都支持Stream<T>返回类型。



3.4.7。异步查询结果



使用Spring的异步方法执行功能可以异步执行版本库查询。这意味着方法将在调用时立即返回,实际的查询执行将发生在已经提交给Spring TaskExecutor的任务中。



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

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

@Async
ListenableFuture<User> findOneByLastname(String lastname);




使用java.util.concurrent.Future的返回类型。


使用Java 8 java.util.concurrent.CompletableFuture作为返回类型。


使用一个org.springframework.util.concurrent.ListenableFuture返回类型。



3.5。创建存储库实例



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



3.5.1。XML配置



每个Spring Data模块都包含一个存储库元素,它允许您简单地定义Spring为您扫描的基本包。



例子21.通过XML启用Spring Data存储库



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

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

</beans:beans>


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


使用过滤器


默认情况下,基础架构将拾取扩展Repository位于已配置基础包下的持久性技术特定子接口的每个接口,并为其创建一个bean实例。但是,您可能需要更细致地控制创建哪个接口的bean实例。要做到这一点,你使用<include-filter /><exclude-filter />内部元素<repositories />。语义与Spring的上下文命名空间中的元素完全等价。有关详细信息,请参阅这些元素的Spring参考文档


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



例22.使用排除过滤元素



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


这个例子排除了SomeRepository被实例化的所有接口。


3.5.2。JavaConfig


存储库基础架构也可以使用@Enable${store}RepositoriesJavaConfig类上的特定于存储区的注释来触发。有关Spring容器的基于Java的配置的介绍,请参阅参考文档。1 ]


启用Spring Data存储库的示例配置看起来像这样。



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



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

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




该示例使用特定于JPA的注释,您将根据实际使用的商店模块更改该注释。这同样适用于EntityManagerFactorybean的定义。请参阅有关商店特定配置的章节。



3.5.3。独立使用



您也可以使用Spring容器之外的存储库基础结构,例如在CDI环境中。你的类路径中仍然需要一些Spring库,但是通常你也可以通过程序设置库。提供存储库支持的Spring Data模块提供了一个您可以使用的持久化技术特定的RepositoryFactory,如下所示。



示例24.独立使用存储库工厂



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



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



在本节中,您将学习关于存储库自定义以及片段如何形成组合存储库的信息。



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



3.6.1。定制个人存储库



为了丰富具有自定义功能的存储库,您首先要为自定义功能定义一个片段接口和一个实现。然后让您的存储库接口另外扩展从片段接口。



示例25.自定义存储库功能的接口



interface CustomizedUserRepository {
  void someCustomMethod(User user);
}



示例26.定制存储库功能的实现



class CustomizedUserRepositoryImpl implements CustomizedUserRepository {

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




要找到的类的最重要的位是Impl与片段接口相比的名称的后缀。


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


示例27.更改您的存储库接口



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

  // Declare query methods here
}



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



Spring数据存储库通过使用构成存储库组合的片段来实现。片段是基础知识库,功能方面如QueryDsl和自定义接口以及它们的实现。每次将接口添加到存储库接口时,都可以通过添加片段来增强组合。每个Spring Data模块提供基础知识库和知识库方面的实现。



示例28.碎片及其实现



interface HumanRepository {
  void someHumanMethod(User user);
}

class HumanRepositoryImpl implements HumanRepository {

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

interface EmployeeRepository {

  void someEmployeeMethod(User user);

  User anotherEmployeeMethod(User user);
}

class ContactRepositoryImpl implements ContactRepository {

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

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



示例29.您的存储库接口的更改



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

  // Declare query methods here
}



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


例子30.碎片覆盖 

save(…)


interface CustomizedSave<T> {
  <S extends T> S save(S entity);
}

class CustomizedSaveImpl<T> implements CustomizedSave<T> {

  public <S extends T> S save(S entity) {
    // Your custom implementation
  }
}



例子31.定制的存储库接口



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

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



组态


如果使用名称空间配置,则存储库基础结构会尝试通过扫描我们找到的存储库所在的包中的类来自动检测自定义实现片段。这些类需要遵循将名称空间元素的属性附加repository-impl-postfix到找到的片段接口名称的命名约定。这个后缀默认为Impl


示例32.配置示例



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

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


第一个配置示例将尝试查找一个类com.acme.repository.CustomizedUserRepositoryImpl作为自定义存储库实现,而第二个示例将尝试查找com.acme.repository.CustomizedUserRepositoryFooBar


解决歧义



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


鉴于CustomizedUserRepository上面介绍的以下两个自定义实现,第一个实现将被选中。它的bean名称customizedUserRepositoryImpl与fragment接口(CustomizedUserRepository)加上后缀匹配Impl


例子33.解析实现



package com.acme.impl.one;

class CustomizedUserRepositoryImpl implements CustomizedUserRepository {

  // Your custom implementation
}



package com.acme.impl.two;

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

  // Your custom implementation
}


如果您使用bean名称添加注释UserRepository接口,@Component("specialCustom")并且Impl匹配为存储库实现定义的接口,com.acme.impl.two那么将被选中而不是第一个。


手动接线



如果您的自定义实现仅使用基于注释的配置和自动装配,那么刚才显示的方法效果很好,因为它将被视为任何其他Spring bean。如果您的实现片段bean需要特殊的布线,您只需声明这个bean并按照上述约定进行命名即可。然后,基础架构将通过名称引用手动定义的bean定义,而不是自己创建一个。



例34.自定义实现的手动连线



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

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



3.6.2。自定义基础存储库



上述方法需要在定制基本存储库行为时自定义所有存储库接口,以便所有存储库都受到影响。要更改所有存储库的行为,您需要创建一个扩展持久性技术特定的存储库基类的实现。此类将作为存储库代理的自定义基类。



示例35.定制存储库基类



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

  private final EntityManager entityManager;

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

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

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




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


最后一步是让Spring Data基础设施知道定制的存储库基类。在JavaConfig中,这是通过使用注释的repositoryBaseClass属性来实现的@Enable…Repositories


示例36.使用JavaConfig配置定制存储库基类



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



XML名称空间中提供了相应的属性。



示例37.使用XML配置定制存储库基类



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



3.7。从聚合根发布事件


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


例子38.从一个聚合根暴露域事件



class AnAggregateRoot {

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

    @AfterDomainEventPublication 
    void callbackMethod() {
       // … potentially clean up domain events list
    }
}




使用该方法@DomainEvents可以返回单个事件实例或一组事件。它不能有任何争论。


所有事件发布之后,使用注释的方法@AfterDomainEventPublication。它可以用来潜在地清理要发布的事件列表。


每次调用一个Spring数据库的save(…)方法时,都会调用这些方法。


3.8。Spring数据扩展



本部分记录了一组Spring数据扩展,可以在各种环境下使用Spring Data。目前大部分的整合是针对Spring MVC的。



3.8.1。Querydsl扩展



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


几个Spring Data模块提供了与Querydsl的集成QueryDslPredicateExecutor


例子39. QueryDslPredicateExecutor接口



public interface QueryDslPredicateExecutor<T> {

  Optional<T> findById(Predicate predicate);  

  Iterable<T> findAll(Predicate predicate);   

  long count(Predicate predicate);            

  boolean exists(Predicate predicate);        

  // … more functionality omitted.
}




查找并返回一个匹配的实体Predicate


查找并返回所有匹配的实体Predicate


返回匹配的实体的数量Predicate


如果与Predicate存在匹配的实体返回。


要使用Querydsl支持,只需QueryDslPredicateExecutor在您的存储库接口上进行扩展。


示例40.存储库上的Querydsl集成



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

}


以上可以使用Querydsl来编写类型安全查询Predicate


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

userRepository.findAll(predicate);



3.8.2。Web支持




本节包含Spring Data Web支持的文档,因为它是在1.6范围内的Spring Data Commons中实现的。由于新引入的支持改变了很多东西,我们在Legacy Web支持中保留了以前行为的文档。


如果模块支持存储库编程模型,则Spring Data模块附带有各种Web支持。Web相关的东西需要在类路径上使用Spring MVC JAR,其中一些甚至提供了与Spring HATEOAS 2 ]的集成。通常,通过@EnableSpringDataWebSupport在JavaConfig配置类中使用注释来启用集成支持。


例子41.启用Spring Data web支持



@Configuration
@EnableWebMvc
@EnableSpringDataWebSupport
class WebConfiguration {}


@EnableSpringDataWebSupport批注注册几个组件,我们将在一个位讨论。它也会在类路径中检测到Spring HATEOAS,并为其注册集成组件。

或者,如果您使用的是XML配置,则可以注册为SpringDataWebSupport或者HateoasAwareSpringDataWebSupport以Spring bean的形式注册:


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



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

<!-- If you're using Spring HATEOAS as well register this one *instead* of the former -->
<bean class="org.springframework.data.web.config.HateoasAwareSpringDataWebConfiguration" />



基本的Web支持



上面显示的配置设置将注册一些基本组件:



DomainClassConverter

  • 使Spring MVC能够从请求参数或路径变量中解析存储库管理的域类的实例。

HandlerMethodArgumentResolver

  •  让Spring MVC根据请求参数解析Pageable和Sort实例。



DomainClassConverter


DomainClassConverter让你在你的Spring MVC控制器方法签名直接使用域类型,这样就不必通过库手动查找实例:


例子43.在方法签名中使用域类型的Spring MVC控制器



@Controller
@RequestMapping("/users")
class UserController {

  @RequestMapping("/{id}")
  String showUserForm(@PathVariable("id") User user, Model model) {

    model.addAttribute("user", user);
    return "userForm";
  }
}


正如你所看到的,该方法直接接收一个User实例,不需要进一步的查找。通过让Spring MVC首先将路径变量转换为域类的id类型并最终通过调用findById(…)为域类型注册的存储库实例来访问实例,可以解决该实例。



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



用于Pageable和Sort的HandlerMethodArgumentResolvers


上面的配置片段也注册了一个PageableHandlerMethodArgumentResolver以及一个实例SortHandlerMethodArgumentResolver。注册启用PageableSort成为有效的控制器方法参数


示例44.使用Pageable作为控制器方法参数



@Controller
@RequestMapping("/users")
class UserController {

  private final UserRepository repository;

  UserController(UserRepository repository) {
    this.repository = repository;
  }

  @RequestMapping
  String showUsers(Model model, Pageable pageable) {

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



这个方法签名会导致Spring MVC尝试使用下面的默认配置从请求参数派生一个Pageable实例:


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

page

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

size

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

sort

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

为了定制这个行为,注册一个实现接口的bean PageableHandlerMethodArgumentResolverCustomizer或者SortHandlerMethodArgumentResolverCustomizer分别的bean 。这个customize()方法会被调用,允许你改变设置。就像下面的例子。


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


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

如果您需要从请求中解析多个PageableSort实例(例如,对于多个表),则可以使用Spring的@Qualifier注释来区分。请求参数必须以前缀${qualifier}_。所以对于这样的方法签名:


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


你有填充foo_pagebar_page等。

Pageable交给方法的默认值等于a,new PageRequest(0, 20)但可以使用参数@PageableDefault上的注释进行定制Pageable


超媒体支持Pageables


Spring HATEOAS提供了一个表示模型类PagedResources,它允许Page使用必要的Page元数据丰富实例的内容,以及让客户端轻松浏览页面的链接。页面到a的转换PagedResources是通过Spring HATEOAS ResourceAssembler接口的实现完成的PagedResourcesAssembler


示例45.使用PagedResourcesAssembler作为控制器方法参数



@Controller
class PersonController {

  @Autowired PersonRepository repository;

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

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


如上所示启用配置允许将PagedResourcesAssembler其用作控制器方法参数。调用toResources(…)它将导致以下内容:


  • 的内容

Page

  • 将成为内容

PagedResources

  • 实例。

PagedResources

  • 会得到一个

PageMetadata

  • 附加填充信息形成的实例

Page

  • 和基础

PageRequest

  • 取决于页面状态的

PagedResources

  • 获取

prev

next

  • 链接。链接将指向调用的方法映射到的URI。添加到该方法的分页参数将与该设置相匹配,

PageableHandlerMethodArgumentResolver

  • 以确保以后可以解析链接。


假设我们在数据库中有30个Person实例。你现在可以触发一个请求,你会看到类似的东西:GET http://localhost:8080/persons


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


您会看到汇编程序生成了正确的URI,并且还提取了缺省配置,将参数解析Pageable为一个即将到来的请求。这意味着,如果您更改该配置,链接将自动遵守更改。默认情况下,汇编程序指向它被调用的控制器方法,但是可以通过交付自定义Link来定制,作为基础来构建分页链接以重载PagedResourcesAssembler.toResource(…)方法。


Querydsl网络支持


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

这意味着给定User来自以前样本的对象一个查询字符串


?firstname=Dave&lastname=Matthews



可以解决



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


使用QuerydslPredicateArgumentResolver



@EnableSpringDataWebSupport当在类路径上找到Querydsl时, 该功能将自动启用。


添加一个@QuerydslPredicate方法签名将提供一个准备使用Predicate,可以通过执行QueryDslPredicateExecutor



类型信息通常从方法返回类型中解析出来。由于这些信息不一定与域类型相匹配,因此使用该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<String, String> parameters) {

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

    return "index";
  }
}




解析查询字符串参数匹配PredicateUser



默认绑定如下:


Object

  • 简单的属性如

eq

Object

  • 就像集合的属性一样

contains

Collection

  • 简单的属性如

in


这些绑定可以通过Java 8 的bindings属性@QuerydslPredicate或通过使用Java 8 default methods添加QuerydslBinderCustomizer到存储库接口来定制。


interface UserRepository extends CrudRepository<User, String>,
                                 QueryDslPredicateExecutor<User>,                
                                 QuerydslBinderCustomizer<QUser> {               

  @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属性的默认绑定定义为不区分大小写包含匹配项。


从解决方案中排除密码属性Predicate



3.8.3。存储库填充程序


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

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


例子46.在JSON中定义的数据



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



您可以使用Spring Data Commons中提供的存储库名称空间的populator元素轻松地填充存储库。要将上述数据填充到PersonRepository中,请执行以下操作:



示例47.声明一个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对象将被解组到的类型将通过检查_classJSON文档的属性来确定。基础设施将最终选择适当的存储库来处理刚被反序列化的对象。

为了使用XML来定义存储库应该被填充的数据,可以使用该unmarshaller-populator元素。您可以将其配置为使用Spring OXM为您提供的XML编码器选项之一。有关详细信息,请参阅Spring参考文档


示例48.声明一个反编组存储器加载器(使用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>



3.8.4。遗留网络支持



Spring MVC的域类Web绑定



鉴于您正在开发Spring MVC Web应用程序,您通常必须从URL解析域类标识。默认情况下,您的任务是将该请求参数或URL部分转换为域类,以将其交给下面的层,或直接在实体上执行业务逻辑。这看起来像这样:



@Controller
@RequestMapping("/users")
class UserController {

  private final UserRepository userRepository;

  UserController(UserRepository userRepository) {
    Assert.notNull(repository, "Repository must not be null!");
    this.userRepository = userRepository;
  }

  @RequestMapping("/{id}")
  String showUserForm(@PathVariable("id") Long id, Model model) {

    // Do null check for id
    User user = userRepository.findById(id);
    // Do null check for user

    model.addAttribute("user", user);
    return "user";
  }
}


首先为每个控制器声明一个存储库依赖关系,分别查找由控制器或存储库管理的实体。查看实体也是样板,因为它总是一个findById(…)电话。幸运的是,Spring提供了注册自定义组件的方法,允许在一个String值到任意类型之间进行转换。


属性编辑器


对于3.0之前的Spring版本PropertyEditors,必须使用简单的Java 。为了与之整合,Spring Data提供了一个DomainClassPropertyEditorRegistrar查找注册在Spring中的所有Spring Data存储库,ApplicationContextPropertyEditor为托管域类注册一个自定义。


<bean class="….web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter">
  <property name="webBindingInitializer">
    <bean class="….web.bind.support.ConfigurableWebBindingInitializer">
      <property name="propertyEditorRegistrars">
        <bean class="org.springframework.data.repository.support.DomainClassPropertyEditorRegistrar" />
      </property>
    </bean>
  </property>
</bean>



如果你像前面的例子那样配置了Spring MVC,你可以像下面这样配置你的控制器,这可以减少大量的杂乱和样板。



@Controller
@RequestMapping("/users")
class UserController {

  @RequestMapping("/{id}")
  String showUserForm(@PathVariable("id") User user, Model model) {

    model.addAttribute("user", user);
    return "userForm";
  }
}