SpringData学习

Spring Data 为数据访问提供熟悉且一致的基于 Spring 的编程模型,同时仍保留底层数据存储的特殊特征。

包含许多基于特定数据库的子项目,类似于面向接口编程,Spring Data 提供了统一接口,然后各种关系型和非关系型数据库,MapReduce 框架自行提供实现。

特性:(我关注的几个特性)

  • Powerful repository and custom object-mapping abstractions

    强大的 repository 和自定义对象映射抽象

  • Dynamic query derivation from repository method names

    基于 repository 的方法名称派生出动态查询 掌握 repository 中查询方法名的命名规则非常重要

  • Implementation domain base classes providing basic properties

    可以提供基本属性的实现 domain 基类

  • Possibility to integrate custom repository code

    可以集成自定义 repository 存储库代码

  • Easy Spring integration via JavaConfig and custom XML namespaces

    通过 JavaConfig 和自定义 XML 命名空间轻松集成 Spring

主要模块

  • Spring Data Commons - 支撑每个 Spring Data 模块的核心 Spring 概念。

    Spring Data Commons 是 Spring Data 项目的一部分,该项目提供跨 Spring Data 项目的共享基础架构。它包含技术中立的存储库接口以及用于持久化 Java 类的元数据模型。

  • 其他 Spring Data 模块分别对应各种不同的数据访问实现。

注意:这些模块都是单独发布,所以会出现各种不同的版本号,使用时需要注意不同版本的兼容性。

找到兼容版本的最简单方法是依赖我们随定义的兼容版本一起提供的 Spring Data Release Train BOM。(这个东西在哪里?)

Spring Data Commons

我认为这部分对重要的两个内容就是

  • 创建对象映射,(这部分是将 Java 对象与底层数据库中的表映射到一起,为后面查询代码提供表信息)

  • 创建 repository 的查询方法,(这部分是将查询方法名与底层数据库的查询语句基本规则进行映射,两者结合可以动态生成查询语句所需要的所有信息)

下面的内容上面两个点来展开。

3. Object Mapping Fundamentals

对象映射基本原则/基本知识

本节涵盖 Spring Data 对象映射、对象创建、字段和属性访问、可变性和不变性的基础知识。

This section covers the fundamentals of Spring Data object mapping, object creation, field and property access, mutability and immutability.

因为不同数据库实现提供的解析规则会有些不同,所以实际使用时,需要查看 Spring Data 支持的这些数据存储实现文档中这部分对应的规则。例如,对象映射,例如索引、自定义列或字段名称等。

Spring Data 对象映射的核心职责是创建域对象的实例并将存储原生(store-native)数据结构映射到这些实例上。这意味着我们需要两个基本步骤:

  1. 使用公开的构造函数之一创建实例
  2. 实例填充以具体化所有公开的属性。

3.1. Object creation

对象创建

Spring Data 会自动尝试检测(要用于具体化该类型对象的持久化)实体entity的构造函数。解析算法的工作原理如下:

Spring Data automatically tries to detect a persistent entity’s constructor to be used to materialize objects of that type.

  1. 如果只有单个构造函数,直接使用;
  2. 如果有无参构造函数,直接使用无参构造函数,忽略其他构造函数;
  3. 如果有多个构造函数,且存在 @PersistenceConstructor 注解,使用被注解标注的构造函数;

关于值解析?

值解析假定构造函数参数名称与实体的属性名称匹配,即解析将被执行,就像要填充属性一样,包括映射中的所有自定义(不同的数据存储列或字段名称等)。这还需要类文件中可用的参数名称信息或构造函数中存在的 @ConstructorProperties 注释。

值解析可以通过使用 Spring Framework 的 @Value 值注释使用特定存储的 SpEL 表达式进行自定义。有关更多详细信息,请参阅有关存储特定映射的部分。

这部分应该放在属性填充部分吧?

为了避免反射的开销,Spring Data 的对象创建,默认使用运行时生成的工厂类,它会直接调用域类(domain classes)的构造函数。(这比反射提高了大约 10% 的性能)

To avoid the overhead of reflection

例如:

1
2
3
4
5
6
7
8
9
10
11
class Person {
Person(String firstname, String lastname) { … }
}

// Spring Data 将在运行时创建一个语义上等同于这个的工厂类:直接调用 Person 的构造函数创建对象实例。
class PersonObjectInstantiator implements ObjectInstantiator {

Object newInstance(Object... args) {
return new Person((String) args[0], (String) args[1]);
}
}

但是有资格进行此类优化的域类(domain class),需要满足以下几个条件:

  • domain class 不能是 private 的;
  • 如果有内部类,该内部类必须是 static 的;
  • 它不能是 CGLib 代理类(proxy class);
  • Spring Data 使用的构造函数不能是 private 的

