前言
学习NoSQL和Document数据库
NoSQL数据库迅速占领了存储市场。这是一个巨大的领域,有着过剩的解决方案、条款和模式(更糟糕的是,即使条款本身也有多个含义)。然而一些原则是相同的,但必须对MongoDB熟悉到一定的程度。熟悉的最好方法是阅读这个文档,并学习例子。这通常不会花费超过5-10分钟阅读,尤其对那些只有RDMBS背景的人来说,这些练习将开阔视野。
学习MongoDB的起点是www.monodb.org。下面是一些有用的资源列表:
- 手册介绍了MongoDB,包含开始向导,引用文档和教程的链接
- 在线命令行提供了一个和MongoDB实例交互的便捷方式,并且有在线教程
- MongoDB java语言中心
- 一些可以购买的书籍
- Karl Seguin的在线数据:MongoBD小册
和Spring Data Repositories一起工作
Spring Data仓库抽象的目标是显著地减少用来实现各种持久化仓库的数据访问层的样本代码的数量。
Spring Data repository文档和你的模块
本章节阐释了Spring Data仓库的核心概念和接口。本章节的信息是从Spring Data Commons模块拉取过来的。它使用Java Persistence API(JPA)模块的配置和例子。需要根据使用的特定模块的等效配置来适配XML命令空间声明和需要继承的类型。“命名空间引用”覆盖了XML配置。“仓库查询关键字”覆盖了查询方法的关键字,一般被仓库抽象支持。要看模块的特定特性,参考这个文档关于那个模块的章节。
核心概念
Spring Data仓库抽象的核心接口是Repository。它把领域类型和领域类的ID类型作为类型参数。这个接口一般作为标记接口来捕获一起工作的类型并且帮助发现继承这个接口的接口。CrudRepository为管理的实体类提供了丰富的CRUD功能。
CrudRepository接口
public interface CrudRepository<T, ID> 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.
}
框架还提供了与指定技术相关的抽象,比如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);
}
访问User以20为一页的第二页,可以像下面这么做:
PagingAndSortingRepository<User, Long> repository = // … get access to a bean
Page<User> users = repository.findAll(PageRequest.of(1, 20));
除了查询方法,关于数量和删除的派生查询也是可用的。下面的列表展示了一个派生数量查询的接口:
派生数量查询
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);
}
查询方法
标准的CRUD功能的仓库通常有底层数据仓库的查询功能。使用Spring Data,声明这些查询变成一个四步过程:
- 声明一个接口继承Repository或它的一个子接口,键入它处理的类型和ID,如下面的例子所展示的那样:
- 声明接口的查询方法
- 配置Spring来创建这些接口的代理实例,使用JavaConfig或XML配置。
a. 使用Java配置,创建如下类似的类:
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
@EnableJpaRepositories
class Config { … }
b. 使用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属性。
4. 注入这个仓库实例,如下面例子所示:
class SomeClient {
private final PersonRepository repository;
SomeClient(PersonRepository repository) {
this.repository = repository;
}
void doSomething() {
List<Person> persons = repository.findByLastname("Matthews");
}
}
具体详细解释这四步的章节:
定义仓库接口
首先,定义一个特定域类的仓库。这个接口必须继承Repository并填入域类和ID类。如果想暴露域类的CRUD方法,继承CrudRepository而不是Repository。
微调仓库定义
通常,仓库接口继承Repository,CrudRepository和PagingAndSortingRepository。另外,如果不想继承Spring Data接口,可以使用@RepositoryDefinition注解仓库实例。继承CrudRepository暴露了一整套操作实体的方法。如果想选择某些方法暴露出来,可以从CrudRepository复制你想暴露的方法到你的域仓库中。
这么做能让你在Spring Data Repsoitories功能的基础上定义自己的抽象。
下面的例子展示了如何选择性的暴露CRUD方法(这个例子是findById和save):
选择性地暴露CRUD方法
@NoRepositoryBean
interface MyBaseRepository<T, ID> extends Repository<T, ID> {
Optional<T> findById(ID id);
<S extends T> S save(S entity);
}
interface UserRepository extends MyBaseRepository<User, Long> {
User findByEmailAddress(EmailAddress emailAddress);
}
在上面的例子中,为所有的域仓库定义了一个通用的根本接口,暴露findById(…)和save(…)方法。这些方法路由到存储仓库的基本实现中,这些实现由Spring Data提供,因为它们匹配CrudRepository的方法签名。所以UserRepository现在可以保存users,通过ID查找单个的user,并且可以通过邮箱地址触发查询。
中间的仓库接口使用@NoRepositoryBean注解。确保所有不应该被Spring Data在运行时创建实例的仓库添加了这个注解。
在多个Spring Data模块中使用Repositories
在程序中使用一个唯一的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引用Person,它被JPA的@Entity注解注释,所以这个仓库很清楚地属于Spring Data JPA。UserRepository引用User,它被Spring Data MongoDB的@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 { … }
定义查询方法
仓库代理有两种方式从方法名派生一个存储相关的查询:
- 直接从方法名派生查询
- 使用一个手动定义的查询
可用的选项取决于实际的存储。然而,肯定有一个策略决定实际创建了哪个查询。下一个章节描述了可用的选项。
查询查找策略
xml使用query-lookup-strategy,注解使用queryLookupStrategy。有些数据仓库可能不支持这个策略。
- CREATE 根据查询方法名构造一个存储相关的查询
- USE_DECLARED_QUERY 尝试找到一个声明的查询,如果不能找到,抛出一个异常
- CREATE_IF_NOT_FOUND (默认)结合CREATE和USE_DECLARED_QUERY。它首先查询声明的查询,并且,如果没有找到声明的查询,创建一个基于自定义方法名称的查询
查询创建
这个机制剥去方法的前缀find…By,read…By,query…By,count…By和get…By并开始解析剩下的部分。可能设置Distinct标记。可以用And或Or定义条件。
从方法名称创建查询
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);
}
最终的解析结果取决于创建查询的持久化仓库。然而,这里有一些一般注意项:
- 表达式通常是属性遍历和可以连接的运算符的组合。可以用AND和OR连接属性表达式。也支持操作符比如Between,LessThan,GreaterThan和Like。
- 支持单个属性的IgnoreCase(比如findByLastnameIgnoreCase(…))或所有属性的(比如findByLastnameAndFirstnameAllIgnoreCase(…))。
- 支持OrderBy(Asc或Desc)参考特殊参数处理
属性表达式
属性表达式只能引用到治理实体的直接属性上,如前面例子显示的那样。在查询创建时刻,已经确保这个被解析的属性已经是治理实体的一个属性。然而,可以遍历嵌套属性来定义约束。
List<Person> findByAddressZipCode(ZipCode zipCode);
假设Person有个Address,Address有个ZipCode。这种情况下,这个方法创建属性遍历x.address.zipCode。算法开始使用整个部分作为属性并且检查域类是否有这个属性。如果有,则使用这个属性。如果没有,算法从右边开始分割为头部和尾部,这个例子是AddressZip和Code。然后开始查找有没有头部的属性,然后检查尾部的属性,如果没有则继续分割。如果第一次分割没有匹配,则左移分割(Address,ZipCode)继续。
大部分情况这种方法是可以的,但是算法有可能选择错误的属性。假如Person类也有一个addressZip属性。算法在第一次分割就匹配了,选择错误的属性,然后失败(因为addressZip类可能没有code属性)。
在方法中使用_来解决这个问题,所以方法名称可以是下面这样:
List<Person> findByAddress_ZipCode(ZipCode zipCode);
因为下划线被认为是保留字符,所以强烈建议标准的命名转换(那就是,属性名称不使用下划线而使用骆驼风格)。
特殊参数处理
框架能识别特定参数比如Pageable和Sort,来动态应用分页和排序查询。下面的例子演示了这些特性:
在查询方法使用Pageable,Slice和Sort
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);
APIs的Sort和Pageable为non-null值。如果不想应用排序或分页,使用Sort.unsorted()和Pageable.unpaged()。
Page知道数据的总数,会触发一次数量查询。因为开销昂贵,所以返回一个Slice。Slice只知道下个Slice是否可用,在遍历大数据集时非常有效。
Pageable也包含排序选项。如果只需要排序,添加org.springframework.data.domain.Sort参数到方法。这是,就不会创建一个Page对象(这意味着不会触发一次额外的数量查询)。然而,这样查询就只能查看给定范围的实体。
为了知道查询实体有多少页,必须触发额外的数量查询。默认情况下,查询从实际触发的查询中派生。
分页和排序
简单的排序表达式可以使用属性名称定义。多个表达式可以连接为一个表达式。
定义排序表达式
Sort sort = Sort.by("firstname").ascending()
.and(Sort.by("lastname").descending());
更加类型安全的定义排序表达式,使用类型来定义表达式并且使用方法来定义要排序的属性。
用类型安全API定义排序表达式
TypedSort<Person> person = Sort.sort(Person.class);
TypedSort<Person> sort = person.by(Person::getFirstname).ascending()
.and(person.by(Person::getLastname).descending());
如果存储实现支持Querydsl,可以使用元模型类型定义排序表达式
用Querydsl API定义排序表达式
TypedSort<Person> person = Sort.sort(Person.class);
TypedSort<Person> sort = person.by(Person::getFirstname).ascending()
.and(person.by(Person::getLastname).descending());
限制查询结果
后面跟个数字来表示限制的数量,不指定默认为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’的最小值和最大值。
仓库方法返回集合或可迭代对象
用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"));
返回自定义Streamable包装类型
满足下面规则无需手动包装集合到自定义集合类型:
- 类型实现Streamable
- 类型有构造函数或静态工厂方法of(…)或valueOf(…),以Streamable为参数
一个用例样本:
class Product {
MonetaryAmount getPrice() { … }
}
@RequiredArgConstructor(staticName = "of")
class Products implements Streamable<Product> {
private Streamable<Product> streamable;
public MonetaryAmount getTotal() {
return streamable.stream() //
.map(Priced::getPrice)
.reduce(Money.of(0), MonetaryAmount::add);
}
}
interface ProductRepository implements Repository<Product, Long> {
Products findAllByDescriptionContaining(String text);
}
- Product能访问价格
- 可通过Products.of(…)构造Streamable
<Product>
包装类 - 包装类暴露额外计算新值的API
- 查询方法可以直接返回包装类,而无需返回Streamable
<Product>
然后手动包装
Vavr集合支持
Vavr是加强Java函数式编程概念的库
Vavr collection type | Used Vavr implementation type | Valid Java source types |
---|---|---|
io.vavr.collection.Seq | io.vavr.collection.List | java.util.Iterable |
io.vavr.collection.Set | io.vavr.collection.LinkedHashSet | java.util.Iterable |
io.vavr.collection.Map | io.vavr.collection.LinkedHashMap | java.util.Map |
仓库方法的Null处理
除了Java 8的Optional,Spring Dataa还支持:
- com.google.common.base.Optional
- scala.Option
- io.vavr.control.Option
另外,查询方法返回值也可以选择不用包装类,如果没有结果,就返回null。返回集合,集合变体,包装类,和流的方法不会返回null,而是返回空的集合。
可空注解
在运行时会进行null检查
- @NonNullApi :包级别非空
- @NonNull:参数和返回值非空
- @Nullable:参数和返回值可空
在package-info.java声明非空
@org.springframework.lang.NonNullApi
package com.acme;
仓库会在运行时检查空约束。如果查询结果违反约束,则抛出异常。如果想再次可以为空,可以在单独的方法上使用@Nullable。
使用不同的可空约束
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);
}
- 仓库位于定义了non-null行为的包(或子包)
- 查询没有结果抛出EmptyResultDataAccessException,emailAddress为null时抛出IllegalArgumentException
- 没有产生结果时,返回null;也接收emailAddress为null的值
- 没有结果返回Optional.empty(),emailAddress为null时抛出IllegalArgumentException
Kotlin仓库的可空性
Kotlin有植入语言层面的空约束定义。
在Kotlin仓库上使用空约束
interface UserRepository : Repository<User, String> {
fun findByUsername(username: String): User
fun findByFirstname(firstname: String?): User?
}
- 方法定义参数和结果都为非空(Kotlin默认)。Kotlin编译器拒绝传入的null值。查询是空结果的话,抛出EmptyResultDataAccessException异常。
- 方法接收null的firstname参数和返回null如果没有产生结果的话。
流化查询结果
可以把Java 8的Stream<T>
作为返回值。不是把查询结果包装到Stream,而是把数据存储相关的方法用来进行流操作。如下面例子展示:
用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块。如下面的例子:
在try-with-resources块中使用Stream<T>
try (Stream<User> stream = repository.findAllByCustomQueryAndStream()) {
stream.forEach(…);
}
并不是所有的模块目前支持Stream
<T>
作为返回值
异步查询结果
异步意味着方法立即返回,但是实际查询等到提交到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作为返回值
创建仓库实例
一个方法是使用Spring命名空间,但更推荐Java配置。
XML配置
通过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
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的名称继承接口名称,比如UserRepository被注册为userRepository。base-package属性允许通配符来定义要扫描包的模式。
使用过滤器
使用<repositories />
标签下的<include-filter />
和<exclude-filter />
来过滤。
比如要排除特定的接口不被初始化为仓库实例,可以用下面的配置:
使用exclude-filter标签
<repositories base-package="com.acme.repositories">
<context:exclude-filter type="regex" expression=".*SomeRepository" />
</repositories>
上面的例子排除所有以SomeRepository结尾的接口被初始化。
Java配置
通过存储相关的@Enable${store}Repositories注解来触发仓库框架。
基于注解的仓库配置样本
@Configuration
@EnableJpaRepositories("com.acme.repositories")
class ApplicationConfiguration {
@Bean
EntityManagerFactory entityManagerFactory() {
// …
}
}
上面的例子用的是JPA相关配置,实际使用的时候要根据存储模块更换。EntityManagerFactory的定义也是。
单独使用
也可以在Spring容器之外使用仓库设施。Spring Data模块装配了一个持久化技术相关的RepositoryFactory,使用如下:
仓库工厂的单独使用
RepositoryFactorySupport factory = … // Instantiate factory here
UserRepository repository = factory.getRepository(UserRepository.class);
自定义Spring Data Repositories实现
本章节涉及自定义仓库和碎片如何构成一个聚合仓库
自定义单独的仓库
首先定义一个碎片接口和实现:
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)的引用,加入切面等。
也可以让仓库接口继承碎片接口,如下展示:
修改仓库接口
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
}
}
修改仓库接口
interface UserRepository extends CrudRepository<User, Long>, HumanRepository, ContactRepository {
// Declare query methods here
}
组合多个自定义实现的仓库按照它们声明的顺序导入。自定义实现比基本实现和仓库切面有更高的优先级。这个顺序使你可以重载基本仓库和切面方法并且解决冲突如果两个碎片有相同的方法签名。仓库碎片没有限制在单个仓库接口使用。多个仓库可能用同一个碎片接口,让你能在不同的仓库重用自定义行为。
碎片重载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,匹配碎片接口加后缀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
}
如果用@Component(“specialCustom”)注释UserRepository,那么bean名称加Impl匹配定义在com.acme.impl.two的仓库实现,会用这个而不是第一个。
手动注入
框架可以通过名称指向手动定义的bean而不是创建一个。下面的例子展示了如何手动注入自定义实现:
手动注入自定义实现
<repositories base-package="com.acme.repository" />
<beans:bean id="userRepositoryImpl" class="…">
<!-- further configuration -->
</beans:bean>
自定义基本仓库
上面描述的方法需要每个仓库接口都自定义,当你想自定义基本接口的行为并影响每个仓库时。为了改变每个仓库的行为,可以创建一个实现继承持久化技术相关的仓库基本类来代替。这个类作为仓库代理的自定义基类,如下面例子所示:
自定义仓库基类
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配置中,可以使用@Enable${store}Repositories注解的repositoryBaseClass属性,如下面例子:
使用JavaConfig配置自定义仓库
@Configuration
@EnableJpaRepositories(repositoryBaseClass = MyRepositoryImpl.class)
class ApplicationConfiguration { … }
对应XML命名空间也有个属性,如下例所示:
<repositories base-package="com.acme.repository"
base-class="….MyRepositoryImpl" />
发布聚合根事件
仓库管理的实体称为聚合根。在领域驱动设计程序中,这些聚合根通常发布领域事件。Spring Data提供一个叫@DomainEvents的注解,可以用在聚合根的方法上,使得发布成为可能,如下例所示:
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 Data仓库的save(…)方法调用时,会被调用。
Spring Data拓展
本章节记录了一系列的Spring Data扩展,能让Spring Data在不同的上下文使用。现在,大部分集成都针对Spring MVC。
Querydsl扩展
Querydsl是一个通过它流畅的API开启静态类型SQL化查询的构造器。
多个Spring Data模块通过QuerydslPredicateExecutor提供Querydsl支持,如下例所示:
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,如下例所示:
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);
Web支持
本章包含目前(和最近)版本的Spring Data Commons的关于Spring Data web支持的文档。以前版本的行为参考“web.legacy”
支持仓库编程模型的Spring Data模块装载了多种web支持。web相关的组件需要Spring MVC JARs在类路径上。有些甚至提供和Spring HATEOAS的集成。一般来说,通过在JavaConfig配置文件类上使用@EnableSpringDataWebSupport注解来开启集成支持,如下例所示:
开启Spring Data网络支持
@Configuration
@EnableWebMvc
@EnableSpringDataWebSupport
class WebConfiguration {}
@EnableSpringDataWebSupport注解注册了一些将要讨论的组件。也会检测类路径上的Spring HATEOAS,如果存在的话,也会为它注册集成组件。
另外,如果使用XML配置,注册SpringDataWebConfiguration或HateoasAwareSpringDataWebConfiguration作为Spring beans,如下例所示(SpringDataWebConfiguration):
在XML开启Spring Data网络支持
<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" />
基本网络支持
上面展示的配置注册了一些基本组件:
- DomainClassConverter让Spring MVC从请求参数或路径变量中解析域类实例
- HandlerMethodArgumentResolver让Spring MVC从请求参数中解析Pageable和Sort
DomainClassConverter
DomainClassConverter 让你在Spring MVC控制器方法签名上直接使用域类,所有不再需要通过仓库手动查找实例,如下例所示:
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的HandlerMethodArgumentResolver
先前章节展示的配置注册了一个PageableHandlerMethodArgumentResolver和SortHandlerMethodArgumentResolver。这些注册让Pageable和Sort成为合法控制器方法参数,如下例所示:
使用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实例:
Pageable的请求参数
字段 | 默认值 |
---|---|
page | 从零开始,默认0 |
size | 20 |
sort | 格式property,property(,ASC|DESC)。默认是升序。使用多个sort参数如果想切换方向-比如?sort=firstname&sort=lastname,asc |
要自定义这种行为,注册一个bean依次实现PageableHandlerMethodArgumentResolverCustomizer接口或SortHandlerMethodArgumentResolverCustomizer接口。它的customize()方法被调用,让你改变设置,如下例所示:
@Bean SortHandlerMethodArgumentResolverCustomizer sortCustomizer() {
return s -> s.setPropertyDelimiter("<-->");
}
如果设置现有MethodArgumentResolver的属性不足以满足需求,继承SpringDataWebConfiguration或HATEOAS-enabled的等价类,重载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注解来自定义。
Pageable的超媒体支持
Spring HATEOAS装载了一个表现模型类,允许增强Page实例的内容,加入必要的Page元信息和让客户端轻松导航的链接。Page到PagedResources的转换是通过Spring HATEOAS的ResourceAssembler接口的实现完成的,叫做PagedResourcesAssembler。下例展示了如何使用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链接,取决于page的状态。链接指向方法映射的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(…)方法。
Web数据绑定支持
Spring Data投影(在Projections介绍)可以用来绑定入站请求负载,可以通过使用JSONPath表达式(需要Jayway JsonPath)或XPath表达式(需要XmlBeam),如下例所示:
使用JSONPath或XPath表达式绑定HTTP负载
@ProjectedPayload
public interface UserPayload {
@XBRead("//firstname")
@JsonPath("$..firstname")
String getFirstname();
@XBRead("/lastname")
@JsonPath({ "$.lastname", "$.user.lastname" })
String getLastname();
}
上个例子展示的类型可以用作Spring MVC处理器方法参数或在一个RestTemplate的方法上使用ParameterizedTypeReference。上面的方法声明会尝试在给定文档的所有位置上查找firstname。XML的lastname查找在文档的最顶层进行。JSON变体首先在顶层尝试查找lastname但是也会在嵌套的user子文档尝试查找如果前者没有返回结果。这样的话,源文档结构上的改变可以轻松减轻而无需客户端调用暴露的方法(通常是基于类负载绑定的缺陷)。
如Projections描述的那样,嵌套投影已被支持。如果方法返回一个复杂,非接口类型,Jackson的ObjectMapper会被用来映射最终的值。
对于Spring MVC,只要加了@EnableSpringDataWebSupport注解,必要的转换器就会自动注册并且需要的依赖在类路径上可用。对于RestTemplate的使用,手动注册一个ProjectingJackson2HttpMessageConverter(JSON)或XmlBeamHttpMessageConverter。
更多信息,参见经典Spring Data Examples repository中的web投影例子。
Querydsl网络支持
对于有QueryDSL集成的存储,可以从包含在Request查询字符串中的属性来派生查询。
考虑下面查询字符串:
?firstname=Dave&lastname=Matthews
给定前个例子的User对象,查询字符串可使用QuerydslPredicateArgumentResolver解析成下列值。
QUser.user.firstname.eq("Dave").and(QUser.user.lastname.eq("Matthews"))
有@EnableSpringDataWebSupport注解,类路径找到Querydsl时,该功能自动开启。
添加@QuerydslPredicate到方法签名上时提供一个准备可用的Predicate,可以用QuerydslPredicateExecutor运行。
类型信息通常从方法返回类型解析。因为那个信息一般不匹配域类,所以最好的方法就是使用QuerydslPredicate的root属性。
下面的例子展示了如何在方法签名使用@QuerydslPredicate
@Controller
class UserController {
@Autowired UserRepository repository;
@RequestMapping(value = "/", method = RequestMethod.GET)
String index(Model model, @QuerydslPredicate(root = User.class) Predicate predicate,
Pageable pageable, @RequestParam MultiValueMap<String, String> parameters) {
model.addAttribute("users", repository.findAll(predicate, pageable));
return "index";
}
}
- 解析查询字符串参数为匹配User的Predicate。
默认绑定如下:
- 简单属性上的Object为as
- 集合属性上的Object为contains
- 简单属性上的Collection为in
这些绑定可以通过@QuerydslPredicate的属性bindings自定义或通过Java 8的默认方法并添加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属性为一个简单的contains绑定
- 定义默认的String属性为大小写不敏感的contains匹配
- 从Predicate中排除password属性
仓库填充器
如果使用Spring JDBC模块,可能熟悉用SQL脚本来填充DataSource的支持。类似的抽象也在仓库级别可用,尽管它没有用SQL作为数据定义语言,因为它必须是存储无关的。这样填充器支持XML(通过Spring的OXM抽象)和JSON(通过Jackson)来定义要填充到仓库的数据。
假设你有一个如下内容的data.json文件:
数据定义在JSON
[ { "_class" : "com.acme.Person",
"firstname" : "Dave",
"lastname" : "Matthews" },
{ "_class" : "com.acme.Person",
"firstname" : "Carter",
"lastname" : "Beauford" } ]
可以通过Spring Data Commons提供的仓库命名空间里的装配器元素来填充仓库。要填充先前的数据到PersonRepository,声明一个类似下面的填充器:
声明一个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
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属性来决定。框架最终选择合适的仓库来处理返序列化的对象。
要使用XML定义仓库要填充的数据,可以使用unmarshaller-populator元素。可以用Spring OXM的一个XML序列化选项来配置它。详情参见Spring引用文档。下例展示了如何使用JAXB来解编仓库填充器。
声明一个解编仓库填充器(使用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>