2. 使用 Spring Data 仓库
Spring Data 仓库抽象的目标是显著减少为各种持久化存储实现数据访问层所需的样板代码量。
|
Spring Data 仓库文档与您的模块 |
2.1. 核心概念
Spring Data 仓库抽象中的核心接口是Repository。
它接受要管理的领域类以及该领域类的标识符类型作为类型参数。
此接口主要作为一个标记接口,用于捕获要处理的类型,并帮助您发现扩展此接口的其他接口。
CrudRepository 接口为被管理的实体类提供了复杂的 CRUD 功能。
CrudRepository 接口public interface CrudRepository<T, ID> extends Repository<T, ID> {
<S extends T> S save(S entity); (1)
Optional<T> findById(ID primaryKey); (2)
Iterable<T> findAll(); (3)
long count(); (4)
void delete(T entity); (5)
boolean existsById(ID primaryKey); (6)
// … more functionality omitted.
}
| 1 | 保存给定的实体。 |
| 2 | 返回由给定 ID 标识的实体。 |
| 3 | 返回所有实体。 |
| 4 | 返回实体的数量。 |
| 5 | 删除给定的实体。 |
| 6 | 指示具有给定ID的实体是否存在。 |
此接口中声明的方法通常称为 CRUD 方法。
我们还提供了特定于持久化技术的抽象接口,例如 JpaRepository 或 MongoRepository。
这些接口扩展了 CrudRepository,除了提供像 CrudRepository 这样较为通用的、与具体持久化技术无关的接口之外,还暴露了底层持久化技术自身的能力。 |
在 CrudRepository 之上,有一个 PagingAndSortingRepository 抽象层,它添加了额外的方法以简化对实体的分页访问:
PagingAndSortingRepository接口public interface PagingAndSortingRepository<T, ID> extends CrudRepository<T, ID> {
Iterable<T> findAll(Sort sort);
Page<T> findAll(Pageable pageable);
}
要以每页 20 条的分页大小访问 User 的第二页,你可以执行类似如下的操作:
PagingAndSortingRepository<User, Long> repository = // … get access to a bean
Page<User> users = repository.findAll(PageRequest.of(1, 20));
除了查询方法之外,还可以为计数(count)和删除(delete)查询生成派生查询。 以下列表展示了派生计数查询的接口定义:
interface UserRepository extends CrudRepository<User, Long> {
long countByLastname(String lastname);
}
以下代码清单展示了派生删除查询的接口定义:
interface UserRepository extends CrudRepository<User, Long> {
long deleteByLastname(String lastname);
List<User> removeByLastname(String lastname);
}
2.2. 查询方法
标准的 CRUD 功能仓库通常会对底层数据存储执行查询。 在 Spring Data 中,声明这些查询是一个四步过程:
-
声明一个接口,该接口需继承 Repository 或其某个子接口,并将其泛型化为应处理的领域类和 ID 类型,如下例所示:
interface PersonRepository extends Repository<Person, Long> { … } -
在接口上声明查询方法。
interface PersonRepository extends Repository<Person, Long> { List<Person> findByLastname(String lastname); } -
设置 Spring 以通过 JavaConfig 或 XML 配置 为这些接口创建代理实例。
-
要使用 Java 配置,请创建一个类似于以下的类:
@EnableJpaRepositories class Config { … } -
要使用 XML 配置,请定义一个类似于以下的 bean:
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:jpa="http://www.springframework.org/schema/data/jpa" xsi:schemaLocation="http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/data/jpa https://www.springframework.org/schema/data/jpa/spring-jpa.xsd"> <jpa:repositories base-package="com.acme.repositories"/> </beans>本示例中使用了 JPA 命名空间。 如果您对其他任何存储使用仓库抽象,则需要将其更改为对应存储模块的适当命名空间声明。 换句话说,您应将
jpa替换为例如mongodb。此外,请注意,JavaConfig 变体没有显式配置包,因为默认使用带注解类的包。 要自定义要扫描的包,请使用特定于数据存储的仓库的
@Enable${store}Repositories注解的其中一个basePackage…属性。
-
-
注入仓库实例并使用它,如下例所示:
class SomeClient { private final PersonRepository repository; SomeClient(PersonRepository repository) { this.repository = repository; } void doSomething() { List<Person> persons = repository.findByLastname("Matthews"); } }
以下各节将详细解释每个步骤:
2.3. 定义仓库接口
要定义一个仓库接口,您首先需要定义一个特定于领域类的仓库接口。
该接口必须继承 Repository 并针对领域类和 ID 类型进行泛型化。
如果您希望为该领域类型暴露 CRUD 方法,请继承 CrudRepository 而不是 Repository。
2.3.1. 微调存储库定义
通常,您的仓库接口会扩展 Repository、CrudRepository 或 PagingAndSortingRepository。
或者,如果您不想扩展 Spring Data 接口,也可以使用 @RepositoryDefinition 注解您的仓库接口。
扩展 CrudRepository 将暴露一整套用于操作实体的方法。
如果您希望有选择地暴露某些方法,可以将 CrudRepository 中您想要暴露的方法复制到您的领域仓库中。
| 这样做可以让您在提供的 Spring Data Repositories 功能之上定义自己的抽象。 |
以下示例展示了如何有选择地暴露 CRUD 方法(在本例中为 findById 和 save):
@NoRepositoryBean
interface MyBaseRepository<T, ID> extends Repository<T, ID> {
Optional<T> findById(ID id);
<S extends T> S save(S entity);
}
interface UserRepository extends MyBaseRepository<User, Long> {
User findByEmailAddress(EmailAddress emailAddress);
}
在前面的示例中,您为所有领域仓库定义了一个通用的基础接口,并暴露了 findById(…) 和 save(…) 方法。这些方法会被路由到 Spring Data 所提供的、您所选存储技术对应的基础仓库实现中(例如,如果您使用 JPA,则实现类为 SimpleJpaRepository),因为它们与 CrudRepository 中的方法签名相匹配。
因此,UserRepository 现在可以保存用户、通过 ID 查找单个用户,并触发查询以通过电子邮件地址查找 Users。
中间的仓库接口使用 @NoRepositoryBean 注解进行标注。
请确保将该注解添加到所有 Spring Data 在运行时不应创建实例的仓库接口上。 |
2.3.2. 在多个 Spring Data 模块中使用仓库
在应用程序中使用唯一的 Spring Data 模块会使事情变得简单,因为定义范围内的所有仓库接口都会绑定到该 Spring Data 模块。 有时,应用程序需要使用多个 Spring Data 模块。 在这种情况下,仓库定义必须区分不同的持久化技术。 当 Spring Data 在类路径上检测到多个仓库工厂时,会进入严格的仓库配置模式。 严格配置会利用仓库或领域类的详细信息,来决定仓库定义应绑定到哪个 Spring Data 模块:
-
如果仓库定义扩展了特定模块的仓库,则它是该特定 Spring Data 模块的有效候选者。
-
如果领域类使用了特定于模块的类型注解进行标注,那么它就是对应 Spring Data 模块的有效候选。 Spring Data 模块既接受第三方注解(例如 JPA 的
@Entity),也提供自己的注解(例如用于 Spring Data MongoDB 和 Spring Data Elasticsearch 的@Document)。
以下示例展示了一个使用模块特定接口(本例中为 JPA)的仓库:
interface MyRepository extends JpaRepository<User, Long> { }
@NoRepositoryBean
interface MyBaseRepository<T, ID> extends JpaRepository<T, ID> { … }
interface UserRepository extends MyBaseRepository<User, Long> { … }
MyRepository 和 UserRepository 在其类型层次结构中继承了 JpaRepository。
它们是 Spring Data JPA 模块的有效候选者。
以下示例展示了一个使用泛型接口的仓库:
interface AmbiguousRepository extends Repository<User, Long> { … }
@NoRepositoryBean
interface MyBaseRepository<T, ID> extends CrudRepository<T, ID> { … }
interface AmbiguousUserRepository extends MyBaseRepository<User, Long> { … }
AmbiguousRepository 和 AmbiguousUserRepository 在其类型层次结构中仅继承了 Repository 和 CrudRepository。
虽然在使用唯一的 Spring Data 模块时这样做没有问题,但在存在多个模块的情况下,无法区分这些仓库应绑定到哪个特定的 Spring Data 模块。
以下示例展示了一个使用带注解的领域类的仓库:
interface PersonRepository extends Repository<Person, Long> { … }
@Entity
class Person { … }
interface UserRepository extends Repository<User, Long> { … }
@Document
class User { … }
PersonRepository 引用了带有 JPA Person 注解的 @Entity,因此该仓库显然属于 Spring Data JPA。UserRepository 引用了带有 Spring Data MongoDB 的 User 注解的 @Document。
以下不良示例展示了一个使用带有混合注解的领域类的仓库:
interface JpaPersonRepository extends Repository<Person, Long> { … }
interface MongoDBPersonRepository extends Repository<Person, Long> { … }
@Entity
@Document
class Person { … }
此示例展示了一个同时使用 JPA 和 Spring Data MongoDB 注解的领域类。
它定义了两个仓库:JpaPersonRepository 和 MongoDBPersonRepository。
其中一个用于 JPA,另一个用于 MongoDB。
Spring Data 无法再区分这两个仓库,从而导致未定义的行为。
仓库类型详情 和 区分领域类注解 用于严格的仓库配置,以识别特定 Spring Data 模块的仓库候选者。 在同一领域类型上使用多种持久化技术特定的注解是可行的,并能实现领域类型在多种持久化技术之间的复用。 然而,Spring Data 将无法再确定一个唯一的模块来绑定该仓库。
区分存储库的最后一种方法是限定存储库基础包的范围。 基础包定义了扫描存储库接口定义的起点,这意味着存储库定义必须位于相应的包中。 默认情况下,基于注解的配置使用配置类所在的包。 基于 XML 配置的基础包 是必需的。
以下示例展示了基于注解驱动的基础包配置:
@EnableJpaRepositories(basePackages = "com.acme.repositories.jpa")
@EnableMongoRepositories(basePackages = "com.acme.repositories.mongo")
class Configuration { … }
2.4. 定义查询方法
仓库代理有两种方式根据方法名派生出特定于存储的查询:
-
通过直接从方法名派生查询。
-
通过使用手动定义的查询。
可用的选项取决于实际使用的存储。 然而,必须有一种策略来决定实际生成的查询是什么。 下一节将介绍可用的选项。
2.4.1. 查询查找策略
以下策略可供仓库基础设施用于解析查询。
使用 XML 配置时,可以通过命名空间的 query-lookup-strategy 属性来配置该策略。
对于 Java 配置,可以使用 queryLookupStrategy 注解的 Enable${store}Repositories 属性。
某些策略可能不被特定的数据存储所支持。
-
CREATE尝试根据查询方法名构造一个特定于存储的查询。 通常的做法是从方法名中移除一组已知的前缀,然后解析方法名的其余部分。 您可以在“查询创建”中了解更多关于查询构造的信息。 -
USE_DECLARED_QUERY尝试查找一个已声明的查询,如果找不到则抛出异常。 该查询可以通过某个注解定义,或通过其他方式声明。 请参阅特定存储的文档,以了解该存储支持的可用选项。 如果在启动时仓库基础设施未能为该方法找到已声明的查询,则会启动失败。 -
CREATE_IF_NOT_FOUND(默认值)结合了CREATE和USE_DECLARED_QUERY。 它首先查找已声明的查询,如果未找到已声明的查询,则会基于方法名称创建一个自定义查询。 这是默认的查询查找策略,因此在您未显式配置任何内容时将使用该策略。 它允许通过方法名快速定义查询,同时也可以根据需要引入已声明的查询来对这些查询进行自定义调整。
2.4.2. 查询创建
Spring Data 仓库基础设施内置的查询构建器机制,适用于为仓库中的实体构建带约束条件的查询。
以下示例展示了如何创建多个查询:
interface PersonRepository extends Repository<Person, Long> {
List<Person> findByEmailAddressAndLastname(EmailAddress emailAddress, String lastname);
// Enables the distinct flag for the query
List<Person> findDistinctPeopleByLastnameOrFirstname(String lastname, String firstname);
List<Person> findPeopleDistinctByLastnameOrFirstname(String lastname, String firstname);
// Enabling ignoring case for an individual property
List<Person> findByLastnameIgnoreCase(String lastname);
// Enabling ignoring case for all suitable properties
List<Person> findByLastnameAndFirstnameAllIgnoreCase(String lastname, String firstname);
// Enabling static ORDER BY for a query
List<Person> findByLastnameOrderByFirstnameAsc(String lastname);
List<Person> findByLastnameOrderByFirstnameDesc(String lastname);
}
解析查询方法名分为主体和谓词。
第一部分(find…By、exists…By)定义查询的主体,第二部分构成谓词。
引入子句(主体)可以包含更多表达式。
find(或其他引入关键字)与 By 之间的任何文本均被视为描述性内容,除非使用了结果限制关键字,例如 Distinct 用于在要创建的查询上设置 distinct 标志,或 Top/First 用于限制查询结果。
附录包含 查询方法主题关键字的完整列表 和 查询方法谓词关键字(包括排序和字母大小写修饰符)。
然而,第一个 By 作为分隔符,用于指示实际条件谓词的开始。
在最基本的层面上,您可以定义实体属性的条件,并使用 And 和 Or 将它们连接起来。
解析该方法的实际结果取决于您为其创建查询的持久化存储。 然而,有一些通用事项需要注意:
-
表达式通常是属性遍历与可串联的操作符组合而成。 你可以使用
AND和OR将属性表达式组合起来。 你还可以在属性表达式中使用诸如Between、LessThan、GreaterThan和Like等操作符。 所支持的操作符可能因数据存储而异,因此请查阅参考文档中的相应部分。 -
方法解析器支持为单个属性设置
IgnoreCase标志(例如,findByLastnameIgnoreCase(…)),或者为所有支持忽略大小写的属性类型(通常是String类型的实例)设置该标志(例如,findByLastnameAndFirstnameAllIgnoreCase(…))。 是否支持忽略大小写可能因数据存储而异,因此请查阅参考文档中与特定存储相关的查询方法部分。 -
您可以通过在引用属性的查询方法后附加一个
OrderBy子句,并提供排序方向(Asc或Desc)来应用静态排序。 要创建支持动态排序的查询方法,请参阅"特殊参数处理"。
2.4.3. 属性表达式
属性表达式只能引用被管理实体的直接属性,如前面示例所示。 在查询创建时,您已经确保所解析的属性是被管理领域类的一个属性。 然而,您也可以通过遍历嵌套属性来定义约束条件。 请考虑以下方法签名:
List<Person> findByAddressZipCode(ZipCode zipCode);
假设一个 Person 通过一个 ZipCode 与一个 Address 相关联。
在这种情况下,该方法会创建 x.address.zipCode 属性遍历。
解析算法首先将整个部分(AddressZipCode)解释为属性,并检查域类中是否存在具有该名称(首字母小写)的属性。
如果算法成功,它将使用该属性。
如果失败,算法将从右侧按驼峰命名法将源字符串拆分为头部和尾部,并尝试查找对应的属性——在我们的示例中是 AddressZip 和 Code。
如果算法找到匹配头部的属性,它将使用尾部并从此处继续向下构建树结构,按照上述方式进一步拆分尾部。
如果第一次拆分不匹配,算法会将拆分点向左移动(Address、ZipCode)并继续执行。
尽管这在大多数情况下都能正常工作,但该算法仍有可能选择错误的属性。
假设 Person 类还有一个 addressZip 属性。
该算法会在第一轮分割时就进行匹配,从而选中错误的属性并导致失败(因为 addressZip 的类型很可能没有 code 属性)。
为了解决这种歧义,您可以在方法名中使用 _ 来手动定义遍历点。
因此,我们的方法名将如下所示:
List<Person> findByAddress_ZipCode(ZipCode zipCode);
由于我们将下划线字符视为保留字符,因此强烈建议遵循标准的 Java 命名约定(即:属性名中不使用下划线,而采用驼峰式命名)。
2.4.4. 特殊参数处理
要在查询中处理参数,请像前面的示例中那样定义方法参数。
此外,基础设施会识别某些特定类型(如 Pageable 和 Sort),以便动态地为您的查询应用分页和排序功能。
以下示例演示了这些特性:
Pageable、Slice 和 SortPage<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);
接受 Sort 和 Pageable 的 API 期望将非 null 的值传入方法。
如果您不想应用任何排序或分页,请使用 Sort.unsorted() 和 Pageable.unpaged()。 |
第一种方法允许你将一个 org.springframework.data.domain.Pageable 实例传递给查询方法,从而为静态定义的查询动态添加分页功能。
Page 能够获知可用元素和页面的总数。
这是通过底层框架触发一次 count 查询来计算总数量实现的。
由于这可能开销较大(取决于所使用的存储),你可以改为返回一个 Slice。
Slice 仅知道是否存在下一个 Slice,在遍历大型结果集时,这可能就已足够。
排序选项也通过 Pageable 实例进行处理。
如果你只需要排序,可以在方法中添加一个 org.springframework.data.domain.Sort 参数。
如你所见,返回 List 也是可行的。
在这种情况下,不会创建构建实际 Page 实例所需的额外元数据(这意味着原本必需的额外计数查询也不会被执行)。
而是仅限制查询以检索指定范围内的实体。
| 要了解整个查询会返回多少页,您需要触发一个额外的计数查询。 默认情况下,该查询是从您实际触发的查询中派生而来的。 |
分页与排序
你可以通过使用属性名称来定义简单的排序表达式。 你可以将多个表达式连接起来,将多个排序条件合并为一个表达式。
Sort sort = Sort.by("firstname").ascending()
.and(Sort.by("lastname").descending());
为了以更类型安全的方式定义排序表达式,请从要定义排序表达式的类型开始,并使用方法引用指定用于排序的属性。
TypedSort<Person> person = Sort.sort(Person.class);
Sort sort = person.by(Person::getFirstname).ascending()
.and(person.by(Person::getLastname).descending());
TypedSort.by(…) 通常通过使用 CGlib 等工具在运行时创建代理,这在使用 Graal VM Native 等工具进行原生镜像编译时可能会产生干扰。 |
如果你的存储库实现支持 Querydsl,你也可以使用生成的元模型类型来定义排序表达式:
QSort sort = QSort.by(QPerson.firstname.asc())
.and(QSort.by(QPerson.lastname.desc()));
2.4.5. 限制查询结果
您可以通过使用 first 或 top 关键字来限制查询方法的结果,这两个关键字可以互换使用。
您可以在 first 或 top 后附加一个可选的数值,以指定要返回的最大结果数量。
如果省略该数值,则默认结果数量为 1。
以下示例展示了如何限制查询结果的数量:
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”个最大元素的查询方法。 |
2.4.6. 返回集合或可迭代对象的仓库方法
返回多个结果的查询方法可以使用标准的 Java Iterable、List 和 Set。
除此之外,我们还支持返回 Spring Data 的 Streamable(它是 Iterable 的自定义扩展),以及由 Vavr 提供的集合类型。
请参阅附录,了解所有可能的 查询方法返回类型。
将 Streamable 用作查询方法的返回类型
您可以使用 Streamable 作为 Iterable 或任何集合类型的替代方案。
它提供了便捷方法来访问非并行的 Stream(Iterable 所不具备的功能),并支持直接对元素进行 ….filter(…) 和 ….map(…) 操作,以及将 Streamable 与其他流进行拼接:
interface PersonRepository extends Repository<Person, Long> {
Streamable<Person> findByFirstnameContaining(String firstname);
Streamable<Person> findByLastnameContaining(String lastname);
}
Streamable<Person> result = repository.findByFirstnameContaining("av")
.and(repository.findByLastnameContaining("ea"));
返回自定义可流式包装类型
为集合提供专用的包装类型是一种常用模式,用于提供返回多个元素的查询结果的 API。 通常,这些类型通过调用返回类集合类型的仓库方法,并手动创建包装类型的实例来使用。 如果这些包装类型满足以下条件,你可以省去这一额外步骤,因为 Spring Data 允许你直接将这些包装类型用作查询方法的返回类型:
-
该类型实现了
Streamable。 -
该类型公开了一个构造函数或一个名为
of(…)或valueOf(…)的静态工厂方法,该方法接受Streamable作为参数。
以下列表展示了一个示例:
class Product { (1)
MonetaryAmount getPrice() { … }
}
@RequiredArgsConstructor(staticName = "of")
class Products implements Streamable<Product> { (2)
private final Streamable<Product> streamable;
public MonetaryAmount getTotal() { (3)
return streamable.stream()
.map(Priced::getPrice)
.reduce(Money.of(0), MonetaryAmount::add);
}
@Override
public Iterator<Product> iterator() { (4)
return streamable.iterator();
}
}
interface ProductRepository implements Repository<Product, Long> {
Products findAllByDescriptionContaining(String text); (5)
}
| 1 | 一个 Product 实体,用于暴露访问产品价格的 API。 |
| 2 | 一个用于 Streamable<Product> 的包装类型,可通过使用 Products.of(…)(使用 Lombok 注解创建的工厂方法)来构造。
当然,也可以直接使用接受 Streamable<Product> 参数的标准构造函数。 |
| 3 | 包装类型暴露了一个额外的 API,用于在 Streamable<Product> 上计算新值。 |
| 4 | 实现 Streamable 接口并将调用委托给实际的结果。 |
| 5 | 该包装类型 Products 可直接用作查询方法的返回类型。
您无需返回 Streamable<Product> 并在仓库客户端中手动对其进行包装。 |
支持 Vavr 集合
Vavr 是一个在 Java 中拥抱函数式编程概念的库。 它附带了一套自定义的集合类型,你可以将其用作查询方法的返回类型,如下表所示:
| Vavr 集合类型 | 使用的 Vavr 实现类型 | 有效的 Java 源代码类型 |
|---|---|---|
|
|
|
|
|
|
|
|
|
您可以将第一列中的类型(或其子类型)用作查询方法的返回类型,系统会根据实际查询结果的 Java 类型(第三列),使用第二列中对应的类型作为实现类型。
或者,您也可以声明 Traversable(即 Vavr 中与 Iterable 等价的类型),此时我们会根据实际返回值推导出具体的实现类。
例如,java.util.List 会被转换为 Vavr 的 List 或 Seq,java.util.Set 则会变成 Vavr 的 LinkedHashSet Set,依此类推。
2.4.7. 仓库方法的空值处理
从 Spring Data 2.0 开始,返回单个聚合实例的仓库 CRUD 方法使用 Java 8 的 Optional 来表示值可能不存在的情况。
除此之外,Spring Data 还支持在查询方法中返回以下包装类型:
-
com.google.common.base.Optional -
scala.Option -
io.vavr.control.Option
或者,查询方法可以选择不使用任何包装类型。
如果没有查询结果,则通过返回 null 来表示。
返回集合、集合替代类型、包装类型或流的 Repository 方法保证永远不会返回 null,而是返回相应的空表示形式。
详细信息请参阅
可空性注解
你可以通过使用Spring Framework 的可空性注解来为仓库方法表达可空性约束。
它们提供了一种对工具友好的方式,并在运行时选择性地进行 null 检查,如下所示:
-
@NonNullApi: 在包级别使用,用于声明参数和返回值的默认行为分别是不接受也不产生null值。 -
@NonNull:用于必须不为null的参数或返回值(在适用@NonNullApi的参数和返回值上不需要)。 -
@Nullable: 用于可以null的参数或返回值。
Spring 注解使用 JSR 305 注解(一项虽已停滞但被广泛使用的 JSR)进行了元注解。
JSR 305 元注解使工具提供商(如 IDEA、Eclipse 和 Kotlin)能够以通用方式提供空安全支持,而无需对 Spring 注解进行硬编码支持。
要为查询方法启用可空性约束的运行时检查,您需要在包级别通过在 @NonNullApi 中使用 Spring 的 package-info.java 来激活非空性,如下例所示:
package-info.java 中声明非空性@org.springframework.lang.NonNullApi
package com.acme;
一旦启用了非空默认设置,存储库查询方法的调用将在运行时根据可空性约束进行验证。
如果查询结果违反了所定义的约束,则会抛出异常。
这种情况发生在方法本应返回 null,但被声明为非空(即在存储库所在包上定义的注解所指定的默认行为)时。
如果您希望再次允许某些方法返回可空结果,请在个别方法上选择性地使用 @Nullable 注解。
本节开头提到的结果包装类型仍按预期正常工作:空结果将被转换为代表“缺失”的值。
以下示例展示了上述多种技术:
package com.acme; (1)
interface UserRepository extends Repository<User, Long> {
User getByEmailAddress(EmailAddress emailAddress); (2)
@Nullable
User findByEmailAddress(@Nullable EmailAddress emailAdress); (3)
Optional<User> findOptionalByEmailAddress(EmailAddress emailAddress); (4)
}
| 1 | 该仓库位于一个我们已定义非空行为的包(或子包)中。 |
| 2 | 当查询未产生结果时,抛出 EmptyResultDataAccessException。
当传递给该方法的 IllegalArgumentException 为 emailAddress 时,抛出 null。 |
| 3 | 当查询未产生结果时,返回 null。
同时也接受 null 作为 emailAddress 的值。 |
| 4 | 当查询未产生结果时,返回 Optional.empty()。
当传入方法的 IllegalArgumentException 为 emailAddress 时,抛出 null。 |
基于 Kotlin 的仓库中的可空性
Kotlin 在语言层面内置了可空性约束的定义。
Kotlin 代码被编译为字节码,该字节码不会通过方法签名来表达可空性约束,而是通过编译进来的元数据来实现。
请确保在项目中包含 kotlin-reflect JAR,以启用对 Kotlin 可空性约束的内省。
Spring Data 仓库利用该语言机制来定义这些约束,从而执行相同的运行时检查,如下所示:
interface UserRepository : Repository<User, String> {
fun findByUsername(username: String): User (1)
fun findByFirstname(firstname: String?): User? (2)
}
| 1 | 该方法将参数和返回值都定义为不可为空(Kotlin 的默认行为)。
Kotlin 编译器会拒绝向该方法传入 null 的调用。
如果查询结果为空,则会抛出 EmptyResultDataAccessException 异常。 |
| 2 | 此方法接受 null 作为 firstname 参数的值,并且如果查询未产生结果,则返回 null。 |
2.4.8. 流式查询结果
你可以通过使用 Java 8 的 Stream<T> 作为返回类型,以增量方式处理查询方法的结果。
与将查询结果包装在 Stream 中不同,这里会使用特定于数据存储的方法来执行流式处理,如下例所示:
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 语句块,如下例所示: |
try-with-resources 块中处理 Stream<T> 结果try (Stream<User> stream = repository.findAllByCustomQueryAndStream()) {
stream.forEach(…);
}
并非所有 Spring Data 模块当前都支持 Stream<T> 作为返回类型。 |
2.4.9. 异步查询结果
你可以通过使用Spring 的异步方法执行功能来异步运行仓库查询。
这意味着方法在调用时会立即返回,而实际的查询则在一个已提交给 Spring TaskExecutor 的任务中执行。
异步查询与响应式查询不同,不应混用。
有关响应式支持的更多详细信息,请参阅特定存储的文档。
以下示例展示了一些异步查询:
@Async
Future<User> findByFirstname(String firstname); (1)
@Async
CompletableFuture<User> findOneByFirstname(String firstname); (2)
@Async
ListenableFuture<User> findOneByLastname(String lastname); (3)
| 1 | 使用 java.util.concurrent.Future 作为返回类型。 |
| 2 | 使用 Java 8 的 java.util.concurrent.CompletableFuture 作为返回类型。 |
| 3 | 使用 org.springframework.util.concurrent.ListenableFuture 作为返回类型。 |
2.5. 创建仓库实例
本节介绍如何为已定义的仓库接口创建实例和 Bean 定义。实现方式之一是使用随每个支持仓库机制的 Spring Data 模块提供的 Spring 命名空间,尽管我们通常推荐使用 Java 配置。
2.5.1. XML 配置
每个 Spring Data 模块都包含一个 repositories 元素,允许你定义一个基础包,Spring 会自动为你扫描该包,如下例所示:
<?xml version="1.0" encoding="UTF-8"?>
<beans:beans xmlns:beans="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://www.springframework.org/schema/data/jpa"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/data/jpa
https://www.springframework.org/schema/data/jpa/spring-jpa.xsd">
<repositories base-package="com.acme.repositories" />
</beans:beans>
在前面的示例中,Spring 被指示扫描 com.acme.repositories 及其所有子包,查找扩展 Repository 或其任一子接口的接口。
对于找到的每个接口,基础设施会注册特定于持久化技术的 FactoryBean,以创建适当的代理来处理查询方法的调用。
每个 Bean 都使用派生自接口名称的 Bean 名称进行注册,因此 UserRepository 的接口将注册为 userRepository。
嵌套仓库接口的 Bean 名称以前缀形式包含其外部类型的名称。
base-package 属性允许使用通配符,以便您可以定义要扫描的包模式。
使用过滤器
默认情况下,基础设施会拾取配置的基础包下扩展特定持久化技术子接口 Repository 的每个接口,并为其创建一个 Bean 实例。
然而,您可能希望对哪些接口创建 Bean 实例进行更细粒度的控制。
为此,请在 <repositories /> 元素内使用 <include-filter /> 和 <exclude-filter /> 元素。
其语义与 Spring 上下文命名空间中的元素完全等效。
有关详细信息,请参阅这些元素的 Spring 参考文档。
例如,若要排除某些接口不被实例化为仓库 Bean,您可以使用以下配置:
<repositories base-package="com.acme.repositories">
<context:exclude-filter type="regex" expression=".*SomeRepository" />
</repositories>
前面的示例排除了所有以 SomeRepository 结尾的接口被实例化。
2.5.2. Java 配置
您还可以通过在 Java 配置类上使用特定于存储的 @Enable${store}Repositories 注解来触发仓库基础设施。有关 Spring 容器基于 Java 的配置的介绍,请参阅 Spring 参考文档中的 JavaConfig。
启用 Spring Data 仓库的示例配置如下所示:
@Configuration
@EnableJpaRepositories("com.acme.repositories")
class ApplicationConfiguration {
@Bean
EntityManagerFactory entityManagerFactory() {
// …
}
}
上述示例使用了 JPA 特定的注解,您应根据实际使用的存储模块进行相应更改。EntityManagerFactory bean 的定义也是如此。请参阅涵盖特定存储配置的相关章节。 |
2.5.3. 独立使用
你也可以在 Spring 容器之外使用仓库(repository)基础设施——例如,在 CDI 环境中。你仍然需要在类路径中包含一些 Spring 库,但通常也可以通过编程方式设置仓库。提供仓库支持的 Spring Data 模块都附带了一个特定于持久化技术的 RepositoryFactory,你可以按如下方式使用它:
RepositoryFactorySupport factory = … // Instantiate factory here
UserRepository repository = factory.getRepository(UserRepository.class);
2.6. Spring Data 存储库的自定义实现
Spring Data 提供了多种选项,只需少量编码即可创建查询方法。 但当这些选项无法满足您的需求时,您也可以为仓库方法提供自己的自定义实现。 本节将介绍如何实现这一点。
2.6.1. 自定义单个仓库
要为仓库(repository)添加自定义功能,首先必须定义一个片段接口以及该自定义功能的实现,如下所示:
interface CustomizedUserRepository {
void someCustomMethod(User user);
}
class CustomizedUserRepositoryImpl implements CustomizedUserRepository {
public void someCustomMethod(User user) {
// Your custom implementation
}
}
与片段接口对应的类名中最重要的部分是 Impl 后缀。 |
该实现本身不依赖于 Spring Data,可以是一个普通的 Spring Bean。
因此,您可以使用标准的依赖注入行为来注入对其他 Bean(例如 JdbcTemplate)的引用、参与切面(aspects)等。
然后,您可以让你的仓库接口继承该片段接口,如下所示:
interface UserRepository extends CrudRepository<User, Long>, CustomizedUserRepository {
// Declare query methods here
}
通过您的仓库接口扩展片段接口,可以将 CRUD 功能与自定义功能结合起来,并使其对客户端可用。
Spring Data 仓库是通过构成仓库组合的片段来实现的。
片段包括基础仓库、功能特性(如 QueryDsl)以及自定义接口及其实现。
每次向您的仓库接口中添加一个接口时,您都会通过增加一个片段来增强该组合。
基础仓库和仓库特性实现由每个 Spring Data 模块提供。
以下示例展示了自定义接口及其实现:
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 的自定义仓库接口:
interface UserRepository extends CrudRepository<User, Long>, HumanRepository, ContactRepository {
// Declare query methods here
}
仓库可以由多个自定义实现组成,这些实现会按照其声明的顺序被导入。 自定义实现的优先级高于基础实现和仓库切面(aspects)。 这种排序机制允许你覆盖基础仓库和切面中的方法,并在两个片段提供相同方法签名时解决歧义。 仓库片段不仅限于在单一仓库接口中使用。 多个仓库可以共用同一个片段接口,从而让你在不同的仓库之间复用自定义逻辑。
以下示例展示了一个仓库片段及其实现:
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
}
}
以下示例展示了一个使用前述仓库片段的仓库:
interface UserRepository extends CrudRepository<User, Long>, CustomizedSave<User> {
}
interface PersonRepository extends CrudRepository<Person, Long>, CustomizedSave<Person> {
}
配置
如果您使用命名空间配置,存储库基础设施会尝试通过扫描在其中找到存储库的包下方的类来自动检测自定义实现片段。这些类需要遵循命名约定:将命名空间元素的 repository-impl-postfix 属性值追加到片段接口名称之后。此后缀的默认值为 Impl。以下示例展示了一个使用默认后缀的存储库以及一个为后缀设置了自定义值的存储库:
<repositories base-package="com.acme.repository" />
<repositories base-package="com.acme.repository" repository-impl-postfix="MyPostfix" />
前面示例中的第一个配置尝试查找名为 com.acme.repository.CustomizedUserRepositoryImpl 的类,用作自定义仓库实现。
第二个示例尝试查找 com.acme.repository.CustomizedUserRepositoryMyPostfix。
歧义解析
如果在不同包中找到多个具有匹配类名的实现,Spring Data 会使用 Bean 名称来确定使用哪一个。
鉴于前面所示的 CustomizedUserRepository 的以下两种自定义实现,将使用第一种实现。
其 Bean 名称为 customizedUserRepositoryImpl,该名称与片段接口(CustomizedUserRepository)的名称加上后缀 Impl 相匹配。
package com.acme.impl.one;
class CustomizedUserRepositoryImpl implements CustomizedUserRepository {
// Your custom implementation
}
package com.acme.impl.two;
@Component("specialCustomImpl")
class CustomizedUserRepositoryImpl implements CustomizedUserRepository {
// Your custom implementation
}
如果你使用 UserRepository 注解 @Component("specialCustom") 接口,那么该 Bean 的名称加上 Impl 后,就会与在 com.acme.impl.two 中为仓库实现所定义的名称相匹配,并会替代第一个实现被使用。
手动配置
如果你的自定义实现仅使用基于注解的配置和自动装配,那么前面所示的方法可以很好地工作,因为该实现会被视为与其他任何 Spring Bean 一样。 如果你的实现片段 Bean 需要特殊的装配方式,你可以按照前一节中描述的约定来声明该 Bean 并为其命名。 这样,基础设施将通过名称引用手动定义的 Bean 定义,而不是自行创建一个。 以下示例展示了如何手动装配一个自定义实现:
<repositories base-package="com.acme.repository" />
<beans:bean id="userRepositoryImpl" class="…">
<!-- further configuration -->
</beans:bean>
2.6.2. 自定义基础仓库
上一节中描述的方法要求在你希望自定义基础仓库行为(从而影响所有仓库)时,对每个仓库接口进行定制。 要改为对所有仓库统一更改行为,你可以创建一个实现类,该类继承特定于持久化技术的仓库基类。 然后,该类将作为仓库代理的自定义基类,如下例所示:
class MyRepositoryImpl<T, ID>
extends SimpleJpaRepository<T, ID> {
private final EntityManager entityManager;
MyRepositoryImpl(JpaEntityInformation entityInformation,
EntityManager entityManager) {
super(entityInformation, entityManager);
// Keep the EntityManager around to used from the newly introduced methods.
this.entityManager = entityManager;
}
@Transactional
public <S extends T> S save(S entity) {
// implementation goes here
}
}
该类需要具有一个超类的构造函数,供特定于存储的仓库工厂实现使用。
如果仓库基类有多个构造函数,请重写接受 EntityInformation 以及特定于存储的基础设施对象(例如 EntityManager 或模板类)的那个构造函数。 |
最后一步是让 Spring Data 基础设施感知自定义的存储库基类。在 Java 配置中,您可以通过使用 repositoryBaseClass 属性的 @Enable${store}Repositories 注解来实现,如下例所示:
@Configuration
@EnableJpaRepositories(repositoryBaseClass = MyRepositoryImpl.class)
class ApplicationConfiguration { … }
A corresponding attribute is available in the XML namespace, as shown in the following example:
<repositories base-package="com.acme.repository"
base-class="….MyRepositoryImpl" />
2.7.从聚合根发布事件
由仓库(repositories)管理的实体是聚合根(aggregate roots)。
在领域驱动设计(Domain-Driven Design)应用程序中,这些聚合根通常会发布领域事件(domain events)。
Spring Data 提供了一个名为 @DomainEvents 的注解,你可以将其用于聚合根中的某个方法上,以尽可能简便地实现事件发布,如下例所示:
class AnAggregateRoot {
@DomainEvents (1)
Collection<Object> domainEvents() {
// … return events you want to get published here
}
@AfterDomainEventPublication (2)
void callbackMethod() {
// … potentially clean up domain events list
}
}
| 1 | 使用 @DomainEvents 注解的方法可以返回单个事件实例或一组事件。
该方法不能接受任何参数。 |
| 2 | 在所有事件发布完成后,我们有一个使用 @AfterDomainEventPublication 注解的方法。
你可以用它来清理待发布的事件列表(以及其他用途)。 |
每次调用以下任一 Spring Data 仓库方法时,都会调用这些方法:
-
save(…),saveAll(…) -
delete(…),deleteAll(…),deleteAllInBatch(…),deleteInBatch(…)
请注意,这些方法以聚合根实例作为参数。
这就是为什么明显缺少 deleteById(…) 方法,因为实现可能会选择直接执行一个删除该实例的查询,因此我们从一开始就不会有机会访问到聚合实例。
2.8. Spring 数据扩展
本节文档介绍了一组 Spring Data 扩展,这些扩展使得 Spring Data 能够在各种上下文中使用。 目前,大多数集成都是面向 Spring MVC 的。
2.8.1。查询 DSL 扩展
Querydsl 是一个框架,通过其流畅的 API 支持构建静态类型的类 SQL 查询。
多个 Spring Data 模块通过 QuerydslPredicateExecutor 提供与 Querydsl 的集成,如下例所示:
public interface QuerydslPredicateExecutor<T> {
Optional<T> findById(Predicate predicate); (1)
Iterable<T> findAll(Predicate predicate); (2)
long count(Predicate predicate); (3)
boolean exists(Predicate predicate); (4)
// … more functionality omitted.
}
| 1 | 查找并返回与 Predicate 匹配的单个实体。 |
| 2 | 查找并返回所有匹配 Predicate 的实体。 |
| 3 | 返回与Predicate匹配的实体数量。 |
| 4 | 返回是否存在与Predicate匹配的实体。 |
要使用 Querydsl 支持,请在您的仓库接口中继承 QuerydslPredicateExecutor,如下例所示:
interface UserRepository extends CrudRepository<User, Long>, QuerydslPredicateExecutor<User> {
}
前面的示例允许您使用 Querydsl 的 Predicate 实例编写类型安全的查询,如下例所示:
Predicate predicate = user.firstname.equalsIgnoreCase("dave")
.and(user.lastname.startsWithIgnoreCase("mathews"));
userRepository.findAll(predicate);
2.8.2. Web 支持
支持仓库(repository)编程模型的 Spring Data 模块提供了多种 Web 支持。
这些与 Web 相关的组件要求 classpath 中包含 Spring MVC 的 JAR 文件。
其中一些甚至提供了与 Spring HATEOAS 的集成。
通常,可以通过在 JavaConfig 配置类中使用 @EnableSpringDataWebSupport 注解来启用集成支持,如下例所示:
@Configuration
@EnableWebMvc
@EnableSpringDataWebSupport
class WebConfiguration {}
@EnableSpringDataWebSupport 注解会注册若干组件。
我们将在本节稍后部分讨论这些组件。
它还会检测类路径上是否存在 Spring HATEOAS,并在存在时为其注册相应的集成组件。
或者,如果你使用 XML 配置,注册 0 或 1 作为 Spring bean,如下例所示(针对 2):
<bean class="org.springframework.data.web.config.SpringDataWebConfiguration" />
<!-- If you use Spring HATEOAS, register this one *instead* of the former -->
<bean class="org.springframework.data.web.config.HateoasAwareSpringDataWebConfiguration" />
基本 Web 支持
上一节中所示的配置注册了几个基本组件:
-
使用
DomainClassConverter类 让 Spring MVC 从请求参数或路径变量中解析由仓库管理的领域类实例。 -
HandlerMethodArgumentResolver实现,使 Spring MVC 能够从请求参数中解析Pageable和Sort实例。 -
Jackson 模块,用于对
Point和Distance等类型进行序列化/反序列化,或根据所使用的 Spring Data 模块存储特定类型。
使用DomainClassConverter类
DomainClassConverter 类允许您在 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 的实例。
该注册使得 Pageable 和 Sort 可作为有效的控制器方法参数,如下例所示:
@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 实例:
|
要检索的页面。从0开始索引,默认值为0。 |
|
您要检索的页面大小。默认值为20。 |
|
排序属性的格式为 |
要自定义此行为,请分别注册一个实现 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),但你可以通过在 @PageableDefault 参数上使用 Pageable 注解来自定义它。
分页的超媒体支持
Spring HATEOAS 随带一个表示模型类(PagedResources),该类允许在 Page 实例中添加必要的 Page 元数据以及链接,以便客户端可以轻松导航页面。
将 Page 转换为 PagedResources 是通过实现 Spring HATEOAS 的 ResourceAssembler 接口来完成的,称为 PagedResourcesAssembler。
下面的例子展示了如何使用 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(…) 方法。
Spring Data Jackson 模块
核心模块以及一些特定于存储的模块,附带了一组用于类型的 Jackson 模块,例如org.springframework.data.geo.Distance和org.springframework.data.geo.Point,由 Spring Data 域使用。
这些模块仅导入一次Web 支持已启用且com.fasterxml.jackson.databind.ObjectMapper已可用。
在初始化期间,SpringDataJacksonModules(例如 SpringDataJacksonConfiguration)会被基础设施自动发现,从而使其声明的 com.fasterxml.jackson.databind.Module 对 Jackson 的 ObjectMapper 可用。
通用基础设施会为以下域类型注册数据绑定混入(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
|
单个模块可能提供额外的 |
Web 数据绑定支持
您可以使用 Spring Data 投影(如[投影]所述)通过使用JSONPath表达式(需要Jayway JsonPath)或XPath表达式(需要XmlBeam),来绑定传入的请求有效负载,如下例所示:
@ProjectedPayload
public interface UserPayload {
@XBRead("//firstname")
@JsonPath("$..firstname")
String getFirstname();
@XBRead("/lastname")
@JsonPath({ "$.lastname", "$.user.lastname" })
String getLastname();
}
您可以将前面示例中所示的类型用作 Spring MVC 处理器方法的参数,或者在 ParameterizedTypeReference 的某个方法上使用 RestTemplate。
上述方法声明会尝试在给定文档的任意位置查找 firstname。
lastname 的 XML 查找是在传入文档的顶层进行的。
而其 JSON 变体则首先尝试在顶层查找 lastname,如果未找到值,还会尝试在 lastname 子文档中嵌套查找 user。
通过这种方式,即使源文档的结构发生变化,也能轻松缓解影响,而无需修改调用这些公开方法的客户端代码(这通常是基于类的载荷绑定的一个缺点)。
嵌套投影如 [projections] 中所述得到支持。如果方法返回复杂非接口类型,则使用 Jackson ObjectMapper 映射最终值。
对于 Spring MVC,只要启用了 @EnableSpringDataWebSupport 并且类路径中存在所需的依赖项,必要的转换器就会自动注册。
在与 RestTemplate 一起使用时,请手动注册一个 ProjectingJackson2HttpMessageConverter(JSON)或 XmlBeamHttpMessageConverter。
有关更多信息,请参阅权威的Spring Data 示例仓库中的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 注解即可提供一个可直接使用的 Predicate,你可以通过 QuerydslPredicateExecutor 来执行它。
类型信息通常从方法的返回类型解析得出。
由于该信息不一定与领域类型匹配,因此使用 root 的 QuerydslPredicate 属性可能是个不错的主意。 |
以下示例展示了如何在方法签名中使用 @QuerydslPredicate:
@Controller
class UserController {
@Autowired UserRepository repository;
@RequestMapping(value = "/", method = RequestMethod.GET)
String index(Model model, @QuerydslPredicate(root = User.class) Predicate predicate, (1)
Pageable pageable, @RequestParam MultiValueMap<String, String> parameters) {
model.addAttribute("users", repository.findAll(predicate, pageable));
return "index";
}
}
| 1 | 将查询字符串参数解析为与 Predicate 匹配的 User。 |
默认绑定如下:
-
Object用于简单属性,如eq。 -
在集合类属性上使用
Object的contains方法。 -
Collection用于简单属性,如in。
你可以通过 bindings 注解的 @QuerydslPredicate 属性来定制这些绑定,或者利用 Java 8 的 default methods(默认方法),在仓库接口中添加 QuerydslBinderCustomizer 方法,如下所示:
interface UserRepository extends CrudRepository<User, String>,
QuerydslPredicateExecutor<User>, (1)
QuerydslBinderCustomizer<QUser> { (2)
@Override
default void customize(QuerydslBindings bindings, QUser user) {
bindings.bind(user.username).first((path, value) -> path.contains(value)) (3)
bindings.bind(String.class)
.first((StringPath path, String value) -> path.containsIgnoreCase(value)); (4)
bindings.excluding(user.password); (5)
}
}
| 1 | QuerydslPredicateExecutor 提供了用于 Predicate 的特定查找方法的访问。 |
| 2 | 在仓库接口上定义的 QuerydslBinderCustomizer 会被自动识别,并作为 @QuerydslPredicate(bindings=…) 的快捷方式。 |
| 3 | 将 username 属性的绑定定义为一个简单的 contains 绑定。 |
| 4 | 将 String 属性的默认绑定定义为不区分大小写的 contains 匹配。 |
| 5 | 从 password 解析中排除 Predicate 属性。 |
你可以在应用来自仓库或 QuerydslBinderCustomizerDefaults 的特定绑定之前,注册一个包含默认 Querydsl 绑定的 @QuerydslPredicate Bean。 |
2.8.3. 存储库填充程序
如果你使用 Spring JDBC 模块,可能已经熟悉通过 SQL 脚本填充 DataSource 的功能。
在仓库(repositories)层面也提供了类似的抽象,但由于它必须与底层存储无关,因此并未使用 SQL 作为数据定义语言。
因此,这些填充器(populators)支持使用 XML(通过 Spring 的 OXM 抽象)和 JSON(通过 Jackson)来定义用于填充仓库的数据。
假设你有一个名为 data.json 的文件,其内容如下:
[ { "_class" : "com.acme.Person",
"firstname" : "Dave",
"lastname" : "Matthews" },
{ "_class" : "com.acme.Person",
"firstname" : "Carter",
"lastname" : "Beauford" } ]
您可以使用 Spring Data Commons 中提供的 repository 命名空间的 populator 元素来填充您的仓库。
要将上述数据填充到您的 PersonRepository 中,请声明一个类似于以下内容的 populator:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:repository="http://www.springframework.org/schema/data/repository"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/data/repository
https://www.springframework.org/schema/data/repository/spring-repository.xsd">
<repository:jackson2-populator locations="classpath:data.json" />
</beans>
上述声明会导致 data.json 文件被 Jackson 的 ObjectMapper 读取并反序列化。
JSON 对象被反序列化的目标类型是通过检查 JSON 文档中的 _class 属性来确定的。
基础设施最终会选择合适的仓库(repository)来处理反序列化后的对象。
要改用 XML 来定义应填充到存储库中的数据,可以使用 unmarshaller-populator 元素。将其配置为使用 Spring OXM 中可用的其中一个 XML 编组选项之一即可。有关详细信息,请参阅 Spring 参考文档。以下示例显示了如何使用 JAXB 解编一个存储库填充器:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:repository="http://www.springframework.org/schema/data/repository"
xmlns:oxm="http://www.springframework.org/schema/oxm"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/data/repository
https://www.springframework.org/schema/data/repository/spring-repository.xsd
http://www.springframework.org/schema/oxm
https://www.springframework.org/schema/oxm/spring-oxm.xsd">
<repository:unmarshaller-populator locations="classpath:data.json"
unmarshaller-ref="unmarshaller" />
<oxm:jaxb2-marshaller contextPath="com.acme" />
</beans>