如果不满足上面任一条件,Spring Data 就会改为使用反射进行对象创建。

3.2. Property population

属性填充

当实例创建之后,Spring Data 会填充(这个类剩余的所有需要持久化的)属性。

  • 一旦创建了实体的实例,Spring Data 就会填充该类的所有剩余持久属性。除非实体的构造函数已经填充(即通过其构造函数参数填充),否则将首先填充标识符属性(用 @Id 注解标注的属性)以允许循环对象引用的解析。之后,所有尚未由构造函数填充的非瞬态属性(non-transient properties)都在实体实例上设置。

属性填充使用下面的算法规则:

  1. 如果属性是不可变的(immutable)(有 final 修饰),但公开(expose) 了 with... 方法(见下文),我们使用 with... 方法创建一个具有新属性值的新实体实例(a new entity instance)。
  2. 如果定义了属性访问(即通过 getter 和 setter 访问),我们将调用 setter 方法。
  3. 如果属性是可变的(mutable)(没有 final 修饰),我们直接设置字段。
  4. 如果属性是不可变的,我们将使用构造函数在创建实例时填充。
  5. 默认情况下,我们直接设置字段值。

与在对象构造中的优化类似,我们也使用 Spring Data 运行时生成的访问器类来填充实体实例中的属性。

这使我们比反射提高了大约 25% 的性能。对于有资格进行此类优化的域类(domain class),它需要遵守一组约束:

  • 类型(types)不得位于默认值或 java 包下;–这里不太懂,从英文表述上,(感觉是属性的类型必须是项目的子目录下,不能在项目的根目录下,即不能在src/main/java 这个目录下,需要在其下的某个包内。)

    Types must not reside in the default or under the java package.

  • 类型及其构造函数必须是 public 的;

  • 作为内部类的类型必须是静态的(static);

  • 使用的 Java 运行时(Java Runtime)必须允许在原始 ClassLoader 中声明类。 Java 9 和更新版本施加了某些限制。

默认情况下,Spring Data 会尝试使用生成的属性访问器(property accessors),并在检测到超出限制时回退到基于反射的访问器。

问题:如何查看 Spring Data 是否使用了 运行时生成的实例化器或者属性访问器?而不是反射?查一下

问题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
  private String firstname;
// TODO(SLi): 这个注解的作用是什么?Type.FIELD 和 Type.PROPERTY 的区别是什么?
private @AccessType(Type.PROPERTY) String lastname;
void setLastname(String lastname) {
this.lastname = lastname;
}


private static final MethodHandle firstname;

if ("firstname".equals(name)) {
firstname.invoke(person, (String) value);
} else if ("lastname".equals(name)) {
this.person.setLastname((String) value);
}

我的理解:

  • Spring Data 默认使用 @AccessType(Type.FIELD) 字段访问类型,进行属性值的读写,这个时候,如果是 private 类型的,由于可见性规则,填充属性值时,需要在运行时生成的属性访问器中使用 MethodHandles与该字段进行交互;

  • 使用 @AccessType(Type.PROPERTY) 注解后,则允许直接调用方法,而无需使用 MethodHandles

  • 但是不对啊,firstname如果也添加一个 setter 方法不是也可以直接使用了?在属性填充的算法规则中,如果属性定义了 setter 访问器,会直接使用 setter。

3.3. General recommendations

官方一般建议

  • 尽量坚持使用不可变对象 — 不可变对象很容易创建,因为实现对象只需调用其构造函数即可。仅构造函数实现比属性填充快 30%。同时可以避免域对象(domain object)被 setter 方法影响。
  • 提供一个全参数构造函数 — 这允许对象映射以跳过属性填充的方式获得最佳性能。
  • 使用工厂方法而不是重载构造函数,避免使用 @PersistenceConstructor注解 — 为了获得最佳性能需要一个全参数构造函数,我们通常希望公开更多应用程序用例特定的构造函数,这些构造函数省略自动生成的标识符等内容。使用静态工厂方法来公开全参构造函数的这些变体是一种既定的模式。
    • ??? 这里不太明白,使用工厂方法,避免重载构造函数,避免使用@PersistenceConstructor 注解,到这里我明白,但是之后的内容我就不太理解,可能是翻译的不太对。
  • 确保遵守允许使用生成的实例化器和属性访问器类的约束;
  • 对于要生成的标识符,仍然使用 final 字段与全参数持久性构造函数(首选)或 with… 方法结合使用;
  • 可以选择使用 Lombok 避免大量模板代码,Lombok’s @AllArgsConstructor 可以代替全参构造函数。(这部分可选,根据情况。)

