Spring Data 综合

Spring Data 综合

Spring Data Repository 抽象的目标是显著减少各种访问持久化存储的样板式代码。

核心概念

Repository 是 Spring Data 的核心接口。此接口主要用作标记接口,以捕获要使用的类型并帮助您发现扩展此接口的接口。CrudRepositoryListCrudRepository 接口为被管理的实体类提供复杂的 CRUD 功能。ListCrudRepository 提供等效方法,但它们返回 List,而 CrudRepository 方法返回 Iterable

CrudRepository 接口定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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.
}

Spring Data 项目也提供了一些特定持久化技术的抽象接口,如:JpaRepository 或 MongoRepository。这些接口扩展了 CrudRepository 并暴露了一些持久化技术的底层功能。

除了 CrudRepository 之外,还有一个 PagingAndSortingRepository 接口,它添加了额外的方法来简化对实体的分页访问:

1
2
3
4
5
6
public interface PagingAndSortingRepository<T, ID>  {

Iterable<T> findAll(Sort sort);

Page<T> findAll(Pageable pageable);
}

【示例】要按页面大小 20 访问 User 的第二页,可以执行如下操作

1
2
PagingAndSortingRepository<User, Long> repository = // … get access to a bean
Page<User> users = repository.findAll(PageRequest.of(1, 20));

除了查询方法之外,计数和删除时的查询也是可用的。

【示例】根据姓氏计数

1
2
3
interface UserRepository extends CrudRepository<User, Long> {
long countByLastname(String lastname);
}

【示例】根据姓氏删除

1
2
3
4
5
6
interface UserRepository extends CrudRepository<User, Long> {

long deleteByLastname(String lastname);

List<User> removeByLastname(String lastname);
}

查询方法

使用 Spring Data 对数据库进行查询有以下四步:

  1. 声明一个扩展 Repository 或其子接口的接口,并指定泛型类型(实体类和 ID 类型),如以下示例所示:

    1
    interface PersonRepository extends Repository<Person, Long> { … }
  2. 在接口中声明查询方法

    1
    2
    3
    interface PersonRepository extends Repository<Person, Long> {
    List<Person> findByLastname(String lastname);
    }
  3. 使用 JavaConfigXML 配置为这些接口创建代理实例

    1
    2
    @EnableJpaRepositories
    class Config { … }
  4. 注入 Repository 实例并使用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    class SomeClient {

    private final PersonRepository repository;

    SomeClient(PersonRepository repository) {
    this.repository = repository;
    }

    void doSomething() {
    List<Person> persons = repository.findByLastname("Matthews");
    }
    }

定义 Repository

首先需要定义一个 Repository 接口,该接口必须扩展 Repository 并且指定泛型类型(实体类和 ID 类型)。如果想为该实体暴露 CRUD 方法,可以扩展 CrudRepository 接口。

微调 Repository 定义

Spring Data 提供了很多种 Repository 以应对不同的需求场景。

CrudRepository 提供了 CRUD 功能。

ListCrudRepositoryCrudRepository 类似,但对于那些返回多个实体的方法,它返回一个 List 而不是 Iterable,这样使用可能更方便。

如果使用响应式框架,可以使用 ReactiveCrudRepositoryRxJava3CrudRepository

CoroutineCrudRepository 支持 Kotlin 的协程特性。

PagingAndSortingRepository 提供了分页、排序功能。

如果不想扩展 Spring Data 接口,还可以使用 @RepositoryDefinition 注释您的 Repository 接口。 扩展一个 CRUD Repository 接口,需要暴露一组完整的方法来操作实体。如果希望对暴露的方法有选择性,可以将要暴露的方法从 CRUD Repository 复制到自定义的 Repository 中。 这样做时,可以更改方法的返回类型。 如果可能,Spring Data 将遵循返回类型。 例如,对于返回多个实体的方法,可以选择 Iterable<T>List<T>Collection<T>VAVR 列表。

