Spring Data

核心概念

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 进行数据查询有如下四个步骤:

  1. 声明一个接口, 该接口需要继承Repository 或其子接口 ,并提供domain 类和id参数, 如下:
interface PersonRepository extends Repository { … }
  1. 在该接口中声明查询方法:
interface PersonRepository extends Repository {
  List findByLastname(String lastname);
}
  1. 使用spring建立该接口的代理接口, 可以通过JavaConfig 或 Xml 配置。
    3.1 使用Javaconfig
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;

@EnableJpaRepositories
class Config {}

3.2 使用xml配置




   


上例使用了JPA名称空间。若使用其他的数据源,需要修改为其他的名称空间 。

注意:javaConfig 并咩有显示声明包名称, 因为默认会使用注解所在的包。 要修改默认需要使用 basePackage属性。

  1. 注入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时, 他会按照如下规则进行判断:

  1. repository 是否继承自特定数据源, 将根据特定数据源进行判断。
  2. 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 findByAddressZipCode(ZipCode zipCode);

spring data 会尝试去判断在哪儿进行分割 ,有可能会出现错误。

建议使用进行显示分割,以防其分割错误:
List findByAddress_ZipCode(ZipCode zipCode);
但是,
在java中是保留字符, 不建议使用, 建议使用驼峰结构来表示。

特殊参数处理

特定参数指分页 排序, 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 结构, 或者使用特定数据源的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属性支持通配符。

使用过滤的例子:


  

支持 and 过滤 。

javaConfig

在类上使用注解 @Enable${store}Repositories 可以同样实例化repository 。

如下例子 配置一个JPA:

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

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

@Configuration @Bean

@Bean注解的角色与 xml中配置标签的角色一样。 其作用的方法上, 指示方法实例化、配置、初始化由Spring IoC容器管理的新对象。
@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

  1. 首选自定义一个接口 , 如
interface CustomizedUserRepository {
  void someCustomMethod(User user);
}
  1. 实现该接口 , 接口实现类必须以Impl结尾
class CustomizedUserRepositoryImpl implements CustomizedUserRepository {

  public void someCustomMethod(User user) {
    // Your custom implementation
  }
}
  1. 定义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:




  

  


你可能感兴趣的:(Spring Data)