4. Working with Spring Data Repositories

使用 Spring Data 的 repository

Spring Data repository 抽象的目标是,减少为各种持久性存储(实现数据访问层)所需的样板代码量。

问题:

  1. Spring Data 的特性之一:基于 repository 的方法名称派生出动态查询,

    1. 使用这条特性,需要哪些规则?怎样自定义/派生才能生效?

      例如:下面这个例子,countByLastname这个方法名称的规则是什么?

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

      long countByLastname(String lastname);
      }
    2. 它的原理是怎样的?怎样实现的?

我的理解:

  1. 要达到简单使用框架的目的,需要:

    • 了解知道要用的模块/部分的使用规则,然后按照规则一步步执行。例如,使用 repository 接口需要按照官方文档按照4个步骤依次实现并填充相关接口,剩下的交给框架就可以。再如,使用 Spark 时,知道依赖什么jar包,先创建什么,然后创建什么,方法怎样传入,怎样打包运行,剩下的就交给框架就好了。
    • 快速上手的过程是把 example 内容跑起来,然后抄一下改一改,接着就要把基本规则了解一下,深入时就需要更全面了解规则。
  2. 了解原理或者实现细节的目的 :

    • 当现有框架提供的基本方法无法满足你的某个需求时,通过了解框架某部分的实现原理,可以知道它的工作流程,例如,它一共做了5步,你可以通过重写第三部的内容,让框架运行时加载你的实现方法,从而满足你的这个需求的。
    • 可以需要哪里看哪里,也可以全部都看一遍都掌握。
  3. [Defining Repository Interfaces](# 4.3. Defining Repository Interfaces)

  4. [Defining Query Methods](# 4.4. Defining Query Methods)

  5. [Creating Repository Instances](# 4.5. Creating Repository Instances)

  6. [Custom Implementations for Spring Data Repositories](# 4.6. Custom Implementations for Spring Data Repositories)

4.1. Core concepts

核心概念

underlying datastore 底层数据存储

1
public interface Repository<T, ID>

关于核心接口 Repository

  1. Spring Data repository 抽象的中心接口是 Repository 。它将要管理/处理的域类(domain class)以及域类的 ID 类型作为类型参数。

  2. Repository 接口的主要作用是标记,一是获取传入的 domain class 信息,二是用于判断满足 instance of Repository 条件的接口,用于创建 bean。

  3. 通常创建完映射的表对象后,会创建相应的 repository 接口用来执行查询语句。这些自定义的 domain repository 会继承 repository 或者其子接口。

1
public interface CrudRepository<T, ID> extends Repository<T, ID>

CrudRepository 接口

  • Repository 有几个子接口(具体可以在源码中查看),我们最常用的是 CrudRepository 接口。
  • 为正在管理的实体类提供了复杂的 CRUD 功能。
  • 其他特定存储模块都是继承并扩展了 CrudRepository 接口,并基于自己特定的底层持久化技术做相关实现然后对外提供相关接口方法。例如,JpaRepository 和 MongoRepository
  • 分页访问,在 CrudRepository 之上,有一个 PagingAndSortingRepository 抽象,它添加了额外的方法来简化对实体的分页访问
1
2
3
4
5
6
7
8
9
10
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));

4.3. Defining Repository Interfaces

  • Defining a domain class-specific Repository Interfaces (定义一个特定的域类 repository 接口)

    • 要定义一个特定的domain class Repository 接口,首先是扩展 Repository 或其子接口,同时将要处理的域类(domain class) 和域类的 ID 字段的类型,当做参数传入。如以下示例所示:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      样例1
      interface PersonRepository extends Repository<Person, Long> { … }

      样例2,我们实现了一个所有 domain 的公共基类 repository 接口,同时提供了 findById 和 save 功能,然后所有domain calss继承这个 base repository 接口即可。

      // 注意,所有不需要运行时实例化的中间repository 标记 `@NoRepositoryBean` 注解即可。查看源码我们可知,CrudRepository 和 PagingAndSortingRepository 这两个中间 repository 也都有这个注解。
      @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);
      }
    • 通常我们可以继承3种公共接口和各种特定存储实现的接口,

      • Repository , 最基础的接口,如果希望继承该接口但是又觉得功能不够,可以将 CrudRepository 或者 其他子接口中的部分方法 copy 到我们定义的 domain repository 接口中;
      • CrudRepository , 提供复杂的 CRUD 功能的 repository 接口;
      • PagingAndSortingRepository ,除了提供 CRUD 功能外,还提供了分页查询功能;
      • 以上是公共接口,同时还可以继承各种特定数据存储模块实现的独特接口,例如 JpaRepository,MongoRepository …
    • 当使用了多种数据存储模块时,如既有 JPA,也有MongoDB,此时需要严格区分 repository 定义与 Spring Data 模块的绑定。因为定义 repository 时需要指定 domain class ,所以:

      1. 要么定义的 domain repository 是继承自特定模块实现的接口 ;
    1. 要么是 domain class 使用了特定模块的注解做标注;(最好不要同一个 domain class 复用不同数据存储模块的注解,Spring Data 不容易区分它绑定到哪个模块的 repository 上。)

    2. 区分 repository 的最后一种方法是对存储库基础包进行范围界定。基本包决定了扫描 repository interface definitions (存储库接口定义)的起点。

      这点与 Spring Boot 配置普通 Bean 时一样,都是在配置类上用独特的注解进行标注,且通过 basePackages 进行基本包扫描范围的限制。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
      正确样例:
    样例1,继承自特定模块的repository
    interface MyRepository extends JpaRepository<User, Long> { }

    @NoRepositoryBean
    interface MyBaseRepository<T, ID> extends JpaRepository<T, ID> { … }

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

    样例2,定义的repository 接口使用了标注了特定模块的注解
    // 这里虽然定义的两个 repository 都继承了公共的 Repository,但是使用的 domain class 已经有明确归属。
    interface PersonRepository extends Repository<Person, Long> { … }
    // 这个注解是 JPA 的,所以使用了它定义的 PersonRepository 很显然属于 Spring Data JPA。
    @Entity
    class Person { … }

    interface UserRepository extends Repository<User, Long> { … }
    // 这个注解是 MongoDB的,所以 UserRepository 自然属于 Spring Data MongoDB。
    @Document
    class User { … }

    错误样例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    下面定义的两个 repository ,当使用单个Spring Data 模块时,没有问题。但是当使用多个 Spring Data 模块时,是无法正确分区他们与哪个模块绑定的。
    interface AmbiguousRepository extends Repository<User, Long> { … }

    @NoRepositoryBean
    interface MyBaseRepository<T, ID> extends CrudRepository<T, ID> { … }

    interface AmbiguousUserRepository extends MyBaseRepository<User, Long> { … }


    下面这个例子,Person 即使用了 JPA 的注解标注,又使用了 MongoDB 的注解标注,同时使用了公共接口,导致在多模块时也是无法区分绑定到哪个模块。
    interface JpaPersonRepository extends Repository<Person, Long> { … }

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

    @Entity
    @Document
    class Person { … }

    通过定义扫描 repository 时的 basePackages 值,限定了扫描的范围,同时也达到了区分不同实现的目的。

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