自定义基础 Repository 接口,必须用 @NoRepositoryBean 标记。 这可以防止 Spring Data 尝试直接创建它的实例并失败,因为它无法确定该 Repository 的实体,因为它仍然包含一个通用类型变量。

以下示例显示了如何有选择地暴露 CRUD 方法(在本例中为 findById 和 save):

1
2
3
4
5
6
7
8
9
10
11
@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);
}

使用多个 Spring 数据模块

有时,程序中需要使用多个 Spring Data 模块。在这种情况下,必须区分持久化技术。当检测到类路径上有多个 Repository 工厂时,Spring Data 进入严格的配置模式。

如果定义的 Repository 扩展了特定模块中的 Repository,则它是特定 Spring Data 模块的有效候选者。

如果实体类使用了特定模块的类型注解,则它是特定 Spring Data 模块的有效候选者。 Spring Data 模块接受第三方注解(例如 JPA 的 @Entity)或提供自己的注解(例如用于 Spring Data MongoDB 和 Spring Data Elasticsearch 的 @Document)。

以下示例显示了一个使用模块特定接口(在本例中为 JPA)的 Repository:

1
2
3
4
5
6
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 模块的有效候选者。

以下示例显示了一个使用通用接口的 Repository

1
2
3
4
5
6
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 模块时很好,但是存在多个模块时,无法区分这些 Repository 应该绑定到哪个特定的 Spring Data。

以下示例显示了一个使用带注解的实体类的 Repository

1
2
3
4
5
6
7
8
9
interface PersonRepository extends Repository<Person, Long> { … }

@Entity
class Person { … }

interface UserRepository extends Repository<User, Long> { … }

@Document
class User { … }

PersonRepository 引用 Person,它使用 JPA @Entity 注解进行标记,因此这个 Repository 显然属于 Spring Data JPA。 UserRepository 引用 User,它使用 Spring Data MongoDB 的 @Document 注解进行标记。

以下错误示例显示了一个使用带有混合注解的实体类的 Repository

1
2
3
4
5
6
7
interface JpaPersonRepository extends Repository<Person, Long> { … }

interface MongoDBPersonRepository extends Repository<Person, Long> { … }

@Entity
@Document
class Person { … }

此示例中的实体类同时使用了 JPA 和 Spring Data MongoDB 的注解。示例中定义了两个 Repository:JpaPersonRepository 和 MongoDBPersonRepository。 一个用于 JPA,另一个用于 MongoDB。 Spring Data 不再能够区分 Repository,这会导致未定义的行为。

区分 Repository 的最后一种方法是确定 Repository 扫描 package 的范围。

1
2
3
@EnableJpaRepositories(basePackages = "com.acme.repositories.jpa")
@EnableMongoRepositories(basePackages = "com.acme.repositories.mongo")
class Configuration { … }

定义查询方法

Repository 代理有两种方法可以从方法名称派生特定于存储的查询:

  • 通过直接从方法名称派生查询。
  • 通过使用手动定义的查询。

可用选项取决于实际存储。但是,必须有一个策略来决定创建什么实际查询。

查询策略

以下策略可用于Repository 基础结构来解析查询。 对于 Java 配置,您可以使用 EnableJpaRepositories 注释的 queryLookupStrategy 属性。 特定数据存储可能不支持某些策略。

  • CREATE 尝试从查询方法名称构造特定存储的查询。
  • USE_DECLARED_QUERY 尝试查找已声明的查询,如果找不到则抛出异常。
  • CREATE_IF_NOT_FOUND (默认)结合了 CREATEUSE_DECLARED_QUERY

查询创建

Spring Data 中有一套内置的查询构建器机制,可以自动映射符合命名和参数规则的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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 在要创建的查询上设置不同的标志或 Top/First 限制查询结果。

参考:

Spring Data 支持的查询主语关键词

Spring Data 支持的查询谓语关键词

创建 Repository 实例

自定义 Repository 实现

Spring Data 扩展

参考资料