Spring Data repository 抽象的中心接口是 Repository。它把要管理的 domain 类以及 domain 类的ID类型作为泛型参数。这个接口主要是作为一个标记接口,用来捕捉工作中的类型,并帮助你发现扩展这个接口的接口。 CrudRepository 和 ListCrudRepository 接口为被管理的实体类提供复杂的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判断实体是否存在。 |
ListCrudRepository 提供了同等的方法,但它们返回 List,而 CrudRepository 的方法返回 Iterable。
我们还提供了持久化技术的特定抽象,如 JpaRepository 或 MongoRepository。这些接口扩展了 CrudRepository,除了像 CrudRepository 这样相当通用的持久化技术的接口之外,还暴露了底层持久化技术的能力。 |
除了 CrudRepository 之外,还有一个 PagingAndSortingRepository 的抽象,它增加了额外的分页,排序方法。
Example 4. PagingAndSortingRepository 接口
public interface PagingAndSortingRepository {
Iterable findAll(Sort sort);
Page findAll(Pageable pageable);
}
例如,访问第2页的 User ,每页20条数据,你可以这样:
PagingAndSortingRepository repository = // … get access to a bean
Page users = repository.findAll(PageRequest.of(1, 20));
除了 query 方法外,count 和 delete 查询的查询派生也是可用的。下面的列表显示了派生的 count 查询的接口定义。
Example 5. Derived Count Query
interface UserRepository extends CrudRepository {
long countByLastname(String lastname);
}
下面的列表显示了一个派生的 delete 查询的接口定义。
Example 6. Derived Delete Query
interface UserRepository extends CrudRepository {
long deleteByLastname(String lastname);
List removeByLastname(String lastname);
}
标准的CRUD Repository 通常有对底层数据store的查询。使用Spring Data,声明这些查询成为一个四步过程。
下面的章节将详细解释每一个步骤。
要定义一个 repository 接口,你首先需要定义一个domain类专用的 repository 接口。该接口必须继承 Repository,并将其泛型设置为domain类和ID类。如果你想为该domain类公开CRUD方法,你可以继承 CrudRepository,或其变体,而不是 Repository。
有几种变体可以让你开始使用你的 repository 接口。
典型的方法是继承 CrudRepository,它为你提供了 CRUD 功能的方法。CRUD是指创建、读取、更新、删除。在3.0版本中,我们还引入了 ListCrudRepository,它与 CrudRepository 非常相似,但对于那些返回多个实体的方法,它返回一个 List 而不是一个 Iterable,你可能会发现它更容易使用。
如果你使用的是响应式store,你可以选择 ReactiveCrudRepository,或者 RxJava3CrudRepository,这取决于你使用的是哪种响应式框架。
如果你使用的是Kotlin,你可以选择 CoroutineCrudRepository,它利用了Kotlin的 coroutine(协程)。
额外的你可以扩展 PagingAndSortingRepository、ReactiveSortingRepository、RxJava3SortingRepository 或 CoroutineSortingRepository,如果你需要允许指定一个 Sort 抽象的方法,或者在第一种情况下是 Pageable 抽象。请注意,各种排序 repository 不再像Spring Data 3.0之前的版本那样扩展各自的CRUD库。因此,如果你想获得这两个接口的功能,你需要扩展这两个接口。
如果你不想扩展Spring Data接口,你也可以用 @RepositoryDefinition 来注解你的 repository 接口。扩展CRUD repository 接口之一会暴露出一套完整的方法来操作你的实体。如果你想对暴露的方法有所选择,可以从CRUD repository 复制你想暴露的方法到你的 domain repository。这样做时,你可以改变方法的返回类型。如果可能的话,Spring Data会尊重返回类型。例如,对于返回多个实体的方法,你可以选择 Iterable
如果你的应用程序中的许多 repository 应该有相同的方法集,你可以定义你自己的基础接口来继承。这样的接口必须用 @NoRepositoryBean 来注释。这可以防止Spring Data试图直接创建它的实例而导致异常,因为它仍然包含一个泛型变量,Spring data 无法确定该 repository 的实体。
下面的例子展示了如何有选择地公开CRUD方法(本例中为 findById 和 save)。
Example 7. Selectively exposing CRUD methods
@NoRepositoryBean
interface MyBaseRepository extends Repository {
Optional findById(ID id);
S save(S entity);
}
interface UserRepository extends MyBaseRepository {
User findByEmailAddress(EmailAddress emailAddress);
}
在前面的例子中,你为所有的 domain repository 定义了一个通用的基础接口,并暴露了 findById(…) 以及 save(…)。这些方法被路由到Spring Data提供的你所选择的store的基础 repository 实现(例如,如果你使用JPA,实现是 SimpleJpaRepository),因为它们与 CrudRepository 中的方法签名一致。 所以 UserRepository 现在可以保存用户,通过ID查找单个用户,通过电子邮件地址查找 User。
中间的 repository 接口被注解为 @NoRepositoryBean。确保你在所有Spring Data不应该在运行时创建实例的 repository 接口上添加该注解。 |
在你的应用程序中使用一个独特的Spring Data模块使事情变得简单,因为定义范围内的所有 repository 接口都绑定到Spring Data模块。有时,应用程序需要使用一个以上的Spring Data模块。在这种情况下,repository 定义必须区分持久化技术。当它检测到类路径上有多个 repository 工厂时,Spring Data会进入严格的 repository 配置模式。严格的配置使用 repository 或domain类的细节来决定 repository 定义的Spring Data模块绑定。
下面的例子显示了一个使用特定模块接口的 repository(本例中为JPA)。
Example 8. 使用模块特定接口的 Repository 定义
interface MyRepository extends JpaRepository { }
@NoRepositoryBean
interface MyBaseRepository extends JpaRepository { … }
interface UserRepository extends MyBaseRepository { … }
MyRepository 和 UserRepository 在其类型层次上扩展了 JpaRepository。它们是Spring Data JPA 模块的有效候选者。
下面的例子显示了一个使用通用(泛型)接口的 repository。
Example 9. 使用泛型接口的 repository 定义
interface AmbiguousRepository extends Repository { … }
@NoRepositoryBean
interface MyBaseRepository extends CrudRepository { … }
interface AmbiguousUserRepository extends MyBaseRepository { … }
AmbiguousRepository 和 AmbiguousUserRepository 在其类型层次结构中只继承了 Repository 和 CrudRepository 。虽然在使用唯一的Spring Data模块时这很好,但多个模块无法区分这些 repository 应该被绑定到哪个特定的Spring Data。
下面的例子显示了一个使用带注解的domain类的repository。
Example 10. 使用带注解的 domain 类的Repository 定义
interface PersonRepository extends Repository { … }
@Entity
class Person { … }
interface UserRepository extends Repository { … }
@Document
class User { … }
PersonRepository 引用了 Person,它被 JPA 的 @Entity 注解所注解,所以这个 repository 显然属于Spring Data JPA。UserRepository 引用了 User,它被Spring Data MongoDB 的 @Document 注解所注解。
下面的坏例子显示了一个使用混合注解的 domain 类的 Repository。
Example 11. 使用具有混合注解的 domain 类的 repository 定义
interface JpaPersonRepository extends Repository { … }
interface MongoDBPersonRepository extends Repository { … }
@Entity
@Document
class Person { … }
这个例子展示了一个同时使用JPA和Spring Data MongoDB注解的 domain 类。它定义了两个repository:JpaPersonRepository 和 MongoDBPersonRepository。一个用于JPA,另一个用于MongoDB的使用。Spring Data不再能够区分这些repository,这导致了未定义的行为。
Repository 类型细节和区分domain类注解用于严格的repository库配置,以确定特定Spring Data模块的repository候选者。在同一domain类型上使用多个持久化技术的特定注解是可能的,并且能够在多个持久化技术中重复使用domain类型。然而,Spring Data就不能再确定一个唯一的模块来绑定repository了。
区分 repository 的最后一个方法是通过对 repository base package的扫描。base package 定义了扫描 repository 接口定义的起点,这意味着将 repository 的定义放在适当的包中。默认情况下,注解驱动的配置使用配置类所在的base package。基于XML的配置中的base package,需要手动强制配置。
下面的例子显示了注解驱动的 base package 的配置。
Example 12. 注解驱动的 base package 的配置
@EnableJpaRepositories(basePackages = "com.acme.repositories.jpa")
@EnableMongoRepositories(basePackages = "com.acme.repositories.mongo")
class Configuration { … }
repository 代理有两种方法可以从方法名中推导出 repository 特定的查询。
可用的选项取决于实际的store。然而,必须有一个策略来决定创建什么样的实际查询。下一节将介绍可用的选项。
下列策略可用于 repository 基础设施解析查询。 对于 XML 配置,你可以通过 query-lookup-strategy 属性在命名空间配置策略。 对于 Java 配置,你可以使用 EnableMongoRepositories 注解的 queryLookupStrategy 属性。有些策略可能不支持特定的datastore。
内置在Spring Data repository 基础架构中的查询 builder 机制对于在资源库的实体上建立约束性查询非常有用。
下面的例子展示了如何创建一些查询。
Example 13. Query creation from method names
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);
}
解析查询方法名称分为主语和谓语。第一部分(find…By, exists…By)定义了查询的主语,第二部分形成谓语。引入句(主语)可以包含进一步的表达。在 find(或其他引入关键词)和 By 之间的任何文本都被认为是描述性的,除非使用一个限制结果的关键词,如 Distinct 在要创建的查询上设置一个不同的标志,或 Top / First 来限制查询结果。
附录中包含了 查询方法主语关键词 和 查询方法谓语关键词的完整列表,包括排序和字母修饰语。然而,第一个 By 作为分界符,表示实际条件谓词的开始。在一个非常基本的层面上,你可以在实体属性上定义条件,并用 And 和 Or 来连接它们。
解析方法的实际结果取决于你为之创建查询的持久性store。然而,有一些东西需要注意。
属性表达式只能引用被管理实体的一个直接属性,如前面的例子所示。在查询创建时,你已经确保解析的属性是被管理的domian类的一个属性。然而,你也可以通过遍历嵌套属性来定义约束。考虑一下下面的方法签名。
List findByAddressZipCode(ZipCode zipCode);
假设 Person 有一个带有 ZipCode 的 Address。在这种情况下,该方法创建 x.address.zipCode 属性遍历。解析算法首先将整个部分(AddressZipCode)解释为属性,并检查domain类中是否有该名称的属性(未加首字母)。如果算法成功,它就使用该属性。如果没有,该算法将源头的驼峰字母部分从右侧分割成一个头和一个尾,并试图找到相应的属性—在我们的例子中,是 AddressZip 和 Code。如果该算法找到了具有该头部的属性,它就取其尾部,并从那里继续向下构建树,以刚才描述的方式将尾部分割开来。如果第一次分割不匹配,该算法将分割点移到左边(Address, ZipCode)并继续。
虽然这在大多数情况下应该是有效的,但该算法有可能选择错误的属性。假设 Person 类也有一个 addressZip 属性。该算法将在第一轮分割中已经匹配,选择错误的属性,并且失败(因为 addressZip 的类型可能没有 code 属性)。
为了解决这个模糊的问题,你可以在你的方法名里面使用 _ 来手动定义遍历点。因此,我们的方法名称将如下。
List findByAddress_ZipCode(ZipCode zipCode);
因为我们把下划线字符当作一个保留字符,所以我们强烈建议遵循标准的Java命名惯例(也就是说,不要在属性名中使用下划线,而要使用驼峰大写)。
为了处理你的查询中的参数,定义方法参数,正如在前面的例子中已经看到的。除此之外,基础设施还能识别某些特定的类型,如 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);
API中定义的 Sort 和 Pageable 实际调用时不能为 null。如果你不想应用任何排序或分页,请使用 Sort.unsorted() 和 Pageable.unpaged()。 |
第一个方法让你把 org.springframework.data.domain.Pageable 实例传递给 query 方法,以动态地将分页添加到你静态定义的查询中。一个 Page 知道可用的元素和页面的总数。它是通过基础设施触发一个 count 查询来计算总数量。由于这可能是昂贵的(取决于使用的store),你可以返回一个 Slice。一个 Slice 只知道下一个 Slice 是否可用,当遍历一个较大的结果集时,这可能就足够了。
排序选项也是通过 Pageable 实例处理的。如果你只需要排序,在你的方法中加入 org.springframework.data.domain.Sort 参数。正如你所看到的,返回一个 List 也是可能的。在这种情况下,构建实际的 Page 实例所需的额外元数据并没有被创建(这反过来意味着不需要发出额外的 count 查询)。相反,它限制了查询,只查询给定范围的实体。
要想知道你 query 的总页数,你必须触发一个额外的count查询。默认情况下,这个查询是由你实际触发的查询派生出来的。 |
你可以通过使用属性名称来定义简单的排序表达式。你可以将表达式连接起来,将多个 criteria 收集到一个表达式中。
Example 15. Defining sort expressions
Sort sort = Sort.by("firstname").ascending()
.and(Sort.by("lastname").descending());
对于定义排序表达式的更加类型安全的方式,从定义排序表达式的类型开始,使用方法引用来定义排序的属性。
Example 16. 通过使用类型安全的API来定义排序表达式
TypedSort person = Sort.sort(Person.class);
Sort sort = person.by(Person::getFirstname).ascending()
.and(person.by(Person::getLastname).descending());
TypedSort.by(…) 通过(通常)使用 CGlib 来使用运行时代理,这在使用 Graal VM Native 等工具时可能会干扰原生镜像的编译。 |
如果你的 store 实现支持 Querydsl,你也可以使用生成的 metamodel 类型来定义排序表达式。
Example 17. 通过使用Querydsl API定义排序表达式
QSort sort = QSort.by(QPerson.firstname.asc())
.and(QSort.by(QPerson.lastname.desc()));
你可以通过使用 first 或 top 关键字来限制查询方法的结果,这两个关键字可以互换使用。你可以在 top 或 first 后面附加一个可选的数值,以指定要返回的最大结果大小。如果不加数字,就会假定结果大小为 1。下面的例子显示了如何限制查询的大小。
Example 18. 使用 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 关键字将结果包入。
如果分页或 slice 应用于 limit 查询的分页(以及可用页数的计算),则会在 limit 结果中应用。
通过使用 Sort 参数将结果与动态排序相结合,可以让你表达对 "K" 最小元素和 "K" 最大元素的查询方法。 |
返回多个结果的查询方法可以使用标准的Java Iterable、List 和 Set。除此之外,我们还支持返回Spring Data的 Streamable,这是 Iterable 的一个自定义扩展,以及 Vavr 提供的 collection 类型。请参考附录中对所有可能的 查询方法返回类型的解释。
你可以用 Streamable 来替代 Iterable 或任何 collection 类型。它提供了方便的方法来访问一个非并行的 Stream(Iterable 所没有的),并且能够在元素上直接 …filter(…) 和 …map(…),并将 Streamable 与其他元素连接起来。
Example 19. 使用 Streamable 来组合 query 方法的结果
interface PersonRepository extends Repository {
Streamable findByFirstnameContaining(String firstname);
Streamable findByLastnameContaining(String lastname);
}
Streamable result = repository.findByFirstnameContaining("av")
.and(repository.findByLastnameContaining("ea"));
为集合提供专用的 wrapper 类型是一种常用的模式,为返回多个元素的查询结果提供API。通常,这些类型的使用是通过调用返回类似集合类型的 repository 方法,并手动创建一个 wrapper 类型的实例。你可以避免这个额外的步骤,因为Spring Data允许你使用这些 wrapper 类型作为查询方法的返回类型,如果它们满足以下条件。
下面列出了一个例子。
class Product {
MonetaryAmount getPrice() { … }
}
@RequiredArgsConstructor(staticName = "of")
class Products implements Streamable {
private final Streamable streamable;
public MonetaryAmount getTotal() {
return streamable.stream()
.map(Priced::getPrice)
.reduce(Money.of(0), MonetaryAmount::add);
}
@Override
public Iterator iterator() {
return streamable.iterator();
}
}
interface ProductRepository implements Repository {
Products findAllByDescriptionContaining(String text);
}
一个 Product 实体,公开API以访问 product 的price。 |
|
一个 Streamable |
|
wrapper 类型暴露了一个额外的API,在 Streamable |
|
实现 Streamable 接口并委托给实际结果。 |
|
wrapper 类型 Products 可以直接作为查询方法的返回类型。你不需要返回 Streamable |
Vavr 是一个拥抱Java中函数式编程概念的库。它带有一组自定义的集合类型,你可以将其作为查询方法的返回类型,如下表所示。
Vavr collection 类型 |
使用的Vavr实现类型 |
有效的Java原类型 |
io.vavr.collection.Seq |
io.vavr.collection.List |
java.util.Iterable |
io.vavr.collection.Set |
io.vavr.collection.LinkedHashSet |
java.util.Iterable |
io.vavr.collection.Map |
io.vavr.collection.LinkedHashMap |
java.util.Map |
你可以使用第一列中的类型(或其子类型)作为查询方法的返回类型,并根据实际查询结果的Java类型(第三列),获得第二列中的类型作为实现类型使用。或者,你可以声明 Traversable(相当于Vavr Iterable),然后我们从实际返回值中派生出实现类。也就是说,java.util.List 会变成 Vavr List 或 Seq,java.util.Set 会变成 Vavr LinkedHashSet Set,以此类推。
从Spring Data 2.0开始,返回单个聚合实例的 repository CRUD方法使用Java 8的 Optional 来表示可能没有的值。除此之外,Spring Data还支持在查询方法上返回以下 wrapper 类型。
另外,查询方法可以选择完全不使用 wrapper 类型。没有查询结果的话会通过返回 null 来表示。Repository 方法返回集合、集合替代物、wrapper和流时,保证不会返回 null,而是返回相应的空(Empty)表示。详见 “Repository 查询返回类型”。
你可以通过使用 Spring Framework的nullability注解 来表达 repository 方法的 nullability 约束。它们提供了一种友好的方法,并在运行时选择加入 null 值检查,如下所示。
Spring注解是用 JSR 305 注解(一个休眠状态但广泛使用的JSR)进行元注解的。JSR 305元注解让工具供应商(如 IDEA、 Eclipse 和 Kotlin)以通用的方式提供 null-safety 支持,而不需要对Spring注解进行硬编码支持。为了在运行时检查查询方法的无效性约束,你需要通过在 package-info.java 中使用 Spring 的 @NonNullApi,在包级别上激活null约束,如下例所示。
Example 20. Declaring Non-nullability in package-info.java
@org.springframework.lang.NonNullApi
package com.acme;
一旦定义了非null约束,repository 的查询方法调用就会在运行时被验证是否有nul约束。如果查询结果违反了定义的约束条件,就会抛出一个异常。这种情况发生在方法会返回 null,但被声明为non-nullable(在 repository 所在的包上定义注解的默认值)。如果你想再次选择加入允许结果为null,可以有选择地在个别方法上使用 @Nullable。使用本节开头提到的结果wrapper类型继续按预期工作:一个空的结果被翻译成代表不存在的值。
下面的例子显示了刚才描述的一些技术。
Example 21. Using different nullability constraints
package com.acme;
interface UserRepository extends Repository {
User getByEmailAddress(EmailAddress emailAddress);
@Nullable
User findByEmailAddress(@Nullable EmailAddress emailAdress);
Optional findOptionalByEmailAddress(EmailAddress emailAddress);
}
repository 在一个包(或子包)中,我们已经为其定义了非空的行为。 |
|
当查询没有产生结果时,抛出一个 EmptyResultDataAccessException。当交给该方法的 emailAddress 为 null 时,抛出一个 IllegalArgumentException。 |
|
当查询没有产生结果时返回 null。也接受 null 作为 emailAddress 的值。 |
|
当查询没有产生结果时,返回 Optional.empty()。当交给该方法的 emailAddress 为 null 时,抛出一个 IllegalArgumentException。 |
Kotlin在语言中加入了 对无效性约束的定义。Kotlin代码编译为字节码,它不通过方法签名来表达无效性约束,而是通过编译后的元数据。请确保在你的项目中包含 kotlin-reflect JAR,以实现对Kotlin的nullability约束的内省。Spring Data Repository 使用语言机制来定义这些约束,以应用相同的运行时检查,如下所示。
Example 22. Using nullability constraints on Kotlin repositories
interface UserRepository : Repository {
fun findByUsername(username: String): User
fun findByFirstname(firstname: String?): User?
}
该方法将参数和结果都定义为不可为空(Kotlin默认)。Kotlin编译器会拒绝那些向方法传递 null 的方法调用。如果查询产生了一个空的结果,就会抛出一个 EmptyResultDataAccessException。 |
|
这个方法接受 null 作为 firstname 参数,如果查询没有产生结果,则返回 null。 |
你可以通过使用Java 8 Stream
Example 23. Stream the result of a query with Java 8 Stream
@Query("select u from User u")
Stream findAllByCustomQueryAndStream();
Stream readAllByFirstnameNotNull();
@Query("select u from User u")
Stream streamAllPaged(Pageable pageable);
一个 Stream 可能包裹了底层 data store 的特定资源,因此,在使用后必须关闭。你可以通过使用 close() 方法来手动关闭 Stream,或者使用Java 7 try-with-resources 块来关闭,如下面的例子中所示。 |
Example 24. Working with a Stream
try (Stream stream = repository.findAllByCustomQueryAndStream()) {
stream.forEach(…);
}
目前并非所有的Spring Data模块都支持 Stream |
你可以通过使用 Spring的异步方法运行能力 来异步运行 repository 查询。这意味着该方法在调用后立即返回,而实际的查询发生在一个已经提交给Spring TaskExecutor 的任务中。异步查询与响应式查询不同,不应混合使用。关于响应式支持的更多细节,请参见store的特定文档。下面的例子显示了一些异步查询的案例。
@Async
Future findByFirstname(String firstname);
@Async
CompletableFuture findOneByFirstname(String firstname);
使用 java.util.concurrent.Future 作为返回类型。 |
|
使用 Java 8 java.util.concurrent.CompletableFuture 作为返回类型。 |
本节介绍了如何为定义的 repository 接口创建实例和Bean定义。
在Java配置类上使用store特有的 @EnableMongoRepositories 注解来定义 repository 激活的配置。关于基于Java的Spring容器配置的介绍,请参见 Spring参考文档中的JavaConfig。
启用 Spring Data Repository 的示例配置类似于以下内容。
Example 25. 基于注解的 repository 配置示例
@Configuration
@EnableJpaRepositories("com.acme.repositories")
class ApplicationConfiguration {
@Bean
EntityManagerFactory entityManagerFactory() {
// …
}
}
前面的例子使用了JPA特定的注解,你可以根据你实际使用的store模块来改变它。这同样适用于 EntityManagerFactory Bean的定义。请看涉及store特定配置的章节。 |
每个Spring Data模块都包括一个 repositories 元素,让你定义一个Spring为你扫描的 base package,如下例所示。
Example 26. 通过XML启用Spring Data Repository
在前面的例子中,Spring被指示扫描 com.acme.repositories 及其所有子包,以寻找扩展 Repository 或其子接口之一的接口。对于找到的每个接口,基础设施都会注册持久化技术专用的 FactoryBean,以创建适当的代理,处理查询方法的调用。每个Bean都被注册在一个从接口名称衍生出来的Bean名称下,所以 UserRepository 的接口将被注册在 userRepository 下。嵌套的存储库接口的Bean名是以其包裹的类型名称为前缀。base package 属性允许通配符,这样你就可以定义一个扫描包的模式。
默认情况下,基础架构会抓取每个扩展了位于配置的 base package 下的持久化技术特定的 Repository 子接口的接口,并为其创建一个Bean实例。然而,你可能想要更精细地控制哪些接口为其创建Bean实例。要做到这一点,可以在 Repository 声明中使用 filter 元素。其语义与Spring的组件过滤器中的元素完全等同。详情请见 Spring参考文档 中的这些元素。
例如,为了排除某些接口作为 Repository Bean 的实例化,你可以使用以下配置。
Example 27. 使用 Filter
Java
XML
@Configuration
@EnableMongoRepositories(basePackages = "com.acme.repositories",
includeFilters = { @Filter(type = FilterType.REGEX, pattern = ".*SomeRepository") },
excludeFilters = { @Filter(type = FilterType.REGEX, pattern = ".*SomeOtherRepository") })
class ApplicationConfiguration {
@Bean
EntityManagerFactory entityManagerFactory() {
// …
}
}
前面的例子排除了所有以 SomeRepository 结尾的接口被实例化,包括以 SomeOtherRepository 结尾的接口。
你也可以在Spring容器之外使用资源库基础设施—例如,在CDI环境中。你仍然需要在你的classpath中使用一些Spring库,但是,一般来说,你也可以通过编程来设置 Repository。Repository 支持的Spring Data模块都有一个特定于持久化技术的 RepositoryFactory,你可以使用,如下所示。
Example 28. repository factory 的独立使用
RepositoryFactorySupport factory = … // Instantiate factory here
UserRepository repository = factory.getRepository(UserRepository.class);
Spring Data提供了各种选项来创建查询方法,只需少量编码。但当这些选项不符合你的需求时,你也可以为 repository 方法提供你自己的自定义实现。本节介绍了如何做到这一点。
要用自定义的功能来丰富 repository,你必须首先定义一个片段(fragment)接口和自定义功能的实现,如下所示。
Example 29. 定制 repository 功能的接口
interface CustomizedUserRepository {
void someCustomMethod(User user);
}
Example 30. 实现自定义 repository 的功能
class CustomizedUserRepositoryImpl implements CustomizedUserRepository {
public void someCustomMethod(User user) {
// Your custom implementation
}
}
与片段接口对应的类名中最重要的部分是 Impl 后缀。 |
实现本身并不依赖于Spring Data,它可以是一个普通的Spring Bean。因此,你可以使用标准的依赖注入行为来注入对其他Bean(如 JdbcTemplate)的引用,参与到各个切面,等等。
然后你可以让你的 repository 接口继承片段接口,如下所示。
Example 31. 改变你的 repository 接口
interface UserRepository extends CrudRepository, CustomizedUserRepository {
// Declare query methods here
}
用你的存储库接口继承片段接口,结合了CRUD和自定义功能,并使其对客户端可用。
Spring Data Repository 是通过使用形成 repository 组合的片段来实现的。片段是基础repository、功能方面(如 QueryDsl),以及自定义接口和它们的实现。每当你为你的repository接口添加一个接口,你就通过添加一个片段来增强组合。基础repository和repository方面的实现是由每个Spring Data模块提供的。
下面的例子显示了自定义接口和它们的实现。
Example 32. 片段及其实现
interface HumanRepository {
void someHumanMethod(User user);
}
class HumanRepositoryImpl implements HumanRepository {
public void someHumanMethod(User user) {
// Your custom implementation
}
}
interface ContactRepository {
void someContactMethod(User user);
User anotherContactMethod(User user);
}
class ContactRepositoryImpl implements ContactRepository {
public void someContactMethod(User user) {
// Your custom implementation
}
public User anotherContactMethod(User user) {
// Your custom implementation
}
}
下面的例子显示了一个扩展了 CrudRepository 的自定义 repository 的接口。
Example 33. 修改你的 repository 接口
interface UserRepository extends CrudRepository, HumanRepository, ContactRepository {
// Declare query methods here
}
Repository可以由多个自定义实现组成,这些自定义实现按其声明的顺序被导入。自定义实现的优先级高于基础实现和Repository 切面。这种排序可以让你覆盖基础Repository和切面的方法,并在两个片段贡献了相同的方法签名时解决歧义。Repository片段不限于在单一存储库接口中使用。多个Repository可以使用一个片段接口,让你在不同的 Repository 中重复使用自定义的内容。
下面的例子显示了一个 Repository 片段及其实现。
Example 34. Fragments overriding save(…)
interface CustomizedSave {
S save(S entity);
}
class CustomizedSaveImpl implements CustomizedSave {
public S save(S entity) {
// Your custom implementation
}
}
下面的例子显示了一个使用前述 repository 片段的 repository。
Example 35. 自定义 repository 接口
interface UserRepository extends CrudRepository, CustomizedSave {
}
interface PersonRepository extends CrudRepository, CustomizedSave {
}
repository基础设施试图通过扫描发现repository的包下面的类来自动检测自定义实现片段。这些类需要遵循后缀默认为 Impl 的命名惯例。
下面的例子显示了一个使用默认后缀的 repository 和一个为后缀设置了自定义值的 repository。
Example 36. 配置示例
Java
XML
@EnableMongoRepositories(repositoryImplementationPostfix = "MyPostfix")
class Configuration { … }
前面例子中的第一个配置试图查找一个叫做 com.acme.repository.CustomizedUserRepositoryImpl 的类,作为一个自定义的 repository 实现。第二个例子试图查找 com.acme.repository.CustomizedUserRepositoryMyPostfix。
如果在不同的包中发现了具有匹配类名的多个实现,Spring Data会使用Bean名称来确定使用哪一个。
考虑到前面显示的 CustomizedUserRepository 的以下两个自定义实现,第一个实现被使用。它的Bean名是 customedUserRepositoryImpl,与片段接口(CustomizedUserRepository)加后缀 Impl 的名字一致。
Example 37. 解决模棱两可的实现
package com.acme.impl.one;
class CustomizedUserRepositoryImpl implements CustomizedUserRepository {
// Your custom implementation
}
package com.acme.impl.two;
@Component("specialCustomImpl")
class CustomizedUserRepositoryImpl implements CustomizedUserRepository {
// Your custom implementation
}
If you annotate the UserRepository interface with @Component("specialCustom"), the bean name plus Impl then matches the one defined for the repository implementation in com.acme.impl.two, and it is used instead of the first one.
如果你用 @Component("specialCustom") 来注解 UserRepository 接口,那么Bean名加 Impl 就会与 com.acme.impl.two 中为repository实现定义的豆名相匹配,并被用来替代第一个豆名。
如果你的自定义实现只使用基于注解的配置和自动注入,前面所示的方法很好用,因为它被当作任何其他Spring Bean。如果你的实现片段Bean需要特殊的注入,你可以根据前文所述的约定来声明Bean并为其命名。然后,基础设施通过名称来引用手动定义的Bean定义,而不是自己创建一个。下面的例子展示了如何手动注入一个自定义的实现。
Example 38. 手动注入一个自定义实现
Java
XML
class MyClass {
MyClass(@Qualifier("userRepositoryImpl") UserRepository userRepository) {
…
}
}
当你想定制基础 repository 的行为时,上一节描述的方法需要定制每个 repository 的接口,以便所有的 repository 都受到影响。为了改变所有 repository 的行为,你可以创建一个扩展持久化技术特定 repository 基类的实现。然后这个类作为 repository 代理的自定义基类,如下面的例子所示。
Example 39. 自定义 repository base 类
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
}
}
该类需要有一个super类的构造器,store特定的 repository factory 实现使用该构造器。如果repository 基类有多个构造函数,请复写其中一个构造函数,该构造函数需要一个 EntityInformation 和一个store特定的基础设施对象(如 EntityManager 或模板类)。 |
最后一步是让Spring Data基础设施意识到定制的 repository base 类。在配置中,你可以通过使用 repositoryBaseClass 来做到这一点,如下面的例子所示。
Example 40. 配置一个自定义 repository base 类
Java
XML
@Configuration
@EnableMongoRepositories(repositoryBaseClass = MyRepositoryImpl.class)
class ApplicationConfiguration { … }
由 Repository 管理的实体是 aggregate root。在领域驱动设计应用程序中,这些aggregate root通常会发布 domain 事件。Spring Data提供了一个名为 @DomainEvents 的注解,你可以在 aggregate root 的一个方法上使用该注解,以使这种发布尽可能地简单,如下例所示。
Example 41. 从aggregate root中暴露domain 事件
class AnAggregateRoot {
@DomainEvents
Collection
使用 @DomainEvents 的方法可以返回一个单一的事件实例或一个事件的集合。它必须不接受任何参数。 |
|
在所有的事件都被发布后,我们有一个用 @AfterDomainEventPublication 注解的方法。你可以用它来潜在地清理要发布的事件列表(除其他用途外)。 |
每次调用Spring Data Repository的 save(…)、saveAll(…)、delete(…) 或 deleteAll(…) 方法时都会调用这些方法。
本节记录了一组Spring Data扩展,这些扩展使Spring Data能够在各种情况下使用。目前,大部分的集成都是针对Spring MVC的。
Querydsl 是一个框架,可以通过其 fluent API构建静态类型的类似SQL的查询。
一些Spring Data模块通过 QuerydslPredicateExecutor 提供与 Querydsl 的集成,正如下面的例子所示。
Example 42. QuerydslPredicateExecutor interface
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 43. Repository 上的 Querydsl 整合
interface UserRepository extends CrudRepository, QuerydslPredicateExecutor {
}
前面的例子让你通过使用 Querydsl Predicate 实例来编写类型安全的查询,如下图所示。
Predicate predicate = user.firstname.equalsIgnoreCase("dave")
.and(user.lastname.startsWithIgnoreCase("mathews"));
userRepository.findAll(predicate);
支持 repository 编程模型的Spring Data模块带有各种Web支持。web相关的组件需要添加 Spring MVC 到项目。其中一些甚至提供了与 Spring HATEOAS的整合。一般来说,集成支持是通过在你的 JavaConfig 配置类中使用 @EnableSpringDataWebSupport 注解来启用的,如下面例子所示。
Example 44. 启用 Spring Data web 支持
Java
XML
@Configuration
@EnableWebMvc
@EnableSpringDataWebSupport
class WebConfiguration {}
@EnableSpringDataWebSupport 注解注册了一些组件。我们将在本节后面讨论这些组件。它还会检测classpath上的Spring HATEOAS,并为其注册整合组件(如果存在)。
在XML中启用Spring Data Web支持
上一节所示的配置注册了一些基本组件。
DomainClassConverter 类让你在Spring MVC Controller 方法签名中直接使用 domain 类型,这样你就不需要通过 repository 手动查找实例了,如下例所示。
Example 45. 一个在方法签名中使用 domain 类型的Spring MVC controller
@Controller
@RequestMapping("/users")
class UserController {
@RequestMapping("/{id}")
String showUserForm(@PathVariable("id") User user, Model model) {
model.addAttribute("user", user);
return "userForm";
}
}
该方法直接接收一个 User 实例,而不需要进一步的查找。该实例可以通过让Spring MVC先将路径变量转换为domain类的 id 类型来解决,最终通过调用为domain类注册的资源库实例 findById(…) 来访问该实例。
目前,repository 必须实现 CrudRepository 才有资格被发现进行转换。 |
上一节 中的配置片段还注册了一个 PageableHandlerMethodArgumentResolver 以及一个 SortHandlerMethodArgumentResolver 的实例。注册后,Pageable 和 Sort 可以作为有效的controller方法参数,如下图所示。
Example 46. 使用 Pageable 作为 controller 方法参数
@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)(,IgnoreCase) 进行排序的属性。默认的排序方向是对大小写敏感的升序。如果你想切换方向或大小写敏感性,请使用多个排序参数—例如,?sort=firstname&sort=lastname,asc&sort=city,ignorecase。 |
要自定义这种行为,请注册一个分别实现 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 注解来定制它。
Spring HATEOAS提供了一个表示 model 类(PagedResources),它允许用必要的页面元数据以及链接来丰富 Page 实例的内容,让客户轻松地浏览页面。Page 到 PagedResources 的转换是由Spring HATEOAS ResourceAssembler 接口的实现完成的,这个接口被称为 PagedResourcesAssembler。下面的例子展示了如何使用 PagedResourcesAssembler 作为 controller 方法的参数。
Example 47. 使用 PagedResourcesAssembler 作为 controller 方法参数
@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 被用作控制器方法的参数。对它调用 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
}
}
assembler 产生了正确的URI,并且还拾取了默认的配置,以便为即将到来的请求将参数解析为一个 Pageable。这意味着,如果你改变了配置,链接会自动遵守这一变化。默认情况下,assembler 会指向它被调用的controller方法,但你可以通过传递一个自定义的 Link 来定制,作为建立分页链接的基础,它重载了 PagedResourcesAssembler.toResource(..) 方法。
核心模块和一些特定的存储模块与一组Jackson模块一起发布,用于Spring Data domain 域使用的类型,如 org.springframework.data.geo.Distance 和 org.springframework.data.geo.Point。 一旦启用 web支持 和 com.fasterxml.jackson.databind.ObjectMapper 可用,就会导入这些模块。
在初始化过程中,SpringDataJacksonModules 和 SpringDataJacksonConfiguration 一样,被基础设施所接收,这样,声明的 com.fasterxml.jackson.databind.Module 就被提供给Jackson ObjectMapper。
以下domain类型的 Data binding mixins 由公共基础设施注册。
org.springframework.data.geo.Distance
org.springframework.data.geo.Point
org.springframework.data.geo.Box
org.springframework.data.geo.Circle
org.springframework.data.geo.Polygon
单个模块可以提供额外的 SpringDataJacksonModules。更多细节请参考商店的具体章节。 |
你可以通过使用 JSONPath 表达式(需要 Jayway JsonPath)或 XPath 表达式(需要 XmlBeam)来使用 Spring Data 投影(在 投影 中描述)来绑定传入的请求的payload,如下例所示。
Example 48. 使用JSONPath 或 XPath 表达式来绑定HTTP payload
@ProjectedPayload
public interface UserPayload {
@XBRead("//firstname")
@JsonPath("$..firstname")
String getFirstname();
@XBRead("/lastname")
@JsonPath({ "$.lastname", "$.user.lastname" })
String getLastname();
}
你可以将前面的例子中显示的类型作为Spring MVC controller 的方法参数,或者通过在 RestTemplate 的某个方法中使用 ParameterizedTypeReference。前面的方法声明将尝试在给定 document 中的任何地方找到 firstname。lastname 的XML查找是在传入 document 的顶层进行的。JSON的变体首先尝试顶层的 lastname,但是如果前者没有返回一个值,也会尝试嵌套在 user 子 document 中的 lastname。这样,源 document 结构的变化可以很容易地被减轻,而不需要客户端调用暴露的方法(通常是基于类的 payload 绑定的一个缺点)。
如 投影 中所述,支持嵌套投影。如果该方法返回一个复杂的、非接口类型,则使用Jackson ObjectMapper 来映射最终值。
对于Spring MVC,一旦 @EnableSpringDataWebSupport 被激活,并且classpath上有必要的依赖,必要的 converter 就会被自动注册。对于 RestTemplate 的使用,需要手动注册一个 ProjectingJackson2HttpMessageConverter(JSON)或 XmlBeamHttpMessageConverter。
欲了解更多信息,请参见 Spring Data 示例库 中的 web投影示例。
对于那些有 QueryDSL 集成的 store,你可以从 Request 查询字符串中包含的属性导出查询。
考虑下面这个查询字符串:
?firstname=Dave&lastname=Matthews
给出前面例子中的 User 对象,你可以通过使用 QuerydslPredicateArgumentResolver 将一个查询字符串解析为以下值,如下所示。
QUser.user.firstname.eq("Dave").and(QUser.user.lastname.eq("Matthews"))
当在 classpath 上发现 Querydsl 时,该功能与 @EnableSpringDataWebSupport 一起被自动启用。 |
在方法签名中添加 @QuerydslPredicate 提供了一个随时可用的 Predicate,你可以通过使用 QuerydslPredicateExecutor 来运行它。
类型信息通常是由方法的返回类型来解决的。由于该信息不一定与 domain 类型相匹配,使用 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";
}
}
将查询字符串参数解析为 User 的匹配 Predicate。 |
默认的绑定方式如下。
你可以通过 @QuerydslPredicate 的 bindings 属性或者利用Java 8的 default methods 来定制这些绑定,并将 QuerydslBinderCustomizer 方法添加到 repository 接口,如下所示。
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 的特定查找方法的访问。 |
|
repository 接口上定义的 QuerydslBinderCustomizer 被自动拾取,并成为 @QuerydslPredicate(bindings=…) 的快捷方式。 |
|
定义 username 属性的绑定是一个简单的 contains 绑定。 |
|
定义 String 属性的默认绑定为不区分大小写的 contains 匹配。 |
|
将 password 属性排除在 Predicate 解析之外。 |
你可以在应用来自 repository 或 @QuerydslPredicate 的特定绑定之前,注册一个持有默认Querydsl绑定的 QuerydslBinderCustomizerDefaults bean。 |
如果你使用Spring JDBC模块,你可能很熟悉对用SQL脚本填充 DataSource 的支持。在 repository 层面也有类似的抽象,尽管它不使用SQL作为数据定义语言,因为它必须是独立于store的。因此,填充器支持XML(通过Spring的OXM抽象)和JSON(通过Jackson)来定义数据,用它来填充repository。
假设你有一个名为 data.json 的文件,内容如下。
Example 49. 在JSON中定义的数据
[ { "_class" : "com.acme.Person",
"firstname" : "Dave",
"lastname" : "Matthews" },
{ "_class" : "com.acme.Person",
"firstname" : "Carter",
"lastname" : "Beauford" } ]
你可以通过使用Spring Data Commons中提供的 Repository 命名空间的populator元素来填充你的Repository。为了将前面的数据填充到你的 PersonRepository 中,声明一个类似于下面的 populator。
Example 50. 声明一个 Jackson repository populator
前面的声明导致 data.json 文件被 Jackson ObjectMapper 读取和反序列化。
JSON对象被反序列化的类型是通过检查JSON文档的 _class 属性决定的。基础设施最终会选择适当的 repository 来处理被反序列化的对象。
为了使用XML来定义 repository 应该填充的数据,你可以使用 unmarshaller-populator 元素。你把它配置为使用Spring OXM中的一个 XML marshaller 选项。详情请参见 Spring参考文档。下面的例子展示了如何用JAXB来 unmarshall 对 repository 填充器的 marshall。
下面的例子显示了如何用 JAXB 来 unmarshall 一个 repository 填充器(populator)。
Example 51. 声明一个 unmarshalling repository populator(使用JAXB)。