中间

  • Defining Query Methods (定义查询方法)

    1
    2
    3
    interface PersonRepository extends Repository<Person, Long> {
    List<Person> findByLastname(String lastname);
    }
  • 通过 JavaConfig 或者 XML configuration 配置 Spring 来为接口创建代理实例。

    1
    2
    3
    // 这里使用了 JPA 的样例
    @EnableJpaRepositories
    class Config { … }
  • Inject the repository instance and use it

@NoRepositoryBean

  • 所有不需要运行时实例化的中间repository 标记 @NoRepositoryBean 注解即可。查看源码我们可知,CrudRepositoryPagingAndSortingRepository 这两个中间 repository 也都有这个注解。这样 Spring Data 在运行时就不会将这些有标记的 repository 类注册为 Bean。

4.4. Defining Query Methods

定义查询方法

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

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

创建查询必须有一个策略来决定创建什么实际查询。

4.4.1. Query 查找策略

Query Lookup Strategies

声明查找策略的方式:

  • 使用 XML Configuration , 可以配置命名空间的 query-lookup-strategy 属性;

  • 使用 Java Configuration ,可以配置 Enable${store}Repositories 注解中的 queryLookupStrategy 属性;

    例如,@EnableMongoRepositories

具体策略:(涉及方法创建的部分,查看下面 Query Creation 的内容)

  • CREATE , 尝试从查询方法名称构造特定储存的查询。一般的方法是从方法名称中删除一组给定的前缀,然后解析方法的其余部分。
  • USE_DECLARED_QUERY , 尝试寻找已定义的查询,找不到就抛异常,这个需要查看具体存储在其文档中的可用项。
  • CREATE_IF_NOT_FOUND默认值),结合了 CREATEUSE_DECLARED_QUERY。它首先查找声明的查询,如果没有找到声明的查询,它会创建一个自定义的基于方法名称的查询。这是默认的查找策略,因此,如果您没有明确配置任何内容,就会使用它。它允许按方法名称快速定义查询,还允许通过根据需要引入声明的查询来自定义这些查询。

4.4.2. Query 创建

Query Creation

从方法名创建查询

Spring Data 存储库基础结构中内置的查询构建器机制 对于构建 对 repository 实体有约束的查询非常有用。

  • 内置查询构建器机制,作用:构建查询

解析查询方法名称:

解析查询方法名称,分为主语和谓语。第一部分(find...Byexists...By)定义查询的主语,第二部分形成谓词。介绍从句(这里指主语)可以包含进一步的表达。 find(或其他引入关键字)和 By 之间的任何文本都被认为是描述性的,除非使用结果限制关键字之一,例如 Distinct 在要创建的查询上设置不同的标志或使用 Top/First 来限制查询结果。

The introducing clause (subject) can contain further expressions. Any text between find (or other introducing keywords) and By is considered to be descriptive。 这里的 introducing 怎么翻译比较合理?

了解查询方法支持的主语和条件谓语的关键字是重点

附录包含 查询方法主语关键字(query method subject keywords )和查询方法谓词关键字(query method predicate keywords including sorting and letter-casing modifiers)的完整列表,包括排序和字母大小写修饰符。但是,第一个 By 充当分隔符表明实际标准谓词的开始。在非常基础的层面上,您可以在实体属性上定义条件并将它们与 And 和 Or 连接起来。

查询语句中的谓语,是指,条件(conditions)或条件表达式(将多个条件语句使用 AND 或 OR 连接一起)

定义查询方法的常规注意事项:

虽然解析方法的实际结果取决于您创建查询时所使用的持久化存储。但是也有一些常规注意事项。

  • 表达式通常是将属性(property)与操作符(operator)连接在一起。多个属性表达式可以使用 ANDOR 结合使用。一个属性表达式可以使用的操作符例如,Between, LessThan, GreaterThan, 和 Like,可以使用的操作符因数据存储而异,具体需要查看对应文档的对应部分。
  • 方法解析器支持设置 IgnoreCase 标志,为单个属性(例如 findByLastnameIgnoreCase(…))或支持忽略大小写的类型的所有属性(通常是 String 实例,例如 findByLastnameAndFirstnameAllIgnoreCase(…))。是否支持忽略大小写可能因数据存储而异,因此请参阅参考文档中的相关部分以了解数据存储特定的查询方法。
  • 您可以通过将 OrderBy 子句进行静态排序,并提供排序方向(AscDesc)。要创建支持动态排序的查询方法,请参阅“特殊参数处理”(“Special parameter handling”).
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);
}

4.4.3. 嵌套属性解析/处理方式

Property Expressions

属性表达式只能引用被管理实体的直接属性,例如前面样例所示,通常在创建实例类时已经确定了。但是,当遇到含有嵌套属性时,需要额外注意(可能会产生解析歧义,参考样例1)。

最佳实践是当实体类中包含嵌套类作为属性,且该属性用于创建查询方法时,使用_ 手动定义遍历分割点,即将这个嵌套属性与该属性中涉及的子属性名分开。(参考样例2)

1
2
3
4
5
6
7
8
9
// 样例希望处理Person类中有一个 Address 嵌套类作为属性,Address中有一个 ZipCode 属性字段。

// 样例1:
// 这样写大多数情况可以,但是根据属性解析算法,当实体类中包含 addressZip 属性时,就会解析错误。
List<Person> findByAddressZipCode(ZipCode zipCode);

// 样例2:
// 最好的处理方式:
List<Person> findByAddress_ZipCode(ZipCode zipCode);

上面的例子中,是假设 Person 类有一个带有 ZipCodeAddress 属性。 在这种情况下,该方法会创建 x.address.zipCode 属性遍历。 解析算法首先将整个部分 (AddressZipCode) 解释为属性并检查具有该名称(未大写)的属性的域类。 如果算法成功,它将使用该属性。 如果不是,则算法将来自右侧的驼峰式部分的源拆分为头和尾,并尝试找到相应的属性 — 在我们的示例中为 AddressZipCode。 如果算法找到具有该头部的属性,它会取尾部并继续从那里向下构建树,以刚才描述的方式将尾部拆分。 如果第一个拆分不匹配,则算法将拆分点向左移动AddressZipCode)并继续。

因为 Spring Data 将下划线(_)视为保留字符,所以强烈建议遵循标准的 Java 命名约定(即,要使用驼峰式大小写,不要在属性名称中使用下划线)。

问题:什么是 the managed entity,或 the managed domain class ?

答:在定义 repository 接口时,当做参数传入其中的 实体类,就是被 repository 接口管理的 entity。

4.4.4. 查询方法的动态分页排序

Special parameter handling

Using Pageable, Slice, and Sort in query methods

动态进行分页排序查询。

1
2
3
4
5
6
7
8
9
// 分页
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);

第一个方法允许我们将 org.springframework.data.domain.Pageable 实例传递给查询方法,可以将分页动态添加到静态定义的查询中。Page 知道可用元素和页面的总数。它通过基础结构触发计数查询(count query)来计算总数。由于这可能开销比较大(取决于所使用的存储),我们可以改为返回 SliceSlice 只知道下一个 Slice 是否可用(不触发计数查询的消耗),这在遍历更大的结果集时可能就足够了,例如第二个方法。

排序也通过 Pageable 实例处理。如果只需要排序,请将 org.springframework.data.domain.Sort 参数添加到方法中。如您所见,返回一个 List 也是可能的。在这种情况下,不会创建构建实际 Page 实例所需的附加元数据(不会触发额外的计数查询(count query))。相反(这里应该是指除了排序还需要分页),它限制查询仅查找给定范围的实体。

要了解整个查询获得了多少页,必须触发额外的计数查询。默认情况下,此查询来自实际触发的查询。

Paging and Sorting

定义排序表达式更类型安全的方法是,先定义排序表达式的类型,然后使用方法引用来定义排序的属性。

1
2
3
4
5
6
7
8
// 定义排序表达式
Sort sort = Sort.by("firstname").ascending()
.and(Sort.by("lastname").descending());

// 使用类型安全 API 定义排序表达式
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,您还可以使用生成的元模型类型来定义排序表达式:

1
2
3
// 使用 Querydsl API 定义排序表达式
QSort sort = QSort.by(QPerson.firstname.asc())
.and(QSort.by(QPerson.lastname.desc()));

4.4.5. Limiting Query Results

  • Top / TopN,例如:Top5,默认 Top 表示1;
  • First / FirstN,例如:First5,默认 First 表示1;

您可以通过使用 firsttop 关键字来限制查询方法的结果,这两个关键字可以互换使用(作用相同)。您可以将可选数值附加到 top 或 first 以指定要返回的最大结果大小。如果忽略该数字,则假定结果大小为 1。以下示例显示了如何限制查询大小:

Limiting the result size of a query with Top and First

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

4.4.6. 处理集合类返回值

Repository Methods Returning Collections or Iterables

返回集合或可迭代对象的存储库方法

支持集合的返回值类型

  • Java Iterable
  • List
  • Set
  • Steamable (Spring Data 支持的)
  • Iterable 的自定义扩展(Spring Data 支持的)
  • Vavr 提供的集合类型

返回多个结果的查询方法可以使用标准的 Java IterableListSet。除此之外,我们支持返回 Spring Data 的 StreamableIterable 的自定义扩展,以及 Vavr 提供的集合类型。请参阅解释所有可能的查询方法返回类型的附录。

Using Streamable as Query Method Return Type

在 Spring Data 中,可以使用 Streamable 作为 Iterable 或任何集合类型的代替方案。

  • 提供了访问非并行 Stream 的方法;
  • 可以直接在元素上调用 filter(),map()方法;
  • 支持将多个结果使用例如 and 方法连接到一起返回。如下面的样例1。

使用Streamable合并查询方法结果

1
2
3
4
5
6
7
8
// 样例1:
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"));
Returning Custom Streamable Wrapper Types

返回自定义流包装器类型

TODO,这部分先跳过,用到时再看

4.4.7. Null Handling of Repository Methods

存储库方法的空处理

两种方法处理 null

  1. 返回值使用包装类,保证不会返回null;
  2. 使用与null 有关的注解标注,进行检查参数和结果;

如果repository 方法返回单个实例,可以使用 java 8 的 Optional 来代替返回null 值,同时 Spring Data 支持使用下面几种包装器作为返回值:

  • com.google.common.base.Optional
  • scala.Option
  • io.vavr.control.Option

如果不使用包装器,那么可以直接返回 null 值来表示没有查询结果。

返回集合、集合替代、包装器和流的 repository 方法保证永远不会返回 null 而是返回相应的空表示没有查询结果。

Nullability Annotations
  • @NonNullApi: 在包级别(package level)用于声明参数和返回值的默认行为是既不接受也不产生空值。
  • @NonNull:用在不能为空的参数或返回值处(设置了@NonNullApi的参数和返回值不需要再设置)。
  • @Nullable:用在可以为 null 的参数或返回值。

Spring 注释使用 JSR 305 注释(一种休眠但广泛使用的 JSR)进行元注释。 JSR 305 元注释让工具供应商(如 IDEA、Eclipse 和 Kotlin)以通用方式提供空安全支持,而无需对 Spring 注释进行硬编码支持。要为查询方法启用可空性约束的运行时检查,您需要通过在 package-info.java 中使用 Spring 的 @NonNullApi 在包级别激活非可空性,如下例所示:

Declaring Non-nullability in package-info.java

在 package-info.java 中声明不可为空

1
2
@org.springframework.lang.NonNullApi
package com.acme;

一旦非空默认设置到位,存储库查询方法调用将在运行时验证为可空性约束。如果查询结果违反了定义的约束,则抛出异常。当该方法将返回 null 但被声明为不可为 null 时会发生这种情况(默认情况下,在存储库所在的包上定义了注释)。如果您想再次选择可空结果,请有选择地对各个方法使用 @Nullable。使用本节开头提到的结果包装器类型继续按预期工作:空结果被转换为表示不存在的值。

Using different nullability constraints

使用不同的可空性约束

1
2
3
4
5
6
7
8
9
10
11
12
13
package com.acme;      // 参考上面的例子,已经定义了 non-null(非空) 行为                                                 

import org.springframework.lang.Nullable;

interface UserRepository extends Repository<User, Long> {

User getByEmailAddress(EmailAddress emailAddress); // 当查询未产生结果时抛出 EmptyResultDataAccessException。当传递给方法的 emailAddress 为 null 时,抛出 IllegalArgumentException。

@Nullable
User findByEmailAddress(@Nullable EmailAddress emailAdress); // 当查询未产生结果时返回 null。还接受 null 作为 emailAddress 的值。

Optional<User> findOptionalByEmailAddress(EmailAddress emailAddress); // 当查询未产生结果时返回 Optional.empty()。当传递给方法的 emailAddress 为 null 时,抛出 IllegalArgumentException。
}

4.4.8. Streaming Query Results

流式查询结果

关于流,除了使用 Spring Data 支持的 Streamable 作为返回结果外,还可以使用 Java8 支持的 Stream<T> 作为返回结果。

但是在实际调用时,因为 Stream 可能包装底层数据存储特定的资源,因此必须在使用后关闭。您可以使用 close() 方法或使用 Java 7 try-with-resources 块关闭 Stream,如以下示例所示:

1
2
3
try (Stream<User> stream = repository.findAllByCustomQueryAndStream()) {
stream.forEach(…);
}

注意:并非所有 Spring Data 模块当前都支持 Stream 作为返回类型。具体需要参考实现模块文档。

4.4.9. 异步查询结果

Asynchronous Query Results

Spring 中关于异步方法执行的具体内容参考官方文档:Spring’s asynchronous method running capability.

使用 @Async 注解执行方法的异步查询。

1
2
3
4
5
6
7
8
9
10
11
// 使用 java.util.concurrent.Future 作为返回值;
@Async
Future<User> findByFirstname(String firstname);

// Java 8 中的 java.util.concurrent.CompletableFuture 作为返回值;
@Async
CompletableFuture<User> findOneByFirstname(String firstname);

// 使用 org.springframework.util.concurrent.ListenableFuture 作为返回值;
@Async
ListenableFuture<User> findOneByLastname(String lastname);

Spring 的异步方法运行功能异步运行存储库查询。这意味着该方法在调用时立即返回,而实际查询发生在已提交给 Spring TaskExecutor 的任务中。异步查询不同于反应式查询,不应混合使用。有关反应式支持的更多详细信息,请参阅特定存储的文档。

4.5. Creating Repository Instances

如何为已定义的 repository 接口创建实例和 bean 定义?

  • 使用 XML 中的命名空间
  • Java配置 (Java Configuration)(推荐)
    • 通过在 Java 配置类上使用特定存储的 @Enable${store}Repositories 注解来触发存储库基础结构。

Spring 容器(Spring container)基于 Java 的配置( Java-based configuration)的详细内容参考官方文档, JavaConfig in the Spring reference documentation.

4.6. Custom Implementations for Spring Data Repositories

Spring Data Repositories 的自定义实现 ?

暂时理解为当按照提供的规则无法创建需要的 查询方法时,可以利用自定义方式来处理。

以下示例显示了一个使用默认后缀的存储库和一个为后缀设置自定义值的存储库:

1
2
3
4
5
6
<repositories base-package="com.acme.repository" />  // 默认是 Impl,尝试查找名为 com.acme.repository.CustomizedUserRepositoryImpl 的类

<repositories base-package="com.acme.repository" repository-impl-postfix="MyPostfix" /> // 尝试查找名为 com.acme.repository.CustomizedUserRepositoryMyPostfix 的类

Java Configuration 配置类似
@EnableElasticsearchRepositories(repositoryImplementationPostfix = "xxx")

Resolution of Ambiguity

解决歧义

如果在不同的包中找到多个能匹配类名的实现,Spring Data 使用 bean 名称来标识使用哪个。

与普通的 Bean 相同,

  1. 默认情况下,使用首字母小写的类名作 Bean 名称,默认匹配用接口名+Impl 后缀 来匹配 Bean 名称。
  2. 可以使用注解来自定义 Bean 名称,例如,使用 @Component("specialCustomImpl") 来标注后,就会使用自定义的名称代替默认 Bean 名称。 –需要测试验证。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 第一个包中使用默认的
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
}

Manual Wiring

手动接线

4.6.2. Customize the Base Repository

4.7. Publishing Events from Aggregate Roots

从聚合根发布事件

什么是 aggregate root ???

Entities managed by repositories are aggregate roots. In a Domain-Driven Design application, these aggregate roots usually publish domain events.

存储库管理的实体是聚合根。在域驱动设计应用程序中,这些聚合根通常发布域事件。

每次调用 Spring Data 存储库 save(…)saveAll(…)delete(…)delete All(…) 方法之一时都会调用这些方法。

5. Projections

类似 Select 语句中 选择指定字段输出

基础处理中,我们在 repository 的查询方法中只返回整个实体类,高级处理时,我们有返回 repository 管理的实体类中部分属性的需求,就像 select 部分字段一样。

此时,使用基于接口的投影。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 简单使用时返回被管理的实体 Person
class Person {

@Id UUID id;
String firstname, lastname;
Address address;

static class Address {
String zipCode, city, street;
}
}

interface PersonRepository extends Repository<Person, UUID> {

Collection<Person> findByLastname(String lastname);
}

The aggregate root 多次出现这个概念,需要回去看一下表达的是什么。

default ,Java 8 中出现在 interface 中的新方法,表达什么意思呢?

5.1. Interface-based Projections

当我们想要返回被管理实体中的指定字段时,最简单的方法是,重新定义一个新的接口(interface),然后将需要返回的属性字段的方法暴露出来。

1
2
3
4
5
6
7
8
9
10
11
12
// 定义一个仅返回名称属性的接口,然后再 PersonRepository 中用于查询方法的返回值。
interface NamesOnly {

String getFirstname();
String getLastname();
}


interface PersonRepository extends Repository<Person, UUID> {

Collection<NamesOnly> findByLastname(String lastname);
}

如果实体类有内部嵌套类属性,嵌套类也选择字段返回,那么与外部类也一样处理,例如:

1
2
3
4
5
6
7
8
9
10
11
// 样例2:这里新接口 PersonSummary 仅返回 名字和地址 三个字段,且地址字段是嵌套类作为属性仅有 city 属性返回,那么嵌套类也要新建接口,如下:
interface PersonSummary {

String getFirstname();
String getLastname();
AddressSummary getAddress();

interface AddressSummary {
String getCity();
}
}

下面这部分内容等实际用到时再参考,Spring Data Commons - Reference Documentation

5.1.1. Closed Projections

新建的接口中涉及的属性与实体属性完全一致时,我们称这种投影接口为 closed projects,否则为 open projects

5.1.2. Open Projections

5.1.3. Nullable Wrappers

5.2. Class-based Projections (DTOs)

5.3. Dynamic Projections

将需要返回的类型当做查询方法的一个参数传入。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
样例1
interface PersonRepository extends Repository<Person, UUID> {

<T> Collection<T> findByLastname(String lastname, Class<T> type);
}

void someMethod(PersonRepository people) {

Collection<Person> aggregates =
people.findByLastname("Matthews", Person.class);

Collection<NamesOnly> aggregates =
people.findByLastname("Matthews", NamesOnly.class);
}

6. Query by Example

JPA 和 MyBatis 中都有涉及 example,什么是 query by example?什么是动态查询

Appendix C: Repository query keywords

Keyword Description
find…By, read…By, get…By, query…By, search…By, stream…By General query method returning typically the repository type, a Collection or Streamable subtype or a result wrapper such as Page, GeoResults or any other store-specific result wrapper. Can be used as findBy…, findMyDomainTypeBy… or in combination with additional keywords.
exists…By Exists projection, returning typically a boolean result.
count…By Count projection returning a numeric result.
delete…By, remove…By Delete query method returning either no result (void) or the delete count.
…First<number>…, …Top<number>… Limit the query results to the first <number> of results. This keyword can occur in any place of the subject between find (and the other keywords) and by.
…Distinct… Use a distinct query to return only unique results. Consult the store-specific documentation whether that feature is supported. This keyword can occur in any place of the subject between find (and the other keywords) and by.
  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!
  • Copyrights © 2022-2023 ligongzhao
  • 访问人数: | 浏览次数:

请我喝杯咖啡吧~

支付宝
微信