核心概念
Repository
Repository 是Spring Data 的核心接口 。 它将domain 类 和 domain 类的ID作为管理参数 。 其他的Repository都继承实现该接口。 如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.
}
其他特定技术的抽象如 JpaRepository or MongoRepository 都是CrudRepository的子类。
在CrudRepository上层还有一个抽象接口 PagingAndSortingRepository , 提供分页排序功能:
public interface PagingAndSortingRepository
extends CrudRepository {
Iterable findAll(Sort sort);
Page findAll(Pageable pageable);
}
Query 方法
使用spring data 进行数据查询有如下四个步骤:
- 声明一个接口, 该接口需要继承Repository 或其子接口 ,并提供domain 类和id参数, 如下:
interface PersonRepository extends Repository { … }
- 在该接口中声明查询方法:
interface PersonRepository extends Repository {
List findByLastname(String lastname);
}
- 使用spring建立该接口的代理接口, 可以通过JavaConfig 或 Xml 配置。
3.1 使用Javaconfig
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
@EnableJpaRepositories
class Config {}
3.2 使用xml配置
上例使用了JPA名称空间。若使用其他的数据源,需要修改为其他的名称空间 。
注意:javaConfig 并咩有显示声明包名称, 因为默认会使用注解所在的包。 要修改默认需要使用 basePackage属性。
- 注入repository实例并使用之 :
class SomeClient {
private final PersonRepository repository;
SomeClient(PersonRepository repository) {
this.repository = repository;
}
void doSomething() {
List persons = repository.findByLastname("Matthews");
}
}
后续章节将详细介绍这4个步骤。
定义Repository 接口
首先,定义repository接口,需要指定domain class 和id 类型 , 可以直接继承Repository接口,或者其子接口如CrudRepository 。
调整Repository接口
一般的,定义repository接口需要继承 Repository, CrudRepository, or PagingAndSortingRepository. 但如果不想使用Spring data提供的接口,也可以自定义 。自定义接口上加 @RepositoryDefinition 注解。
当继承CrudRepository时, 会暴露所有的CRUD接口, 若不想暴露那么多 , 则可以直接从Repository继承, 如下所示:
@NoRepositoryBean
interface MyBaseRepository extends Repository {
Optional findById(ID id);
S save(S entity);
}
interface UserRepository extends MyBaseRepository {
User findByEmailAddress(EmailAddress emailAddress);
}
上例中, UserRepository 只会暴露 findById 、save findByEmailAddress接口, 不会暴露其他的 。
注意: @NoRepositoryBean 使spring data 不会生成相应repository的实例, 用在中间repository定义上。
Repository 的NULL处理
从spring data 2.0 开始 , repository的方法使用java 8 的Optional来表示可能缺少的值。 除此之外,spring data还为查询提供如下的封装类:
com.google.common.base.Optional
scala.Option
io.vavr.control.Option
javaslang.control.Option (deprecated as Javaslang is deprecated)
另外, 查询方法可以选择不使用这些封装类。 通过返回null表示没有查询结果 。 Repository 方法返回集合、变种集合、封装、流 时会保证不会出现null , 而是返回相应的空。
repository 方法的可空注解如下:
- @NonNullApi : 在包级别上使用,以声明参数和返回值的默认行为是不接受或生成空值。
- @NonNull : 使用在参数或返回值上,他们不能为null (在使用了@NonNullApi时不需要。)
- @Nullable : 使用在参数或返回值上, 表示可以为null。
如:在package-info.java 上声明:
@org.springframework.lang.NonNullApi
package com.acme;
如例:
package com.acme;
// 该包定义在我们声明的non-null包中;
import org.springframework.lang.Nullable;
interface UserRepository extends Repository {
// 当返回为空时抛出 EmptyResultDataAccessException 异常 。 当输入参数emailAddress空时抛出 IllegalArgumentException
User getByEmailAddress(EmailAddress emailAddress);
@Nullable
// 允许输入参数为空; 允许返回空值
User findByEmailAddress(@Nullable EmailAddress emailAdress);
// 当没有查询结果时返回Optional.empty() ; 当输入emailAddress为空时抛出IllegalArgumentException
Optional findOptionalByEmailAddress(EmailAddress emailAddress);
}
多数据源repository
当使用单一spring data module时,会很简单, 因为所有的repository都会绑定到该模块上。 但是当应用要求多个数据module时, 需要明确repository使用的模块 。
当spring 检查到引入多个数据module时, 他会按照如下规则进行判断:
- repository 是否继承自特定数据源, 将根据特定数据源进行判断。
- domain 类上是否有特定数据源的注解, spring data支持第三方的注解(如JPA的@Entity ) , 也有自己的注解 (如Mongo和Elasticsearch的@Document )
如下例子显示了使用JPA的例子:
interface MyRepository extends JpaRepository { }
@NoRepositoryBean
interface MyBaseRepository extends JpaRepository {
…
}
interface UserRepository extends MyBaseRepository {
…
}
MyRepository and UserRepository 继承自JpaRepository , 会使用spring data JPA。
如下例子 repository 继承通用的repository:
interface AmbiguousRepository extends Repository {
…
}
@NoRepositoryBean
interface MyBaseRepository extends CrudRepository {
…
}
interface AmbiguousUserRepository extends MyBaseRepository {
…
}
通过上述继承关系是无法判断的 。
在定义user类时, 若使用@Entity ,则使用的是 JPA ; 若使用的是@Document , 则使用的是mongo。
如下例子在person上同时使用了两个注解, 会引发异常:
interface JpaPersonRepository extends Repository {
…
}
interface MongoDBPersonRepository extends Repository {
…
}
@Entity
@Document
class Person {
…
}
在同一domain类型上使用多个持久性技术特定的注释是可能的,并允许跨多种持久性技术重用域类型。 但是,Spring Data不再能够确定用于绑定存储库的唯一模块。
区分repository的最后一种方法是定义使用repository的范围。 基础包定义了扫描repository接口定义的起点,这意味着将repository定义放在相应的包中。 默认情况下,注释驱动的配置使用配置类的包。 基于XML的配置中的基本包是必需的。
@EnableJpaRepositories(basePackages = "com.acme.repositories.jpa")
@EnableMongoRepositories(basePackages = "com.acme.repositories.mongo")
interface Configuration { }
定义查询方法
有两种方式来确定查询方法:
- 根据方法名推断
- 手工定义查询
查询策略
使用XML配置,您可以通过query-lookup-strategy属性在命名空间配置策略。 对于Java配置,您可以使用Enable $ {store}存储库注释的queryLookupStrategy属性。 特定数据存储可能不支持某些策略。
- CREATE: 从方法名构造特定数据源的查询语句 。 一般方法是去掉方法前缀,解析其余部分。后面有详细介绍。
- USE_DECLARED_QUERY : 查找声明的查询,未找到则会抛出异常 。 可以是注解或其他方式。 repository在启动时尝试查找相应数据源的声明, 未找到则fail。
- CREATE_IF_NOT_FOUND : 默认选项。 结合 CREATE and USE_DECLARED_QUERY 。 它首先查找声明,未声明则创建一个基于方法名的查询 。 这是默认方式。 它允许根据方法名快速定义,也允许自定义查询 。
Query Creation
其机制是剥离前缀find ... By,read ... By,query ... By,count ... By,and get ...By ,然后解析其余部分。同时也可以使用And Or distinct等等。
例:
interface PersonRepository extends Repository {
List findByEmailAddressAndLastname(EmailAddress emailAddress, String lastname);
// Enables the distinct flag for the query
List findDistinctPeopleByLastnameOrFirstname(String lastname, String firstname);
List findPeopleDistinctByLastnameOrFirstname(String lastname, String firstname);
// Enabling ignoring case for an individual property
List findByLastnameIgnoreCase(String lastname);
// Enabling ignoring case for all suitable properties
List findByLastnameAndFirstnameAllIgnoreCase(String lastname, String firstname);
// Enabling static ORDER BY for a query
List findByLastnameOrderByFirstnameAsc(String lastname);
List findByLastnameOrderByFirstnameDesc(String lastname);
}
最终的语句依赖不同的数据源有所不同, 但是有些共同的部分:
- 表达式是属性和运算符的结合, 支持AND 、 OR 、Between 、LessThen 、GreaterThen 、like 等, 另外因数据源不同有些操作符也不同。
- 方法解析器支持为各个属性设置IgnoreCase标志(例如,findByLastnameIgnoreCase(...))或支持所有属性忽略大小写(通常是String实例 - 例如,findByLastnameAndFirstnameAllIgnoreCase(...))。 是否支持忽略大小写可能因数据源不同而异,因此请参阅参考文档中有关查询方法的相关章节。
- 排序, 使用OrderBy后接字段来实现, 并可以指定方向(Asc或Desc)
属性表达式
如 : person中定义了Address , address 有zipCode字段:
List
spring data 会尝试去判断在哪儿进行分割 ,有可能会出现错误。
建议使用进行显示分割,以防其分割错误:
List
但是,
特殊参数处理
特定参数指分页 排序, Pageable and Sort 。 这俩参数将被特殊对待。
如下:
Page findByLastname(String lastname, Pageable pageable);
Slice findByLastname(String lastname, Pageable pageable);
List findByLastname(String lastname, Sort sort);
List findByLastname(String lastname, Pageable pageable);
第一个方法传入参数 org.springframework.data.domain.Pageable 以生成动态分页查询。 返回的Page对象有全部页数信息和当前页信息。 全部页数信息是通过一个计数语句完成的。这在某些数据源上可能会比较昂贵。 另一种方式是返回Slice结构, 该结构会包含一个是否还有下一页数据字段,这对大数据集非常有用。
Pageable 实例同时会处理sort 。 若只需要sort ,可以直接使用 org.springframework.data.domain.Sort 。 分页也可以只返回List, 这样不会触发count查询, 但是这只适用于查询给定范围的信息。
限制查询结果
查询结果可以通过附加first 和top 关键字来返回一部分数据。 first 和top后跟一个数字, 若没有数字, 则默认为1 。
例子:
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。
若限制查询中使用了 page 或slice , 则是对限制查询后的结果进行page 或slice 。
流化查询结果
可以将查询结果返回为Java8 Stream
例子:
@Query("select u from User u")
Stream findAllByCustomQueryAndStream();
Stream readAllByFirstnameNotNull();
@Query("select u from User u")
Stream streamAllPaged(Pageable pageable);
使用stream 需要在使用完后进行close , 可以手工close , 或使用try-with-resources结构:
try (Stream stream = repository.findAllByCustomQueryAndStream()) {
stream.forEach(…);
}
注意: 不是所有的spring data模块都支持stream
异步查询
某些数据源支持异步查询, 查询方法会立即返回 , 但是不会立即出结果 。
@Async
Future findByFirstname(String firstname);
@Async
CompletableFuture findOneByFirstname(String firstname);
@Async
ListenableFuture findOneByLastname(String lastname);
Use java.util.concurrent.Future as the return type.
Use a Java 8 java.util.concurrent.CompletableFuture as the return type.
Use a org.springframework.util.concurrent.ListenableFuture as the return type.
创建Repository 实例
定义完repository接口后, 需要定义该接口实例 。 一种方法是使用spring data module提供的名称空间配置, 但我们建议使用java configuration 。
xml 配置
每个spring data module 都包含一个 repositories 元素 , 它定义一个将扫描的base package 。
上例中, spring 将会扫描com.acme.repositories 及其子package ,寻找repository及其子接口。 对于每个找到的接口 , spring 都会使用特定数据源的FactoryBean 来创建合适的代理。 每个bean都会注册一个同接口名的bean 。 例如 UserRepository 接口的bean名即为UserRepository 。 另外 base-package属性支持通配符。
使用过滤的例子:
支持
javaConfig
在类上使用注解 @Enable${store}Repositories 可以同样实例化repository 。
如下例子 配置一个JPA:
@Configuration
@EnableJpaRepositories("com.acme.repositories")
class ApplicationConfiguration {
@Bean
EntityManagerFactory entityManagerFactory() {
// …
}
}
@Configuration @Bean
@Bean注解的角色与 xml中配置
@Bean可与Spring @Component一起使用,但是,它们最常用于@Configuration bean。
@Configuration 注解使用在类上 , 作为bean的定义源。在类中通过简单调用@Bean方法来表示bean间依赖关系。
如:
@Configuration
public class AppConfig {
@Bean
public MyService myService() {
return new MyServiceImpl();
}
}
等同于xml配置如下:
当@Bean 与 @Configuration一起使用时, @Bean可以定义bean间的依赖。
但当@Bean不与@Configuration一起使用时, 只是使用了Bean 工厂,不定义依赖。
独立使用
可以在Spring 容器之外使用repository 。 例如, 在CDI环境 。 但仍然需要spring librries。 spring data模块提供特定数据源的RepositoryFactory 以支持repository。 例:
RepositoryFactorySupport factory = … // Instantiate factory here
UserRepository repository = factory.getRepository(UserRepository.class);
自定义实现repository
- 首选自定义一个接口 , 如
interface CustomizedUserRepository {
void someCustomMethod(User user);
}
- 实现该接口 , 接口实现类必须以Impl结尾
class CustomizedUserRepositoryImpl implements CustomizedUserRepository {
public void someCustomMethod(User user) {
// Your custom implementation
}
}
- 定义repository时继承标准的repository和该自定义的
interface UserRepository extends CrudRepository, CustomizedUserRepository {
// Declare query methods here
}
这样客户端即可使用自定义实现的方法了 。
(当然也 同时支持多个自定义repository)
优先级
自定义方法的优先级高于通用repository 和 特定存储库提供的。
可以在自定义中覆盖通用或特定存储库的方法。
xml配置
当使用xml配置时, 同样遵循impl后缀原则 。 spring会扫描base-package下的自定义实现。
可以通过配置修改默认后缀, 如上。
歧义处理
若多个相同类名的 实现在不同的package中发现 , spring data 通过bean名称来确定使用哪一个。
例:
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
}
如上有CustomizedUserRepository 的两个实现, 则第一个实现的bean名符合约定, 使用之。
自定义base repository
如果需要为所有的repository添加base实现, 可以通过实现特定数据源的repository来实现。
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
}
}
配置时需要使用
@Configuration
@EnableJpaRepositories(repositoryBaseClass = MyRepositoryImpl.class)
class ApplicationConfiguration { … }
或
Aggregate roots 事件发布
在Domain-Driven Design 应用中, 聚合根发布domain 事件。
在domain类的方法中使用。
使用@DomainEvents 发布事件
使用@AfterDomainEventPublication 清理事件
这些方法会在调用save时被调用。
Spring data 扩展
目前,大多数扩展都是扩展spring mvc。
Querydsl扩展
Querydsl是一个框架,可以通过其流畅的API构建静态类型的SQL类查询。
有几款spring data module 通过 QuerydslPredicateExecutor 提供 Querydsl的整合。 如下:
public interface QuerydslPredicateExecutor {
Optional findById(Predicate predicate);
Iterable findAll(Predicate predicate);
long count(Predicate predicate);
boolean exists(Predicate predicate);
// … more functionality omitted.
}
- Finds and returns a single entity matching the Predicate.
- Finds and returns all entities matching the Predicate.
- Returns the number of entities matching the Predicate.
- Returns whether an entity that matches the Predicate exists.
然后通过继承QuerydslPredicateExecutor 来使用之:
interface UserRepository extends CrudRepository, QuerydslPredicateExecutor {
}
使用如下:
Predicate predicate = user.firstname.equalsIgnoreCase("dave")
.and(user.lastname.startsWithIgnoreCase("mathews"));
userRepository.findAll(predicate);
Web 支持
通过在configurationclass 上添加注解 @EnableSpringDataWebSupport 来支持:
@Configuration
@EnableWebMvc
@EnableSpringDataWebSupport
class WebConfiguration {}
@EnableSpringDataWebSupport 会自动添加几个组件。 另外他还会检测classpath上是否有HATEOAS , 有则自动注册之。
若使用基于xml的配置, 见下:
基础web 支持
- DomainClassConverter : 让Spring MVC从请求参数或路径变量中解析domain类的实例。
- HandlerMethodArgumentResolver : 让spring mvc 从请求参数中解析Pageable 和Sort实例。
DomainClassConverter
DomainClassConverter 允许在spring mvc中直接使用domain类, 而不用手工查找:
@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方法。
HandlerMethodArgumentResolvers
web支持会同时注册PageableHandlerMethodArgumentResolver 和 SortHandlerMethodArgumentResolver 。 这样controller就可以使用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 ,涉及如下参数:
- page : 想要获取的页码,从0开始 ,默认为0
- size : 每页条数 , 默认20
- sort : 排序属性 ,以property的格式
property,property(,ASC|DESC)
。默认是升序 。 支持多个参数,如: ?sort=firstname&sort=lastname,asc
要自定义该行为, 通过实现接口 PageableHandlerMethodArgumentResolverCustomizer 和 SortHandlerMethodArgumentResolverCustomizer , 实现其中的customize()方法。
若修改MethodArgumentResolver 满足不了需求, 可以通过继承SpringDataWebConfiguration 或 HATEOAS-enabled equivalent , 重写 pageableResolver() or sortResolver() 方法 ,导入自定义配置而不是使用@Enable注解。
若需要从request中解析出多组Pageable 或sort ,如多table情况, 可以使用spring 的@Qualifier标签。 同时request的参数前需要加${qualifier}_前缀 。 例:
String showUsers(Model model,
@Qualifier("thing1") Pageable first,
@Qualifier("thing2") Pageable second) { … }
默认传递到方法中的pageable 等同于 new PageRequest(0, 20) , 可以通过在Pageable参数上加注解@PageableDefault 来自定义 。
超媒体分页
Spring HATEOAS 提供PagedResources类来丰富分页信息。
将Page转换为PagedResources 由PagedResourcesAssembler 实现。
@Controller
class PersonController {
@Autowired PersonRepository repository;
@RequestMapping(value = "/persons", method = RequestMethod.GET)
HttpEntity> persons(Pageable pageable,
PagedResourcesAssembler assembler) {
Page persons = repository.findAll(pageable);
return new ResponseEntity<>(assembler.toResources(persons), HttpStatus.OK);
}
}
web 数据绑定
spring data 的projections(预测)可以用来绑定请求数据, 例子:
@ProjectedPayload
public interface UserPayload {
@XBRead("//firstname")
@JsonPath("$..firstname")
String getFirstname();
@XBRead("/lastname")
@JsonPath({ "$.lastname", "$.user.lastname" })
String getLastname();
}
对于spring mvc , 只要@EnableSpringDataWebSupport注解激活并且类路径上有相关的依赖, 则必要的转换器会自动被注册 。 要使用RestTemplate ,则需要手工注册 ProjectingJackson2HttpMessageConverter (JSON) or XmlBeamHttpMessageConverter 。
repository populators
当使用spring data jdbc模块时, 我们熟悉使用sql语言来与datasource交互 。 作为repository 级别的抽象, 不使用sql作为定义语言,因为他们是依赖数据源的。
populator 是支持json 和xml的 。
如有如下data.json文件:
[ { "_class" : "com.acme.Person",
"firstname" : "Dave",
"lastname" : "Matthews" },
{ "_class" : "com.acme.Person",
"firstname" : "Carter",
"lastname" : "Beauford" } ]
你可以使用repository命名空间的populator来操作repository 。 声明如下:
上述data.json文件将由Jackson的ObjectMapper来读取并序列化。
Json根据文件中的_class 来确定序列化的对象。
下例显示如何声明使用JAXB来处理xml: