© 2018-2022 原作者。
本文档的副本可以供您自己使用和分发给其他人,前提是您不对此类副本收取任何费用,并且进一步前提是每份副本都包含本版权声明,无论是印刷版还是电子版。 |
前言
Spring Data JDBC 项目将核心 Spring 概念应用于开发使用符合域驱动设计原则的JDBC 数据库的解决方案。我们提供“模板”作为存储和查询聚合的高级抽象。
本文档是 Spring Data JDBC Support 的参考指南。它解释了概念、语义和语法。
本节提供一些基本介绍。文档的其余部分仅涉及 Spring Data JDBC 特性,并假定用户熟悉 SQL 和 Spring 概念。
1.学习Spring
Spring Data 使用 Spring 框架的核心功能,包括:
虽然您不需要了解 Spring API,但了解它们背后的概念很重要。至少,您应该熟悉控制反转 (IoC) 背后的理念,并且您应该熟悉您选择使用的任何 IoC 容器。
JDBC Aggregate 支持的核心功能可以直接使用,无需调用 Spring Container 的 IoC 服务。这很像JdbcTemplate
,可以在没有任何其他 Spring 容器服务的情况下“独立”使用。要利用 Spring Data JDBC 的所有特性,例如存储库支持,您需要配置库的某些部分以使用 Spring。
要了解有关 Spring 的更多信息,您可以参考详细解释 Spring 框架的综合文档。有很多关于这个主题的文章、博客条目和书籍。有关更多信息,请参阅 Spring 框架主页。
2. 要求
Spring Data JDBC 二进制文件需要 JDK 8.0 及更高级别和Spring Framework 5.3.21 及更高版本。
在数据库方面,Spring Data JDBC 需要一种方言来抽象通用 SQL 功能而不是供应商特定的风格。Spring Data JDBC 包括对以下数据库的直接支持:
-
DB2
-
H2
-
数据库
-
玛丽亚数据库
-
微软 SQL 服务器
-
MySQL
-
甲骨文
-
Postgres
如果您使用不同的数据库,那么您的应用程序将无法启动。方言部分包含有关在这种情况下如何进行的更多详细信息。
3. 其他帮助资源
学习一个新框架并不总是那么简单。在本节中,我们将尝试提供我们认为易于理解的 Spring Data JDBC 模块入门指南。但是,如果您遇到问题或需要建议,请随时使用以下链接之一:
- 社区论坛
-
Stack Overflow上的Spring Data是所有 Spring Data(不仅仅是 Document)用户共享信息和互相帮助的标签。请注意,仅发布时需要注册。
- 专业支持
-
Spring Data 和 Spring 背后的公司Pivotal Software, Inc.提供专业的源代码支持和有保证的响应时间。
4. 跟随发展
有关 Spring Data JDBC 源代码存储库、夜间构建和快照工件的信息,请参阅 Spring Data JDBC主页。您可以通过Stack Overflow上的社区与开发人员互动,帮助 Spring Data 更好地满足 Spring 社区的需求。如果您遇到错误或想提出改进建议,请在Spring Data 问题跟踪器上创建工单。要及时了解 Spring 生态系统中的最新消息和公告,请订阅 Spring 社区门户。您还可以在 Twitter ( SpringData )上关注 Spring博客或项目团队。
6.新的和值得注意的
本节介绍每个版本的重大更改。
6.1。Spring Data JDBC 2.3 的新特性
-
支持流式传输结果。
-
支持将投影类型指定为返回类型或使用泛型并为查询方法提供 Class 参数。
6.2. Spring Data JDBC 2.2 的新特性
-
Page
并Slice
支持派生查询。
6.4. Spring Data JDBC 2.0 的新特性
-
乐观锁定支持。
-
支持
PagingAndSortingRepository
. -
查询派生。
-
完全支持 H2。
-
默认情况下,所有 SQL 标识符都知道被引用。
-
缺少列不再导致异常。
7. 依赖
由于各个 Spring Data 模块的启动日期不同,它们中的大多数都带有不同的主要和次要版本号。找到兼容版本的最简单方法是依赖我们随定义的兼容版本一起发布的 Spring Data Release Train BOM。在 Maven 项目中,您将在<dependencyManagement />
POM 的部分中声明此依赖项,如下所示:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-bom</artifactId>
<version>2021.2.1</version>
<scope>import</scope>
<type>pom</type>
</dependency>
</dependencies>
</dependencyManagement>
当前的发布火车版本是2021.2.1
. 火车版使用calver和花纹YYYY.MINOR.MICRO
。${calver}
GA 版本和服务版本的版本名称如下,所有其他版本的版本名称如下: ${calver}-${modifier}
,其中modifier
可以是以下之一:
-
SNAPSHOT
: 当前快照 -
M1
,M2
, 等等:里程碑 -
RC1
,RC2
, 等等:发布候选
您可以在我们的Spring Data 示例存储库中找到使用 BOM 的工作示例。有了它,你可以在块中声明你想使用的没有版本的 Spring Data 模块<dependencies />
,如下所示:
<dependencies>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-jpa</artifactId>
</dependency>
<dependencies>
7.1。使用 Spring Boot 进行依赖管理
Spring Boot 会为您选择最新版本的 Spring Data 模块。如果您仍想升级到较新版本,请将spring-data-releasetrain.version
属性设置为您想要使用的训练版本和迭代。
8. 使用 Spring 数据存储库
Spring Data repository 抽象的目标是显着减少为各种持久性存储实现数据访问层所需的样板代码量。
Spring Data 存储库文档和您的模块 |
8.1。核心概念
Spring Data 存储库抽象中的中央接口是Repository
. 它需要域类来管理以及域类的 ID 类型作为类型参数。此接口主要用作标记接口,以捕获要使用的类型并帮助您发现扩展此接口的接口。该CrudRepository
接口为被管理的实体类提供了复杂的 CRUD 功能。
CrudRepository
接口public interface CrudRepository<T, ID> extends Repository<T, ID> {
<S extends T> S save(S entity); (1)
Optional<T> findById(ID primaryKey); (2)
Iterable<T> findAll(); (3)
long count(); (4)
void delete(T entity); (5)
boolean existsById(ID primaryKey); (6)
// … more functionality omitted.
}
1 | 保存给定的实体。 |
2 | 返回由给定 ID 标识的实体。 |
3 | 返回所有实体。 |
4 | 返回实体的数量。 |
5 | 删除给定的实体。 |
6 | 指示具有给定 ID 的实体是否存在。 |
我们还提供了特定于持久性技术的抽象,例如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);
}
8.2. 查询方法
标准 CRUD 功能存储库通常对底层数据存储进行查询。使用 Spring Data,声明这些查询变成了一个四步过程:
-
声明一个扩展 Repository 或其子接口之一的接口,并将其键入应处理的域类和 ID 类型,如下例所示:
interface PersonRepository extends Repository<Person, Long> { … }
-
在接口上声明查询方法。
interface PersonRepository extends Repository<Person, Long> { List<Person> findByLastname(String lastname); }
-
设置 Spring 以使用JavaConfig或XML configuration为这些接口创建代理实例。
-
要使用 Java 配置,请创建一个类似于以下内容的类:
import org.springframework.data.jpa.repository.config.EnableJpaRepositories; @EnableJpaRepositories class Config { … }
-
要使用 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 变体没有显式配置包,因为默认使用带注释的类的包。要自定义要扫描的包,请使用特定于数据存储的存储库的-annotation的
basePackage…
属性之一。@Enable${store}Repositories
-
-
注入存储库实例并使用它,如下例所示:
class SomeClient { private final PersonRepository repository; SomeClient(PersonRepository repository) { this.repository = repository; } void doSomething() { List<Person> persons = repository.findByLastname("Matthews"); } }
以下部分详细解释了每个步骤:
8.3. 定义存储库接口
要定义存储库接口,您首先需要定义特定于域类的存储库接口。接口必须扩展Repository
并输入到域类和 ID 类型。如果要公开该域类型的 CRUD 方法,请扩展CrudRepository
而不是Repository
.
8.3.1. 微调存储库定义
通常,您的存储库接口会扩展Repository
、CrudRepository
或PagingAndSortingRepository
. 或者,如果您不想扩展 Spring Data 接口,也可以使用@RepositoryDefinition
. 扩展CrudRepository
公开了一整套操作实体的方法。如果您希望对公开的方法有选择性,请将要公开的方法复制CrudRepository
到您的域存储库中。
这样做可以让您在提供的 Spring Data Repositories 功能之上定义自己的抽象。 |
以下示例显示了如何选择性地公开 CRUD 方法(在本例中为findById
和save
):
@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 提供的您选择的存储的基本存储库实现(例如,如果您使用 JPA,实现是SimpleJpaRepository
),因为它们与CrudRepository
. 所以UserRepository
现在可以保存用户,通过 ID 查找单个用户,并触发查询以Users
通过电子邮件地址查找。
中间存储库接口用@NoRepositoryBean . 确保将该注释添加到 Spring Data 不应在运行时为其创建实例的所有存储库接口。
|
8.3.2. 将存储库与多个 Spring 数据模块一起使用
在应用程序中使用唯一的 Spring Data 模块会使事情变得简单,因为定义范围内的所有存储库接口都绑定到 Spring Data 模块。有时,应用程序需要使用多个 Spring Data 模块。在这种情况下,存储库定义必须区分持久性技术。当检测到类路径上有多个存储库工厂时,Spring Data 进入严格的存储库配置模式。严格配置使用存储库或域类的详细信息来决定存储库定义的 Spring Data 模块绑定:
-
如果存储库定义扩展了特定于模块的存储库,则它是特定 Spring Data 模块的有效候选者。
-
如果域类使用特定于模块的类型注释进行注释,则它是特定 Spring Data 模块的有效候选者。Spring Data 模块接受第三方注解(例如 JPA 的
@Entity
)或提供自己的注解(例如@Document
Spring Data MongoDB 和 Spring Data Elasticsearch)。
以下示例显示了使用特定于模块的接口(在本例中为 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
扩展。虽然在使用唯一的 Spring Data 模块时这很好,但多个模块无法区分这些存储库应该绑定到哪个特定的 Spring Data。Repository
CrudRepository
以下示例显示了一个使用带注释的域类的存储库:
interface PersonRepository extends Repository<Person, Long> { … }
@Entity
class Person { … }
interface UserRepository extends Repository<User, Long> { … }
@Document
class User { … }
PersonRepository
references Person
,使用 JPA@Entity
注释进行注释,因此这个存储库显然属于 Spring Data JPA。UserRepository
references 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 { … }
8.4. 定义查询方法
存储库代理有两种方法可以从方法名称派生特定于存储的查询:
-
通过直接从方法名称派生查询。
-
通过使用手动定义的查询。
可用选项取决于实际商店。但是,必须有一种策略来决定创建什么实际查询。下一节将介绍可用的选项。
8.4.1. 查询查找策略
以下策略可用于存储库基础架构来解决查询。使用 XML 配置,您可以通过query-lookup-strategy
属性在命名空间配置策略。对于 Java 配置,您可以使用注解的queryLookupStrategy
属性。Enable${store}Repositories
特定数据存储可能不支持某些策略。
-
CREATE
尝试从查询方法名称构造特定于存储的查询。一般的方法是从方法名称中删除一组给定的已知前缀并解析方法的其余部分。您可以在“查询创建”中阅读有关查询构造的更多信息。 -
USE_DECLARED_QUERY
尝试查找已声明的查询,如果找不到则抛出异常。查询可以由某处的注释定义或通过其他方式声明。请参阅特定商店的文档以查找该商店的可用选项。如果存储库基础结构在引导时没有找到该方法的声明查询,它就会失败。 -
CREATE_IF_NOT_FOUND
(默认)结合CREATE
和USE_DECLARED_QUERY
。它首先查找已声明的查询,如果未找到已声明的查询,则创建一个基于自定义方法名称的查询。这是默认查找策略,因此,如果您未明确配置任何内容,则使用此策略。它允许通过方法名称快速定义查询,还可以通过根据需要引入声明的查询来自定义调整这些查询。
8.4.2. 查询创建
Spring Data 存储库基础架构中内置的查询构建器机制对于在存储库的实体上构建约束查询很有用。
以下示例显示了如何创建多个查询:
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
来限制查询结果。
附录包含查询方法主题关键字和查询方法谓词关键字的完整列表,包括排序和字母大小写修饰符。但是,第一个By
用作分隔符以指示实际条件谓词的开始。在非常基本的级别上,您可以在实体属性上定义条件并将它们与And
和连接起来Or
。
解析方法的实际结果取决于您为其创建查询的持久性存储。但是,有一些一般的事情需要注意:
-
表达式通常是结合了可以连接的运算符的属性遍历。您可以将属性表达式与
AND
和结合使用OR
。您还可以获得对运算符(如Between
、LessThan
、和 )的支持GreaterThan
,以及Like
对属性表达式的支持。支持的运算符可能因数据存储而异,因此请参阅参考文档的相应部分。 -
方法解析器支持
IgnoreCase
为单个属性(例如,findByLastnameIgnoreCase(…)
)或支持忽略大小写的类型的所有属性(通常是String
实例 - 例如,findByLastnameAndFirstnameAllIgnoreCase(…)
)设置标志。是否支持忽略大小写可能因商店而异,因此请参阅参考文档中的相关部分以了解商店特定的查询方法。 -
您可以通过将子句附加
OrderBy
到引用属性的查询方法并提供排序方向(Asc
或Desc
)来应用静态排序。要创建支持动态排序的查询方法,请参阅“特殊参数处理”。
8.4.3. 属性表达式
属性表达式只能引用托管实体的直接属性,如前面的示例所示。在创建查询时,您已经确保解析的属性是托管域类的属性。但是,您也可以通过遍历嵌套属性来定义约束。考虑以下方法签名:
List<Person> findByAddressZipCode(ZipCode zipCode);
假设 aPerson
有Address
一个ZipCode
。在这种情况下,该方法会创建x.address.zipCode
属性遍历。解析算法首先将整个部分 ( AddressZipCode
) 解释为属性,并检查域类以查找具有该名称(未大写)的属性。如果算法成功,它将使用该属性。如果不是,该算法将源在驼峰部分从右侧拆分为头部和尾部,并尝试找到相应的属性 - 在我们的示例中,AddressZip
和Code
。如果算法找到具有该头部的属性,它将获取尾部并继续从那里向下构建树,以刚才描述的方式将尾部拆分。如果第一个分割不匹配,算法将分割点向左移动 ( Address
,ZipCode
) 并继续。
虽然这应该适用于大多数情况,但算法可能会选择错误的属性。假设这个Person
类也有一个addressZip
属性。该算法已经在第一个拆分轮中匹配,选择了错误的属性,然后失败(因为 的类型addressZip
可能没有code
属性)。
要解决这种歧义,您可以_
在方法名称中使用手动定义遍历点。所以我们的方法名称如下:
List<Person> findByAddress_ZipCode(ZipCode zipCode);
因为我们将下划线字符视为保留字符,我们强烈建议遵循标准 Java 命名约定(即,不在属性名称中使用下划线,而是使用驼峰式大小写)。
8.4.4. 特殊参数处理
要处理查询中的参数,请定义前面示例中已经看到的方法参数。除此之外,该基础架构还可以识别某些特定类型,例如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);
API 接受Sort 并Pageable 期望将非null 值传递给方法。如果您不想应用任何排序或分页,请使用Sort.unsorted() and Pageable.unpaged() 。
|
第一种方法允许您将org.springframework.data.domain.Pageable
实例传递给查询方法,以动态地将分页添加到静态定义的查询中。APage
知道可用元素和页面的总数。它通过基础设施触发计数查询来计算总数来实现这一点。由于这可能很昂贵(取决于使用的商店),您可以改为返回Slice
. ASlice
只知道下一个Slice
是否可用,这在遍历更大的结果集时可能就足够了。
排序选项也通过Pageable
实例处理。如果您只需要排序,org.springframework.data.domain.Sort
请在您的方法中添加一个参数。如您所见,返回 aList
也是可能的。在这种情况下,不会创建构建实际实例所需的额外元数据Page
(这反过来意味着不会发出必要的额外计数查询)。相反,它将查询限制为仅查找给定范围的实体。
要了解整个查询获得了多少页,您必须触发额外的计数查询。默认情况下,此查询派生自您实际触发的查询。 |
分页和排序
您可以使用属性名称定义简单的排序表达式。您可以连接表达式以将多个条件收集到一个表达式中。
Sort sort = Sort.by("firstname").ascending()
.and(Sort.by("lastname").descending());
要以更安全的方式定义排序表达式,请从要为其定义排序表达式的类型开始,并使用方法引用来定义要排序的属性。
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,您还可以使用生成的元模型类型来定义排序表达式:
QSort sort = QSort.by(QPerson.firstname.asc())
.and(QSort.by(QPerson.lastname.desc()));
8.4.5。限制查询结果
first
您可以使用or关键字来限制查询方法的结果top
,您可以互换使用它们。您可以将可选数值附加到top
或first
指定要返回的最大结果大小。如果省略该数字,则假定结果大小为 1。以下示例显示了如何限制查询大小:
Top
示例 18. 使用and限制查询的结果大小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”个最小元素和“K”个最大元素的查询方法。
|
8.4.6。返回集合或迭代的存储库方法
返回多个结果的查询方法可以使用标准的 Java Iterable
、List
和Set
. 除此之外,我们还支持返回 Spring Data 的Streamable
自定义扩展Iterable
,以及Vavr提供的集合类型。请参阅解释所有可能的查询方法返回类型的附录。
使用 Streamable 作为查询方法返回类型
您可以Streamable
用作任何集合类型的替代品Iterable
或任何集合类型。它提供了方便的方法来访问非并行Stream
(缺少Iterable
)以及直接….filter(…)
和….map(…)
覆盖元素并将其连接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 Wrapper 类型
为集合提供专用的包装器类型是为返回多个元素的查询结果提供 API 的常用模式。通常,通过调用返回类似集合类型的存储库方法并手动创建包装器类型的实例来使用这些类型。您可以避免该额外步骤,因为 Spring Data 允许您使用这些包装器类型作为查询方法返回类型,如果它们满足以下条件:
-
类型实现
Streamable
. -
该类型公开了一个构造函数或一个名为
of(…)
或作为参数的静态工厂方法。valueOf(…)
Streamable
以下清单显示了一个示例:
class Product { (1)
MonetaryAmount getPrice() { … }
}
@RequiredArgsConstructor(staticName = "of")
class Products implements Streamable<Product> { (2)
private final Streamable<Product> streamable;
public MonetaryAmount getTotal() { (3)
return streamable.stream()
.map(Priced::getPrice)
.reduce(Money.of(0), MonetaryAmount::add);
}
@Override
public Iterator<Product> iterator() { (4)
return streamable.iterator();
}
}
interface ProductRepository implements Repository<Product, Long> {
Products findAllByDescriptionContaining(String text); (5)
}
1 | 公开 API 以访问产品价格的Product 实体。 |
2 | Streamable<Product> 可以通过使用Products.of(…) (使用 Lombok 注释创建的工厂方法)构造的包装器类型。采用意志的标准构造函数Streamable<Product> 也可以。 |
3 | 包装器类型公开了一个额外的 API,用于计算Streamable<Product> . |
4 | 实现Streamable 接口并委托给实际结果。 |
5 | 该包装器类型Products 可以直接用作查询方法返回类型。您不需要Streamable<Product> 在存储库客户端中的查询之后返回并手动包装它。 |
支持 Vavr 集合
Vavr是一个包含 Java 函数式编程概念的库。它附带一组自定义集合类型,您可以将其用作查询方法返回类型,如下表所示:
Vavr 集合类型 | 使用的 Vavr 实现类型 | 有效的 Java 源类型 |
---|---|---|
|
|
|
|
|
|
|
|
|
您可以使用第一列中的类型(或其子类型)作为查询方法返回类型,并获取第二列中的类型作为实现类型,具体取决于实际查询结果的 Java 类型(第三列)。或者,您可以声明Traversable
(VavrIterable
等效项),然后我们从实际返回值派生实现类。也就是说,ajava.util.List
变成 VavrList
或Seq
,ajava.util.Set
变成 Vavr LinkedHashSet
Set
,依此类推。
8.4.7。存储库方法的空处理
从 Spring Data 2.0 开始,返回单个聚合实例的存储库 CRUD 方法使用 Java 8Optional
来指示可能缺少值。除此之外,Spring Data 支持在查询方法上返回以下包装类型:
-
com.google.common.base.Optional
-
scala.Option
-
io.vavr.control.Option
或者,查询方法可以选择根本不使用包装器类型。然后通过返回来指示不存在查询结果null
。返回集合、集合替代、包装器和流的存储库方法保证永远不会返回null
,而是返回相应的空表示。有关详细信息,请参阅“存储库查询返回类型”。
可空性注释
您可以使用Spring Framework 的可空性注释来表达存储库方法的可空性约束。它们在运行时提供了一种工具友好的方法和选择加入null
检查,如下所示:
-
@NonNullApi
: 在包级别上用于声明参数和返回值的默认行为分别是既不接受也不产生null
值。 -
@NonNull
: 用于不能使用的参数或返回值null
(在适用的情况下不需要用于参数和返回值@NonNullApi
)。 -
@Nullable
: 用在参数或返回值上即可null
。
Spring 注释使用JSR 305注释(一种休眠但广泛使用的 JSR)进行元注释。JSR 305 元注释让工具供应商(例如IDEA、Eclipse和Kotlin)以通用方式提供空安全支持,而无需对 Spring 注释进行硬编码支持。要为查询方法启用可空性约束的运行时检查,您需要使用 Spring 的@NonNullApi
in在包级别激活不可空性package-info.java
,如下例所示:
package-info.java
@org.springframework.lang.NonNullApi
package com.acme;
一旦非空默认设置到位,存储库查询方法调用将在运行时验证可空性约束。如果查询结果违反了定义的约束,则会引发异常。当方法将返回null
但被声明为不可为空(默认情况下,在存储库所在的包上定义的注释)时,就会发生这种情况。如果您想再次选择可空结果,请有选择地使用@Nullable
单个方法。使用本节开头提到的结果包装类型继续按预期工作:空结果被转换为表示缺席的值。
以下示例显示了刚刚描述的一些技术:
package com.acme; (1)
import org.springframework.lang.Nullable;
interface UserRepository extends Repository<User, Long> {
User getByEmailAddress(EmailAddress emailAddress); (2)
@Nullable
User findByEmailAddress(@Nullable EmailAddress emailAdress); (3)
Optional<User> findOptionalByEmailAddress(EmailAddress emailAddress); (4)
}
1 | 存储库位于我们为其定义了非空行为的包(或子包)中。 |
2 | EmptyResultDataAccessException 当查询不产生结果时抛出一个。IllegalArgumentException 当emailAddress 交给方法时抛出一个null 。 |
3 | null 当查询没有产生结果时返回。也接受null 作为 的值emailAddress 。 |
4 | Optional.empty() 当查询没有产生结果时返回。IllegalArgumentException 当emailAddress 交给方法时抛出一个null 。 |
基于 Kotlin 的存储库中的可空性
Kotlin 将可空性约束的定义融入到语言中。Kotlin 代码编译为字节码,它不通过方法签名表达可空性约束,而是通过编译的元数据。确保kotlin-reflect
在您的项目中包含 JAR 以启用对 Kotlin 可空性约束的自省。Spring Data 存储库使用语言机制来定义这些约束以应用相同的运行时检查,如下所示:
interface UserRepository : Repository<User, String> {
fun findByUsername(username: String): User (1)
fun findByFirstname(firstname: String?): User? (2)
}
1 | 该方法将参数和结果都定义为不可为空(Kotlin 默认)。Kotlin 编译器拒绝传递给方法的方法调用null 。如果查询产生空结果,EmptyResultDataAccessException 则抛出 an。 |
2 | 此方法接受null 参数firstname 并null 在查询未产生结果时返回。 |
8.4.8。流式查询结果
Stream<T>
您可以使用 Java 8作为返回类型以增量方式处理查询方法的结果。不是将查询结果包装在 a 中Stream
,而是使用特定于数据存储的方法来执行流式传输,如以下示例所示:
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);
AStream 可能包装底层数据存储特定的资源,因此必须在使用后关闭。Stream 您可以使用该close() 方法或使用 Java 7块手动关闭try-with-resources ,如以下示例所示:
|
Stream<T>
中的结果try-with-resources
try (Stream<User> stream = repository.findAllByCustomQueryAndStream()) {
stream.forEach(…);
}
并非所有 Spring Data 模块当前都支持Stream<T> 作为返回类型。
|
8.4.9。异步查询结果
您可以使用Spring 的异步方法运行能力异步运行存储库查询。这意味着该方法在调用时立即返回,而实际查询发生在已提交给 Spring 的任务中TaskExecutor
。异步查询与响应式查询不同,不应混用。有关响应式支持的更多详细信息,请参阅特定于商店的文档。以下示例显示了许多异步查询:
@Async
Future<User> findByFirstname(String firstname); (1)
@Async
CompletableFuture<User> findOneByFirstname(String firstname); (2)
@Async
ListenableFuture<User> findOneByLastname(String lastname); (3)
1 | 用作java.util.concurrent.Future 返回类型。 |
2 | 使用 Java 8java.util.concurrent.CompletableFuture 作为返回类型。 |
3 | 使用 aorg.springframework.util.concurrent.ListenableFuture 作为返回类型。 |
8.5。创建存储库实例
本节介绍如何为已定义的存储库接口创建实例和 bean 定义。一种方法是使用每个支持存储库机制的 Spring Data 模块附带的 Spring 命名空间,尽管我们通常建议使用 Java 配置。
8.5.1。XML 配置
每个 Spring Data 模块都包含一个repositories
元素,可让您定义 Spring 为您扫描的基本包,如以下示例所示:
<?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 都在从接口名称派生的 bean 名称下注册,因此接口UserRepository
将在userRepository
. 嵌套存储库接口的 Bean 名称以它们的封闭类型名称为前缀。该base-package
属性允许使用通配符,以便您可以定义扫描包的模式。
使用过滤器
默认情况下,基础设施会选择扩展Repository
位于已配置基础包下的特定于持久性技术的子接口的每个接口,并为其创建一个 bean 实例。但是,您可能希望更细粒度地控制哪些接口具有为其创建的 bean 实例。为此,请在元素内使用<include-filter />
和元素。语义完全等同于 Spring 上下文命名空间中的元素。有关详细信息,请参阅这些元素的Spring 参考文档。<exclude-filter />
<repositories />
例如,要从实例化中排除某些接口作为存储库 bean,您可以使用以下配置:
<repositories base-package="com.acme.repositories">
<context:exclude-filter type="regex" expression=".*SomeRepository" />
</repositories>
前面的示例排除了所有以SomeRepository
实例化结尾的接口。
8.5.2. Java 配置
您还可以通过在 Java 配置类上使用特定于存储的@Enable${store}Repositories
注释来触发存储库基础结构。有关 Spring 容器的基于 Java 的配置的介绍,请参阅Spring 参考文档中的 JavaConfig。
启用 Spring Data 存储库的示例配置类似于以下内容:
@Configuration
@EnableJpaRepositories("com.acme.repositories")
class ApplicationConfiguration {
@Bean
EntityManagerFactory entityManagerFactory() {
// …
}
}
前面的示例使用特定于 JPA 的注释,您可以根据实际使用的存储模块对其进行更改。这同样适用于EntityManagerFactory bean 的定义。请参阅涵盖商店特定配置的部分。
|
8.5.3. 独立使用
您还可以在 Spring 容器之外使用存储库基础架构——例如,在 CDI 环境中。您的类路径中仍然需要一些 Spring 库,但通常您也可以通过编程方式设置存储库。提供存储库支持的 Spring Data 模块附带了特定RepositoryFactory
于您可以使用的持久性技术,如下所示:
RepositoryFactorySupport factory = … // Instantiate factory here
UserRepository repository = factory.getRepository(UserRepository.class);
8.6. Spring Data Repository 的自定义实现
Spring Data 提供了各种选项来创建几乎不需要编码的查询方法。但是当这些选项不能满足您的需求时,您还可以为存储库方法提供您自己的自定义实现。本节介绍如何执行此操作。
8.6.1. 自定义单个存储库
要使用自定义功能丰富存储库,您必须首先为自定义功能定义片段接口和实现,如下所示:
interface CustomizedUserRepository {
void someCustomMethod(User user);
}
class CustomizedUserRepositoryImpl implements CustomizedUserRepository {
public void someCustomMethod(User user) {
// Your custom implementation
}
}
与片段接口对应的类名中最重要的部分是Impl 后缀。
|
实现本身不依赖于 Spring Data,可以是常规的 Spring bean。因此,您可以使用标准的依赖注入行为来注入对其他 bean(例如 a JdbcTemplate
)的引用、参与切面等等。
然后可以让你的repository接口扩展fragment接口,如下:
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
}
}
以下示例显示了扩展的自定义存储库的接口CrudRepository
:
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
与片段 interface( CustomizedUserRepository
) 加上后缀的名称相匹配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
}
如果您使用 注释UserRepository
接口@Component("specialCustom")
,则 bean 名称加上Impl
然后匹配为存储库实现定义的名称com.acme.impl.two
,并且使用它而不是第一个。
手动接线
如果您的自定义实现仅使用基于注释的配置和自动装配,那么前面显示的方法效果很好,因为它被视为任何其他 Spring bean。如果你的实现片段 bean 需要特殊的布线,你可以声明这个 bean 并根据上一节中描述的约定命名它。然后,基础设施按名称引用手动定义的 bean 定义,而不是自己创建一个。以下示例显示了如何手动连接自定义实现:
<repositories base-package="com.acme.repository" />
<beans:bean id="userRepositoryImpl" class="…">
<!-- further configuration -->
</beans:bean>
8.6.2. 自定义基础存储库
当您想要自定义基本存储库行为以便影响所有存储库时,上一节中描述的方法需要自定义每个存储库接口。要改为更改所有存储库的行为,您可以创建一个扩展特定于持久性技术的存储库基类的实现。然后,此类充当存储库代理的自定义基类,如以下示例所示:
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 配置中,您可以使用注解的repositoryBaseClass
属性来执行此操作@Enable${store}Repositories
,如下例所示:
@Configuration
@EnableJpaRepositories(repositoryBaseClass = MyRepositoryImpl.class)
class ApplicationConfiguration { … }
XML 命名空间中提供了相应的属性,如以下示例所示:
<repositories base-package="com.acme.repository"
base-class="….MyRepositoryImpl" />
8.7. 从聚合根发布事件
存储库管理的实体是聚合根。在领域驱动设计应用程序中,这些聚合根通常发布领域事件。Spring Data 提供了一个注解@DomainEvents
,您可以在聚合根的方法上使用该注解,以使发布尽可能简单,如以下示例所示:
class AnAggregateRoot {
@DomainEvents (1)
Collection<Object> domainEvents() {
// … return events you want to get published here
}
@AfterDomainEventPublication (2)
void callbackMethod() {
// … potentially clean up domain events list
}
}
1 | 使用的方法@DomainEvents 可以返回单个事件实例或事件集合。它不能有任何论据。 |
2 | 在所有事件都发布后,我们有一个用 注释的方法@AfterDomainEventPublication 。您可以使用它来潜在地清理要发布的事件列表(以及其他用途)。 |
每次调用 Spring Data 存储库的save(…)
、或方法之一时saveAll(…)
,都会调用这些方法。delete(…)
deleteAll(…)
8.8. Spring 数据扩展
本节记录了一组 Spring Data 扩展,这些扩展支持 Spring Data 在各种上下文中的使用。目前,大部分集成都是针对 Spring MVC 的。
8.8.1。Querydsl 扩展
Querydsl是一个框架,可以通过其流畅的 API 构建静态类型的类似 SQL 的查询。
几个 Spring Data 模块通过 Querydsl 提供集成QuerydslPredicateExecutor
,如以下示例所示:
public interface QuerydslPredicateExecutor<T> {
Optional<T> findById(Predicate predicate); (1)
Iterable<T> findAll(Predicate predicate); (2)
long count(Predicate predicate); (3)
boolean exists(Predicate predicate); (4)
// … more functionality omitted.
}
1 | 查找并返回与 匹配的单个实体Predicate 。 |
2 | 查找并返回与 匹配的所有实体Predicate 。 |
3 | 返回与 匹配的实体数Predicate 。 |
4 | 返回匹配的实体是否Predicate 存在。 |
要使用 Querydsl 支持,QuerydslPredicateExecutor
请在存储库接口上进行扩展,如以下示例所示:
interface UserRepository extends CrudRepository<User, Long>, QuerydslPredicateExecutor<User> {
}
前面的示例允许您使用 Querydsl 实例编写类型安全的查询Predicate
,如以下示例所示:
Predicate predicate = user.firstname.equalsIgnoreCase("dave")
.and(user.lastname.startsWithIgnoreCase("mathews"));
userRepository.findAll(predicate);
8.8.2. 网络支持
支持存储库编程模型的 Spring Data 模块附带各种 Web 支持。Web 相关组件要求 Spring MVC JAR 位于类路径中。其中一些甚至提供与Spring HATEOAS的集成。@EnableSpringDataWebSupport
通常,通过使用JavaConfig 配置类中的注释来启用集成支持,如以下示例所示:
@Configuration
@EnableWebMvc
@EnableSpringDataWebSupport
class WebConfiguration {}
@EnableSpringDataWebSupport
注释注册了一些组件。我们将在本节后面讨论这些内容。它还检测类路径上的 Spring HATEOAS 并为其注册集成组件(如果存在)。
或者,如果您使用 XML 配置,则注册为 Spring beanSpringDataWebConfiguration
或注册HateoasAwareSpringDataWebConfiguration
为 Spring beans,如以下示例所示 (for SpringDataWebConfiguration
):
<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" />
基本网络支持
上一节中显示的配置注册了一些基本组件:
-
A使用
DomainClassConverter
Class让 Spring MVC 从请求参数或路径变量中解析存储库管理的域类的实例。 -
HandlerMethodArgumentResolver
让 Spring MVC 从请求参数解析Pageable
和Sort
实例的实现。 -
Jackson 模块用于反/序列化和 等类型
Point
,Distance
或存储特定类型,具体取决于使用的 Spring 数据模块。
使用DomainClassConverter
类
该类DomainClassConverter
允许您直接在 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 MVCid
先将path变量转换为域类的类型,最终通过调用findById(…)
为域类型注册的repository实例来访问实例。
目前,必须实施存储库才有CrudRepository 资格被发现进行转换。
|
用于分页和排序的 HandlerMethodArgumentResolvers
上一节中显示的配置片段还注册了 aPageableHandlerMethodArgumentResolver
以及SortHandlerMethodArgumentResolver
. 注册启用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
使用以下默认配置从请求参数中派生一个实例:
|
您要检索的页面。0 索引,默认为 0。 |
|
您要检索的页面的大小。默认为 20。 |
|
应按格式排序的属性 |
要自定义此行为,请分别注册一个实现PageableHandlerMethodArgumentResolverCustomizer
接口或SortHandlerMethodArgumentResolverCustomizer
接口的 bean。它的customize()
方法被调用,让您更改设置,如以下示例所示:
@Bean SortHandlerMethodArgumentResolverCustomizer sortCustomizer() {
return s -> s.setPropertyDelimiter("<-->");
}
如果设置现有的属性MethodArgumentResolver
不足以满足您的目的,请扩展其中一个SpringDataWebConfiguration
或启用 HATEOAS 的等效项,覆盖pageableResolver()
orsortResolver()
方法,并导入您的自定义配置文件而不是使用@Enable
注释。
如果您需要从请求中解析多个Pageable
或Sort
实例(例如,对于多个表),您可以使用 Spring 的@Qualifier
注释来区分一个。然后请求参数必须以${qualifier}_
. 以下示例显示了生成的方法签名:
String showUsers(Model model,
@Qualifier("thing1") Pageable first,
@Qualifier("thing2") Pageable second) { … }
您必须填充thing1_page
、thing2_page
等。
Pageable
传入方法的默认值相当于 a PageRequest.of(0, 20)
,但您可以使用参数@PageableDefault
上的注解来自定义它Pageable
。
Pageables 的超媒体支持
PagedResources
Spring HATEOAS附带了一个表示模型Page
类Page
(Page
a到 a的转换PagedResources
是由 Spring HATEOASResourceAssembler
接口的实现完成的,称为PagedResourcesAssembler
. 以下示例显示了如何使用 aPagedResourcesAssembler
作为控制器方法参数:
@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
附加链接,具体取决于页面的状态。链接指向方法映射到的 URI。添加到方法中的分页参数与设置相匹配,PageableHandlerMethodArgumentResolver
以确保稍后可以解析链接。
假设我们Person
在数据库中有 30 个实例。您现在可以触发请求 ( ) 并查看类似于以下内容的输出: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(…)
方法来自定义它。
Spring Data Jackson 模块
核心模块和一些特定于商店的模块附带一组杰克逊模块,用于 Spring Data 域使用的类型,如org.springframework.data.geo.Distance
和。一旦启用Web 支持并可用,就会
导入这些模块。org.springframework.data.geo.Point
com.fasterxml.jackson.databind.ObjectMapper
在初始化期间SpringDataJacksonModules
,像SpringDataJacksonConfiguration
, 被基础设施拾取,以便声明com.fasterxml.jackson.databind.Module
的 s 可供 Jackson 使用ObjectMapper
。
以下域类型的数据绑定混合由公共基础设施注册。
org.springframework.data.geo.Distance org.springframework.data.geo.Point org.springframework.data.geo.Box org.springframework.data.geo.Circle org.springframework.data.geo.Polygon
单个模块可能会提供额外的 |
Web 数据绑定支持
您可以使用 Spring Data 投影(在Projections中描述)通过使用JSONPath表达式(需要Jayway JsonPath)或XPath表达式(需要XmlBeam )来绑定传入的请求负载,如以下示例所示:
@ProjectedPayload
public interface UserPayload {
@XBRead("//firstname")
@JsonPath("$..firstname")
String getFirstname();
@XBRead("/lastname")
@JsonPath({ "$.lastname", "$.user.lastname" })
String getLastname();
}
您可以将前面示例中显示的类型用作 Spring MVC 处理程序方法参数,也可以ParameterizedTypeReference
使用RestTemplate
. 前面的方法声明将尝试firstname
在给定文档中查找任何位置。XML 查找在传入文档的lastname
顶层执行。它的 JSON 变体首先尝试顶级,但如果前者不返回值lastname
,也会尝试lastname
嵌套在子文档中。user
这样,源文档结构的变化可以很容易地缓解,而无需客户端调用公开的方法(通常是基于类的有效负载绑定的缺点)。
支持嵌套投影,如Projections中所述。如果该方法返回复杂的非接口类型,ObjectMapper
则使用 Jackson 来映射最终值。
对于 Spring MVC,必要的转换器在@EnableSpringDataWebSupport
活动时会自动注册,并且所需的依赖项在类路径中可用。对于使用RestTemplate
,注册一个ProjectingJackson2HttpMessageConverter
(JSON)或XmlBeamHttpMessageConverter
手动。
有关更多信息,请参阅规范Spring 数据示例存储库中的Web 投影示例。
Querydsl 网络支持
对于那些具有QueryDSLRequest
集成的商店,您可以从查询字符串中包含的属性派生查询。
考虑以下查询字符串:
?firstname=Dave&lastname=Matthews
给定User
前面示例中的对象,您可以使用 将查询字符串解析为以下值QuerydslPredicateArgumentResolver
,如下所示:
QUser.user.firstname.eq("Dave").and(QUser.user.lastname.eq("Matthews"))
@EnableSpringDataWebSupport 当在类路径中找到 Querydsl 时
,该功能会自动启用。 |
向方法签名添加 a@QuerydslPredicate
提供了一个即用型Predicate
,您可以使用QuerydslPredicateExecutor
.
类型信息通常从方法的返回类型中解析。由于该信息不一定与域类型匹配,因此使用 的root 属性可能是个好主意QuerydslPredicate 。
|
以下示例显示了如何@QuerydslPredicate
在方法签名中使用:
@Controller
class UserController {
@Autowired UserRepository repository;
@RequestMapping(value = "/", method = RequestMethod.GET)
String index(Model model, @QuerydslPredicate(root = User.class) Predicate predicate, (1)
Pageable pageable, @RequestParam MultiValueMap<String, String> parameters) {
model.addAttribute("users", repository.findAll(predicate, pageable));
return "index";
}
}
1 | 将查询字符串参数解析为匹配Predicate for User 。 |
默认绑定如下:
-
Object
在简单的属性上eq
。 -
Object
像属性一样的集合contains
。 -
Collection
在简单的属性上in
。
bindings
您可以通过属性@QuerydslPredicate
或使用 Java 8default methods
并将方法添加到存储库接口来自定义这些绑定QuerydslBinderCustomizer
,如下所示:
interface UserRepository extends CrudRepository<User, String>,
QuerydslPredicateExecutor<User>, (1)
QuerydslBinderCustomizer<QUser> { (2)
@Override
default void customize(QuerydslBindings bindings, QUser user) {
bindings.bind(user.username).first((path, value) -> path.contains(value)) (3)
bindings.bind(String.class)
.first((StringPath path, String value) -> path.containsIgnoreCase(value)); (4)
bindings.excluding(user.password); (5)
}
}
1 | QuerydslPredicateExecutor 提供对Predicate . |
2 | QuerydslBinderCustomizer 存储库界面上定义的自动拾取和快捷方式@QuerydslPredicate(bindings=…) 。 |
3 | 将属性的绑定定义为username 简单contains 绑定。 |
4 | 将属性的默认绑定定义为String 不区分大小写的contains 匹配。 |
5 | 从解析中排除该password 属性。Predicate |
您可以QuerydslBinderCustomizerDefaults 在应用来自存储库或@QuerydslPredicate .
|
8.8.3. 存储库填充器
如果您使用 Spring JDBC 模块,您可能熟悉使用DataSource
SQL 脚本填充 a 的支持。存储库级别也有类似的抽象,尽管它不使用 SQL 作为数据定义语言,因为它必须独立于存储。因此,填充器支持 XML(通过 Spring 的 OXM 抽象)和 JSON(通过 Jackson)来定义用于填充存储库的数据。
假设您有一个名为的文件data.json
,其内容如下:
[ { "_class" : "com.acme.Person",
"firstname" : "Dave",
"lastname" : "Matthews" },
{ "_class" : "com.acme.Person",
"firstname" : "Carter",
"lastname" : "Beauford" } ]
您可以使用 Spring Data Commons 中提供的存储库命名空间的填充器元素来填充存储库。要将前面的数据填充到您的PersonRepository
中,请声明一个类似于以下内容的填充器:
<?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 对象解组的类型是通过检查_class
JSON 文档的属性来确定的。基础设施最终会选择适当的存储库来处理反序列化的对象。
要改为使用 XML 来定义存储库应填充的数据,您可以使用该unmarshaller-populator
元素。您将其配置为使用 Spring OXM 中可用的 XML 编组器选项之一。有关详细信息,请参阅Spring 参考文档。以下示例显示如何使用 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>
参考文档
9. JDBC 存储库
本章指出了 JDBC 存储库支持的特点。这建立在使用 Spring Data Repositories中解释的核心存储库支持之上。您应该对那里解释的基本概念有充分的理解。
9.1。为什么选择 Spring Data JDBC?
Java 世界中关系型数据库的主要持久化 API 肯定是 JPA,它有自己的 Spring Data 模块。为什么还有一个?
JPA 做了很多事情来帮助开发人员。除其他外,它跟踪实体的更改。它为你做延迟加载。它使您可以将广泛的对象构造映射到同样广泛的数据库设计。
这很棒,让很多事情变得非常容易。只需看一下基本的 JPA 教程。但是,对于 JPA 为什么会做某件事,它常常会让人感到困惑。此外,在概念上非常简单的事情使用 JPA 变得相当困难。
Spring Data JDBC 旨在通过采用以下设计决策在概念上更加简单:
-
如果加载实体,SQL 语句就会运行。完成此操作后,您将拥有一个完全加载的实体。没有延迟加载或缓存。
-
如果你保存一个实体,它就会被保存。如果你不这样做,它就不会。没有脏跟踪,也没有会话。
-
有一个如何将实体映射到表的简单模型。它可能只适用于相当简单的情况。如果您不喜欢这样,您应该编写自己的策略。Spring Data JDBC 仅对使用注释自定义策略提供非常有限的支持。
9.2. 领域驱动设计和关系数据库。
所有 Spring Data 模块都受到域驱动设计中“存储库”、“聚合”和“聚合根”概念的启发。这些对于 Spring Data JDBC 可能更为重要,因为它们在某种程度上与使用关系数据库时的常规做法相反。
聚合是一组实体,它们保证在对它的原子更改之间保持一致。一个经典的例子是Order
with OrderItems
。上的属性Order
(例如,numberOfItems
与 的实际数量OrderItems
一致)在进行更改时保持一致。
不能保证跨聚合的引用始终保持一致。他们保证最终会变得一致。
每个聚合只有一个聚合根,它是聚合的实体之一。聚合只能通过聚合根上的方法进行操作。这些是前面提到的原子变化。
存储库是对持久存储的抽象,它看起来像某种类型的所有聚合的集合。一般来说,对于 Spring Data,这意味着您希望Repository
每个聚合根都有一个。此外,对于 Spring Data JDBC,这意味着从聚合根可访问的所有实体都被视为该聚合根的一部分。Spring Data JDBC 假定只有聚合具有存储聚合的非根实体的表的外键,并且没有其他实体指向非根实体。
在当前实现中,从聚合根引用的实体被 Spring Data JDBC 删除和重新创建。 |
您可以使用与您的工作风格和设计数据库相匹配的实现来覆盖存储库方法。
9.3. 入门
引导设置工作环境的一种简单方法是在STS中或从Spring Initializr创建一个基于 Spring 的项目。
首先,您需要设置一个正在运行的数据库服务器。请参阅供应商文档,了解如何配置数据库以进行 JDBC 访问。
在 STS 中创建 Spring 项目:
-
转到 File → New → Spring Template Project → Simple Spring Utility Project,并在出现提示时按 Yes。然后输入项目和包名,例如
org.spring.jdbc.example
. -
将以下内容添加到
pom.xml
文件dependencies
元素:<dependencies> <!-- other dependency elements omitted --> <dependency> <groupId>org.springframework.data</groupId> <artifactId>spring-data-jdbc</artifactId> <version>2.4.1</version> </dependency> </dependencies>
-
将 pom.xml 中的 Spring 版本更改为
<spring.framework.version>5.3.21</spring.framework.version>
-
将 Maven 的 Spring Milestone 存储库的以下位置添加到您的位置,使其与您的元素
pom.xml
处于同一级别:<dependencies/>
<repositories> <repository> <id>spring-milestone</id> <name>Spring Maven MILESTONE Repository</name> <url>https://repo.spring.io/libs-milestone</url> </repository> </repositories>
该存储库也可在此处浏览。
9.4。示例存储库
有一个GitHub 存储库,其中包含几个示例,您可以下载并试用这些示例,以了解该库的工作原理。
9.5。基于注解的配置
Spring Data JDBC 存储库支持可以通过 Java 配置由注解激活,如以下示例所示:
@Configuration
@EnableJdbcRepositories (1)
class ApplicationConfig extends AbstractJdbcConfiguration { (2)
@Bean
DataSource dataSource() { (3)
EmbeddedDatabaseBuilder builder = new EmbeddedDatabaseBuilder();
return builder.setType(EmbeddedDatabaseType.HSQL).build();
}
@Bean
NamedParameterJdbcOperations namedParameterJdbcOperations(DataSource dataSource) { (4)
return new NamedParameterJdbcTemplate(dataSource);
}
@Bean
TransactionManager transactionManager(DataSource dataSource) { (5)
return new DataSourceTransactionManager(dataSource);
}
}
1 | @EnableJdbcRepositories 为派生自的接口创建实现Repository |
2 | AbstractJdbcConfiguration 提供 Spring Data JDBC 所需的各种默认 bean |
3 | 创建DataSource 到数据库的连接。这是以下两个 bean 方法所必需的。 |
4 | 创建NamedParameterJdbcOperations Spring Data JDBC 用于访问数据库的对象。 |
5 | Spring Data JDBC 利用 Spring JDBC 提供的事务管理。 |
上例中的配置类EmbeddedDatabaseBuilder
使用spring-jdbc
. 然后DataSource
用于设置NamedParameterJdbcOperations
和TransactionManager
. 我们最终通过使用@EnableJdbcRepositories
. 如果没有配置基本包,则使用配置类所在的包。扩展AbstractJdbcConfiguration
可确保注册各种 bean。覆盖其方法可用于自定义设置(见下文)。
使用 Spring Boot 可以进一步简化此配置。一旦启动器包含在依赖项中,使用 Spring BootDataSource
就足够了。spring-boot-starter-data-jdbc
其他一切都由 Spring Boot 完成。
在此设置中可能需要自定义几件事。
9.5.1。方言
Spring Data JDBC 使用接口的实现Dialect
来封装特定于数据库或其 JDBC 驱动程序的行为。默认情况下,AbstractJdbcConfiguration
尝试确定正在使用的数据库并注册正确的Dialect
. 这种行为可以通过覆盖来改变jdbcDialect(NamedParameterJdbcOperations)
。
如果您使用没有方言可用的数据库,那么您的应用程序将无法启动。在这种情况下,您必须要求您的供应商提供Dialect
实施。或者,您可以:
-
实现你自己的
Dialect
. -
实现一个
JdbcDialectProvider
返回Dialect
. -
通过在下创建
spring.factories
资源来注册提供者META-INF
并通过添加一行来执行注册
org.springframework.data.jdbc.repository.config.DialectResolver$JdbcDialectProvider=<fully qualified name of your JdbcDialectProvider>
9.6。持久实体
可以使用该CrudRepository.save(…)
方法保存聚合。如果聚合是新的,这将导致聚合根的插入,然后是所有直接或间接引用的实体的插入语句。
如果聚合根不是新的,则删除所有引用的实体,更新聚合根,并再次插入所有引用的实体。请注意,实例是否为新实例是实例状态的一部分。
这种方法有一些明显的缺点。如果实际上只更改了很少的引用实体,则删除和插入是浪费的。虽然这个过程可以并且可能会得到改进,但 Spring Data JDBC 可以提供的功能存在一定的限制。它不知道聚合的先前状态。因此,任何更新过程都必须获取它在数据库中找到的任何内容,并确保将其转换为传递给 save 方法的实体的任何状态。 |
9.6.1。对象映射基础
本节涵盖 Spring Data 对象映射、对象创建、字段和属性访问、可变性和不变性的基础知识。请注意,本节仅适用于不使用底层数据存储(如 JPA)的对象映射的 Spring Data 模块。还请务必查阅特定于存储的部分以获取特定于存储的对象映射,例如索引、自定义列或字段名称等。
Spring Data 对象映射的核心职责是创建域对象的实例并将 store-native 数据结构映射到这些实例上。这意味着我们需要两个基本步骤:
-
使用公开的构造函数之一创建实例。
-
实例填充以实现所有公开的属性。
对象创建
Spring Data 自动尝试检测持久实体的构造函数以用于实现该类型的对象。解析算法的工作原理如下:
-
如果有一个带有注释的静态工厂方法,
@PersistenceCreator
则使用它。 -
如果只有一个构造函数,则使用它。
-
如果有多个构造函数并且恰好一个用 注释
@PersistenceCreator
,则使用它。 -
如果有无参数构造函数,则使用它。其他构造函数将被忽略。
值解析假定构造函数/工厂方法参数名称与实体的属性名称匹配,即解析将像要填充属性一样执行,包括映射中的所有自定义(不同的数据存储列或字段名称等)。这还需要类文件中可用的参数名称信息或@ConstructorProperties
构造函数上存在的注释。
可以使用 Spring Framework 的@Value
value annotation 使用 store-specific SpEL 表达式来自定义 value 解析。请参阅有关商店特定映射的部分以获取更多详细信息。
物业人口
一旦创建了实体的实例,Spring Data 就会填充该类的所有剩余持久属性。除非已经由实体的构造函数填充(即通过其构造函数参数列表使用),否则将首先填充标识符属性以允许解析循环对象引用。之后,在实体实例上设置所有尚未由构造函数填充的非瞬态属性。为此,我们使用以下算法:
-
如果属性是不可变的但公开了一个
with…
方法(见下文),我们使用该with…
方法创建一个具有新属性值的新实体实例。 -
如果定义了属性访问(即通过getter 和setter 访问),我们将调用setter 方法。
-
如果属性是可变的,我们直接设置字段。
-
如果属性是不可变的,我们将使用持久性操作(请参阅对象创建)使用的构造函数来创建实例的副本。
-
默认情况下,我们直接设置字段值。
让我们看一下以下实体:
class Person {
private final @Id Long id; (1)
private final String firstname, lastname; (2)
private final LocalDate birthday;
private final int age; (3)
private String comment; (4)
private @AccessType(Type.PROPERTY) String remarks; (5)
static Person of(String firstname, String lastname, LocalDate birthday) { (6)
return new Person(null, firstname, lastname, birthday,
Period.between(birthday, LocalDate.now()).getYears());
}
Person(Long id, String firstname, String lastname, LocalDate birthday, int age) { (6)
this.id = id;
this.firstname = firstname;
this.lastname = lastname;
this.birthday = birthday;
this.age = age;
}
Person withId(Long id) { (1)
return new Person(id, this.firstname, this.lastname, this.birthday, this.age);
}
void setRemarks(String remarks) { (5)
this.remarks = remarks;
}
}
1 | 标识符属性是最终的,但null 在构造函数中设置为。该类公开了一个withId(…) 用于设置标识符的方法,例如,当将实例插入数据存储并生成标识符时。Person 创建新实例时,原始实例保持不变。相同的模式通常适用于存储管理但可能必须更改以进行持久性操作的其他属性。wither 方法是可选的,因为持久性构造函数(参见 6)实际上是一个复制构造函数,设置属性将被转换为创建一个应用了新标识符值的新实例。 |
2 | firstname 和lastname 属性是可能通过 getter 暴露的普通不可变属性。 |
3 | 该age 属性是不可变的,但从该birthday 属性派生而来。使用所示的设计,数据库值将胜过默认值,因为 Spring Data 使用唯一声明的构造函数。即使意图是应该首选计算,重要的是此构造函数也将age 其作为参数(可能会忽略它),否则属性填充步骤将尝试设置年龄字段并由于它是不可变的且没有with… 方法而失败在场。 |
4 | 该comment 属性是可变的,通过直接设置其字段来填充。 |
5 | remarks 属性是可变的,并且可以通过直接设置字段comment 或调用 setter 方法来填充 |
6 | 该类公开了一个工厂方法和一个用于创建对象的构造函数。这里的核心思想是使用工厂方法而不是额外的构造函数来避免构造函数通过@PersistenceCreator . 相反,属性的默认设置是在工厂方法中处理的。如果您希望 Spring Data 使用工厂方法进行对象实例化,请使用@PersistenceCreator . |
一般建议
-
尝试坚持使用不可变对象 ——不可变对象很容易创建,因为实现对象只需调用其构造函数即可。此外,这可以避免您的域对象被允许客户端代码操纵对象状态的 setter 方法乱扔垃圾。如果您需要这些,最好将它们包保护起来,以便它们只能被有限数量的并置类型调用。仅构造函数实现比属性填充快 30%。
-
提供一个全参数构造函数 ——即使你不能或不想将你的实体建模为不可变值,提供一个将实体的所有属性作为参数(包括可变属性)的构造函数仍然有价值,因为这允许对象映射以跳过属性填充以获得最佳性能。
-
使用工厂方法而不是重载的构造函数来避免
@PersistenceCreator
- 使用最佳性能所需的全参数构造函数,我们通常希望公开更多特定于应用程序用例的构造函数,这些构造函数省略自动生成的标识符等内容。这是一种既定的模式,而不是使用静态工厂方法来公开这些全参数构造函数的变体。 -
确保遵守允许使用生成的实例化器和属性访问器类的约束 ——
-
对于要生成的标识符,仍将 final 字段与全参数持久性构造函数(首选)或
with…
方法结合使用 —— -
使用 Lombok 避免样板代码 - 由于持久性操作通常需要一个构造函数来获取所有参数,因此它们的声明变成了对字段分配的样板参数的繁琐重复,使用 Lombok 可以最好地避免这种情况
@AllArgsConstructor
。
覆盖属性
Java 允许灵活设计域类,其中子类可以定义一个已在其超类中以相同名称声明的属性。考虑以下示例:
public class SuperType {
private CharSequence field;
public SuperType(CharSequence field) {
this.field = field;
}
public CharSequence getField() {
return this.field;
}
public void setField(CharSequence field) {
this.field = field;
}
}
public class SubType extends SuperType {
private String field;
public SubType(String field) {
super(field);
this.field = field;
}
@Override
public String getField() {
return this.field;
}
public void setField(String field) {
this.field = field;
// optional
super.setField(field);
}
}
这两个类都定义了一个field
using 可分配类型。SubType
然而阴影SuperType.field
。根据类设计,使用构造函数可能是设置的唯一默认方法SuperType.field
。或者,调用super.setField(…)
setter 可以设置field
in SuperType
。所有这些机制都会在某种程度上产生冲突,因为属性共享相同的名称但可能代表两个不同的值。如果类型不可分配,Spring Data 会跳过超类型属性。也就是说,被覆盖的属性的类型必须可以分配给它的超类型属性类型才能注册为覆盖,否则超类型属性被认为是瞬态的。我们通常建议使用不同的属性名称。
Spring Data 模块通常支持覆盖不同值的属性。从编程模型的角度来看,有几点需要考虑:
-
应该保留哪个属性(默认为所有声明的属性)?您可以通过使用 注释这些属性来排除属性
@Transient
。 -
如何在数据存储中表示属性?对不同的值使用相同的字段/列名称通常会导致数据损坏,因此您应该使用显式字段/列名称注释至少一个属性。
-
不能使用 using
@AccessType(PROPERTY)
,因为如果不对 setter 实现进行任何进一步的假设,通常无法设置超属性。
Kotlin 支持
Spring Data 调整了 Kotlin 的细节以允许对象创建和变异。
Kotlin 对象创建
Kotlin 类支持实例化,默认情况下所有类都是不可变的,需要明确的属性声明来定义可变属性。考虑以下data
类Person
:
data class Person(val id: String, val name: String)
上面的类编译成一个带有显式构造函数的典型类。我们可以通过添加另一个构造函数来自定义这个类,并用注释@PersistenceCreator
来指示构造函数的偏好:
data class Person(var id: String, val name: String) {
@PersistenceCreator
constructor(id: String) : this(id, "unknown")
}
Kotlin 通过在未提供参数时允许使用默认值来支持参数可选性。当 Spring Data 检测到具有参数默认值的构造函数时,如果数据存储不提供值(或简单地返回null
),它将使这些参数不存在,因此 Kotlin 可以应用参数默认值。考虑以下应用参数默认值的类name
data class Person(var id: String, val name: String = "unknown")
每次name
参数不是结果的一部分或其值为null
时,name
默认为unknown
。
Kotlin 数据类的属性总体
在 Kotlin 中,默认情况下所有类都是不可变的,并且需要显式的属性声明来定义可变属性。考虑以下data
类Person
:
data class Person(val id: String, val name: String)
这个类实际上是不可变的。它允许创建新实例,因为 Kotlin 生成一个copy(…)
创建新对象实例的方法,该方法从现有对象复制所有属性值并将作为参数提供的属性值应用到该方法。
Kotlin 覆盖属性
Kotlin 允许声明属性覆盖以更改子类中的属性。
open class SuperType(open var field: Int)
class SubType(override var field: Int = 1) :
SuperType(field) {
}
这样的安排呈现了两个名为 的属性field
。Kotlin 为每个类中的每个属性生成属性访问器(getter 和 setter)。实际上,代码如下所示:
public class SuperType {
private int field;
public SuperType(int field) {
this.field = field;
}
public int getField() {
return this.field;
}
public void setField(int field) {
this.field = field;
}
}
public final class SubType extends SuperType {
private int field;
public SubType(int field) {
super(field);
this.field = field;
}
public int getField() {
return this.field;
}
public void setField(int field) {
this.field = field;
}
}
getter 和 setterSubType
仅在 set上SubType.field
,而不在SuperType.field
. 在这种安排中,使用构造函数是 set 的唯一默认方法SuperType.field
。添加一个方法来SubType
设置SuperType.field
viathis.SuperType.field = …
是可能的,但不属于支持的约定。属性覆盖在某种程度上会产生冲突,因为属性共享相同的名称但可能代表两个不同的值。我们通常建议使用不同的属性名称。
Spring Data 模块通常支持覆盖不同值的属性。从编程模型的角度来看,有几点需要考虑:
-
应该保留哪个属性(默认为所有声明的属性)?您可以通过使用 注释这些属性来排除属性
@Transient
。 -
如何在数据存储中表示属性?对不同的值使用相同的字段/列名称通常会导致数据损坏,因此您应该使用显式字段/列名称注释至少一个属性。
-
@AccessType(PROPERTY)
由于无法设置超属性,因此无法使用 using 。
9.6.2. 您的实体中支持的类型
当前支持以下类型的属性:
-
所有原始类型及其装箱类型(
int
、float
、Integer
、Float
等) -
枚举被映射到他们的名字。
-
String
-
java.util.Date
,java.time.LocalDate
,java.time.LocalDateTime
和java.time.LocalTime
-
如果您的数据库支持,上述类型的数组和集合可以映射到数组类型的列。
-
您的数据库驱动程序接受的任何内容。
-
对其他实体的引用。它们被认为是一对一的关系或嵌入式类型。一对一关系实体具有
id
属性是可选的。被引用实体的表应该有一个与引用实体的表同名的附加列。您可以通过实施来更改此名称NamingStrategy.getReverseColumnName(PersistentPropertyPathExtension path)
。嵌入式实体不需要id
. 如果存在一个,它将被忽略。 -
Set<some entity>
被认为是一对多的关系。被引用实体的表应该有一个与引用实体的表同名的附加列。您可以通过实施来更改此名称NamingStrategy.getReverseColumnName(PersistentPropertyPathExtension path)
。 -
Map<simple type, some entity>
被认为是合格的一对多关系。被引用实体的表应该有两个额外的列:一个与引用实体的表命名相同的外键,另一个具有相同的名称和一个附加_key
后缀的映射键。NamingStrategy.getReverseColumnName(PersistentPropertyPathExtension path)
您可以通过分别实现和来更改此行为NamingStrategy.getKeyColumn(RelationalPersistentProperty property)
。或者,您可以使用注释属性@MappedCollection(idColumn="your_column_name", keyColumn="your_key_column_name")
-
List<some entity>
映射为Map<Integer, some entity>
.
引用实体的处理是有限的。这是基于上述聚合根的思想。如果您引用另一个实体,则根据定义,该实体是您的聚合的一部分。因此,如果您删除引用,则先前引用的实体将被删除。这也意味着参考是 1-1 或 1-n,但不是 n-1 或 nm。
如果您有 n-1 或 nm 引用,根据定义,您将处理两个单独的聚合。它们之间的引用可以编码为简单的id
值,这些值与 Spring Data JDBC 正确映射。对这些进行编码的更好方法是使它们成为AggregateReference
. AnAggregateReference
是一个 id 值的包装器,它将该值标记为对不同聚合的引用。此外,该聚合的类型被编码在类型参数中。
AggregateReference
class Person {
@Id long id;
AggregateReference<Person, Long> bestFriend;
}
// ...
Person p1, p2 = // some initialization
p1.bestFriend = AggregateReference.to(p2.id);
9.6.3。NamingStrategy
CrudRepository
当您使用Spring Data JDBC 提供的标准实现时,它们需要特定的表结构。NamingStrategy
您可以通过在应用程序上下文中提供 a 来调整它。
9.6.4。Custom table names
当 NamingStrategy 与您的数据库表名称不匹配时,您可以使用@Table
注释自定义名称。此注释的元素value
提供自定义表名称。以下示例将MyEntity
类映射到CUSTOM_TABLE_NAME
数据库中的表:
@Table("CUSTOM_TABLE_NAME")
class MyEntity {
@Id
Integer id;
String name;
}
9.6.5。Custom column names
当 NamingStrategy 与您的数据库列名称不匹配时,您可以使用@Column
注释自定义名称。此注释的元素value
提供自定义列名称。以下示例将类的name
属性映射到数据库中的列:MyEntity
CUSTOM_COLUMN_NAME
class MyEntity {
@Id
Integer id;
@Column("CUSTOM_COLUMN_NAME")
String name;
}
注释可@MappedCollection
用于引用类型(一对一关系)或 Sets、Lists 和 Maps(一对多关系)。
idColumn
注释的元素为引用另一个表中的 id 列的外键列提供自定义名称。在以下示例中,MySubEntity
该类的对应表有一个NAME
列,并且出于关系原因,该CUSTOM_MY_ENTITY_ID_COLUMN_NAME
列的id 列:MyEntity
class MyEntity {
@Id
Integer id;
@MappedCollection(idColumn = "CUSTOM_MY_ENTITY_ID_COLUMN_NAME")
Set<MySubEntity> subEntities;
}
class MySubEntity {
String name;
}
使用时List
,Map
您必须为数据集在 中的位置List
或 中实体的键值添加一列Map
。这个额外的列名可以用注解的keyColumn
元素来定制:@MappedCollection
class MyEntity {
@Id
Integer id;
@MappedCollection(idColumn = "CUSTOM_COLUMN_NAME", keyColumn = "CUSTOM_KEY_COLUMN_NAME")
List<MySubEntity> name;
}
class MySubEntity {
String name;
}
9.6.6。嵌入式实体
嵌入式实体用于在您的 java 数据模型中具有值对象,即使您的数据库中只有一个表。在下面的示例中,您会看到它MyEntity
与@Embedded
注释映射。这样做的结果是,在数据库中需要一个my_entity
包含两列id
和name
(来自EmbeddedEntity
类)的表。
但是,如果该name
列实际上null
在结果集中,则整个属性embeddedEntity
将根据onEmpty
of设置为 null,当所有嵌套属性都是 时@Embedded
,它是对象。
与此行为相反,尝试使用默认构造函数或从结果集中接受可为空参数值的构造函数来创建新实例。null
null
USE_EMPTY
class MyEntity {
@Id
Integer id;
@Embedded(onEmpty = USE_NULL) (1)
EmbeddedEntity embeddedEntity;
}
class EmbeddedEntity {
String name;
}
1 | Null embeddedEntity 如果name 在null . _ 用于使用属性的潜在值进行USE_EMPTY 实例化。embeddedEntity null name |
如果您在一个实体中多次需要一个值对象,这可以通过注解的可选prefix
元素来实现。@Embedded
此元素表示一个前缀,并为嵌入对象中的每个列名添加前缀。
使用快捷方式
|
Collection
包含 a或 a 的嵌入式实体Map
将始终被视为非空,因为它们至少包含空集合或映射。null
因此,即使使用@Embedded(onEmpty = USE_NULL),这样的实体也永远不会存在。
9.6.7。实体状态检测策略
下表描述了 Spring Data 提供的用于检测实体是否为新的策略:
|
默认情况下,Spring Data 检查给定实体的标识符属性。如果标识符属性是 |
|
|
实施 |
如果实体实现 注意: |
提供自定义 |
您可以 |
9.6.8。身份生成
Spring Data JDBC 使用 ID 来识别实体。实体的 ID 必须使用 Spring Data 的注解进行@Id
注解。
当您的数据库具有 ID 列的自动增量列时,生成的值会在将其插入数据库后在实体中设置。
一个重要的约束是,在保存实体后,该实体不能再是新的。请注意,实体是否是新实体是实体状态的一部分。对于自动增量列,这会自动发生,因为 ID 是由 Spring Data 使用 ID 列中的值设置的。如果您不使用自动增量列,则可以使用BeforeConvert
侦听器,它设置实体的 ID(在本文档后面介绍)。
9.6.9。乐观锁定
@Version
Spring Data JDBC 通过在聚合根上注释的数字属性支持乐观锁定
。每当 Spring Data JDBC 保存具有此类版本属性的聚合时,会发生两件事:聚合根的更新语句将包含 where 子句,检查存储在数据库中的版本是否实际未更改。如果不是这种情况,OptimisticLockingFailureException
将被抛出。此外,实体和数据库中的版本属性都会增加,因此并发操作会注意到更改并OptimisticLockingFailureException
在适用的情况下抛出如上所述的。
此过程也适用于插入新聚合,其中 anull
或0
version 表示新实例,然后增加的实例将实例标记为不再是新的,这使得在对象构造期间生成 id 的情况下(例如当 UUID 是用过的。
在删除期间,版本检查也适用,但不增加版本。
9.7。查询方法
本节提供有关 Spring Data JDBC 的实现和使用的一些具体信息。
您通常在存储库上触发的大多数数据访问操作都会导致针对数据库运行查询。定义这样的查询是在存储库接口上声明一个方法的问题,如以下示例所示:
interface PersonRepository extends PagingAndSortingRepository<Person, String> {
List<Person> findByFirstname(String firstname); (1)
List<Person> findByFirstnameOrderByLastname(String firstname, Pageable pageable); (2)
Slice<Person> findByLastname(String lastname, Pageable pageable); (3)
Page<Person> findByLastname(String lastname, Pageable pageable); (4)
Person findByFirstnameAndLastname(String firstname, String lastname); (5)
Person findFirstByLastname(String lastname); (6)
@Query("SELECT * FROM person WHERE lastname = :lastname")
List<Person> findByLastname(String lastname); (7)
@Query("SELECT * FROM person WHERE lastname = :lastname")
Stream<Person> streamByLastname(String lastname); (8)
}
1 | 该方法显示对所有具有给定firstname . 查询是通过解析可以与And 和连接的约束的方法名称来派生的Or 。因此,方法名称导致查询表达式为SELECT … FROM person WHERE firstname = :firstname . |
2 | 用于Pageable 将偏移量和排序参数传递给数据库。 |
3 | 返回一个Slice<Person> 。选择LIMIT+1 行以确定是否有更多数据要使用。ResultSetExtractor 不支持自定义。 |
4 | 运行分页查询返回Page<Person> . 仅选择给定页面范围内的数据,并可能通过计数查询来确定总计数。ResultSetExtractor 不支持自定义。 |
5 | 查找给定条件的单个实体。它以IncorrectResultSizeDataAccessException 非唯一结果完成。 |
6 | 与 <3> 相比,即使查询产生更多结果文档,也始终发出第一个实体。 |
7 | 该findByLastname 方法显示对所有具有给定lastname . |
8 | 该streamByLastname 方法返回 a Stream ,这使得值一旦从数据库返回就成为可能。 |
下表显示了查询方法支持的关键字:
关键词 | 样本 | 逻辑结果 |
---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
查询派生仅限于可以在WHERE 不使用连接的子句中使用的属性。
|
9.7.1。查询查找策略
JDBC 模块支持手动将查询定义为@Query
注释中的字符串或属性文件中的命名查询。
从方法名称派生查询目前仅限于简单属性,这意味着属性直接存在于聚合根中。此外,此方法仅支持选择查询。
9.7.2。使用@Query
下面的例子展示了如何使用@Query
来声明一个查询方法:
interface UserRepository extends CrudRepository<User, Long> {
@Query("select firstName, lastName from User u where u.emailAddress = :email")
User findByEmailAddress(@Param("email") String email);
}
对于将查询结果转换为实体RowMapper
,默认情况下使用与 Spring Data JDBC 自身生成的查询相同。您提供的查询必须与RowMapper
预期的格式相匹配。必须提供在实体的构造函数中使用的所有属性的列。通过 setter、wither 或字段访问设置的属性列是可选的。结果中没有匹配列的属性将不会被设置。该查询用于填充聚合根、嵌入实体和一对一关系,包括作为 SQL 数组类型存储和加载的原始类型数组。为地图、列表、集合和实体数组生成单独的查询。
Spring 完全支持基于-parameters 编译器标志的 Java 8 参数名称发现。通过在构建中使用此标志作为调试信息的替代方案,您可以省略@Param 命名参数的注释。
|
Spring Data JDBC 仅支持命名参数。 |
9.7.3。命名查询
如果上一节中描述的注释中没有给出查询,Spring Data JDBC 将尝试定位命名查询。有两种方法可以确定查询的名称。默认采用查询的域类,即存储库的聚合根,采用其简单名称并附加方法名称,并用 . 分隔.
。或者,@Query
注释有一个name
属性,可用于指定要查找的查询的名称。
命名查询应该在META-INF/jdbc-named-queries.properties
类路径的属性文件中提供。
可以通过将值设置为 来更改该文件的位置@EnableJdbcRepositories.namedQueriesLocation
。
流式传输结果
当您将 Stream 指定为查询方法的返回类型时,Spring Data JDBC 会在元素可用时立即返回。在处理大量数据时,这适用于减少延迟和内存需求。
该流包含与数据库的打开连接。为了避免内存泄漏,最终需要通过关闭流来关闭该连接。推荐的方法是使用try-with-resource clause
. 这也意味着,一旦关闭与数据库的连接,流就无法获取更多元素并可能引发异常。
风俗RowMapper
您可以RowMapper
通过使用@Query(rowMapperClass = ….)
或通过注册RowMapperMap
bean 并注册RowMapper
每个方法的返回类型来配置要使用的内容。以下示例显示了如何注册DefaultQueryMappingConfiguration
:
@Bean
QueryMappingConfiguration rowMappers() {
return new DefaultQueryMappingConfiguration()
.register(Person.class, new PersonRowMapper())
.register(Address.class, new AddressRowMapper());
}
在确定RowMapper
方法使用哪个时,根据方法的返回类型,遵循以下步骤:
-
如果类型是简单类型,
RowMapper
则使用 no。相反,查询应该返回单行和单列,并且对返回类型的转换应用于该值。
-
迭代中的实体类,
QueryMappingConfiguration
直到找到一个是所讨论的返回类型的超类或接口。使用为该类RowMapper
注册的。迭代按照注册的顺序进行,因此请确保在特定类型之后注册更通用的类型。
如果适用,诸如集合之类的包装器类型Optional
将被解包。因此,返回类型Optional<Person>
使用Person
前面过程中的类型。
使用自定义RowMapper 通过QueryMappingConfiguration 、@Query(rowMapperClass=…) 或自定义ResultSetExtractor 禁用实体回调和生命周期事件,因为结果映射可以在需要时发出自己的事件/回调。
|
9.7.4。预测
Spring Data 查询方法通常返回由存储库管理的聚合根的一个或多个实例。但是,有时可能需要基于这些类型的某些属性创建投影。Spring Data 允许对专用返回类型进行建模,以更有选择性地检索托管聚合的部分视图。
想象一个存储库和聚合根类型,例如以下示例:
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);
}
现在假设我们只想检索人的姓名属性。Spring Data 提供了哪些方法来实现这一目标?本章的其余部分回答了这个问题。
基于界面的投影
将查询结果限制为仅 name 属性的最简单方法是声明一个接口,该接口公开要读取的属性的访问器方法,如以下示例所示:
interface NamesOnly {
String getFirstname();
String getLastname();
}
这里重要的是这里定义的属性与聚合根中的属性完全匹配。这样做可以添加一个查询方法,如下所示:
interface PersonRepository extends Repository<Person, UUID> {
Collection<NamesOnly> findByLastname(String lastname);
}
查询执行引擎在运行时为每个返回的元素创建该接口的代理实例,并将对公开方法的调用转发给目标对象。
在您的方法中声明一个Repository 覆盖基本方法的方法(例如,在 中声明CrudRepository ,特定于存储的存储库接口,或Simple…Repository )导致调用基本方法,而不管声明的返回类型如何。确保使用兼容的返回类型,因为基本方法不能用于投影。一些商店模块支持@Query 注释将覆盖的基本方法转换为查询方法,然后可用于返回投影。
|
投影可以递归使用。如果您还想包含一些Address
信息,请为此创建一个投影接口并从 的声明中返回该接口getAddress()
,如以下示例所示:
interface PersonSummary {
String getFirstname();
String getLastname();
AddressSummary getAddress();
interface AddressSummary {
String getCity();
}
}
在方法调用时,address
获取目标实例的属性并依次包装到投影代理中。
封闭投影
其访问器方法都匹配目标聚合的属性的投影接口被认为是封闭投影。以下示例(我们在本章前面也使用过)是一个封闭投影:
interface NamesOnly {
String getFirstname();
String getLastname();
}
如果使用封闭投影,Spring Data 可以优化查询执行,因为我们知道支持投影代理所需的所有属性。有关这方面的更多详细信息,请参阅参考文档的模块特定部分。
打开投影
投影接口中的访问器方法也可用于通过@Value
注解计算新值,如下例所示:
interface NamesOnly {
@Value("#{target.firstname + ' ' + target.lastname}")
String getFullName();
…
}
支持投影的聚合根在target
变量中可用。使用的投影界面@Value
是开放式投影。在这种情况下,Spring Data 无法应用查询执行优化,因为 SpEL 表达式可以使用聚合根的任何属性。
中使用的表达式@Value
不应该太复杂——你要避免在String
变量中编程。对于非常简单的表达式,一种选择可能是使用默认方法(在 Java 8 中引入),如以下示例所示:
interface NamesOnly {
String getFirstname();
String getLastname();
default String getFullName() {
return getFirstname().concat(" ").concat(getLastname());
}
}
这种方法要求您能够完全基于投影接口上公开的其他访问器方法来实现逻辑。第二个更灵活的选择是在 Spring bean 中实现自定义逻辑,然后从 SpEL 表达式调用它,如下例所示:
@Component
class MyBean {
String getFullName(Person person) {
…
}
}
interface NamesOnly {
@Value("#{@myBean.getFullName(target)}")
String getFullName();
…
}
请注意 SpEL 表达式如何引用myBean
和调用getFullName(…)
方法并将投影目标作为方法参数转发。SpEL 表达式评估支持的方法也可以使用方法参数,然后可以从表达式中引用。方法参数可通过Object
名为 的数组获得args
。以下示例显示如何从args
数组中获取方法参数:
interface NamesOnly {
@Value("#{args[0] + ' ' + target.firstname + '!'}")
String getSalutation(String prefix);
}
同样,对于更复杂的表达式,您应该使用 Spring bean 并让表达式调用方法,如前所述。
基于类的投影 (DTO)
定义投影的另一种方法是使用值类型 DTO(数据传输对象),它保存应该检索的字段的属性。这些 DTO 类型的使用方式与使用投影接口的方式完全相同,只是不会发生代理,也不能应用嵌套投影。
如果存储通过限制要加载的字段来优化查询执行,则要加载的字段由公开的构造函数的参数名称确定。
以下示例显示了一个投影 DTO:
class NamesOnly {
private final String firstname, lastname;
NamesOnly(String firstname, String lastname) {
this.firstname = firstname;
this.lastname = lastname;
}
String getFirstname() {
return this.firstname;
}
String getLastname() {
return this.lastname;
}
// equals(…) and hashCode() implementations
}
避免投影 DTO 的样板代码
您可以使用提供注释的Project Lombok显着简化 DTO 的代码(不要与前面接口示例中显示的
字段是 |
动态投影
到目前为止,我们已经使用投影类型作为集合的返回类型或元素类型。但是,您可能希望选择在调用时使用的类型(这使其成为动态的)。要应用动态投影,请使用如下示例中所示的查询方法:
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);
}
检查类型的查询参数Class 是否符合动态投影参数的条件。如果查询的实际返回类型等于参数的通用参数类型Class ,则匹配的Class 参数不可用于查询或 SpEL 表达式中。如果要将Class 参数用作查询参数,请确保使用不同的通用参数,例如Class<?> .
|
9.8。MyBatis 集成
CRUD 操作和查询方法可以委托给 MyBatis。本节介绍如何配置 Spring Data JDBC 以与 MyBatis 集成,以及遵循哪些约定来移交查询的运行以及到库的映射。
9.8.1。配置
将 MyBatis 正确插入 Spring Data JDBC 的最简单方法是导入MyBatisJdbcConfiguration
应用程序配置:
@Configuration
@EnableJdbcRepositories
@Import(MyBatisJdbcConfiguration.class)
class Application {
@Bean
SqlSessionFactoryBean sqlSessionFactoryBean() {
// Configure MyBatis here
}
}
如您所见,您需要声明的只是SqlSessionFactoryBean
asMyBatisJdbcConfiguration
依赖于最终SqlSession
可用的 bean 。ApplicationContext
9.8.2. 使用约定
对于 中的每个操作CrudRepository
,Spring Data JDBC 运行多个语句。如果SqlSessionFactory
在应用程序上下文中有一个,Spring Data 会为每个步骤检查是否SessionFactory
提供了一个语句。如果找到,则使用该语句(包括其配置的到实体的映射)。
语句的名称是通过将实体类型的完全限定名称与确定语句的类型连接起来而构造Mapper.
的String
。例如,如果org.example.User
要插入 的实例,Spring Data JDBC 会查找名为org.example.UserMapper.insert
.
当语句运行时,[ MyBatisContext
] 的一个实例作为参数传递,这使得语句可以使用各种参数。
下表描述了可用的 MyBatis 语句:
姓名 | 目的 | 可能触发此语句的 CrudRepository 方法 | 中可用的属性MyBatisContext |
---|---|---|---|
|
插入单个实体。这也适用于聚合根引用的实体。 |
|
|
|
更新单个实体。这也适用于聚合根引用的实体。 |
|
|
|
删除单个实体。 |
|
|
|
删除用作给定属性路径前缀的类型的任何聚合根引用的所有实体。请注意,用于为语句名称添加前缀的类型是聚合根的名称,而不是要删除的实体的名称。 |
|
|
|
删除用作前缀的类型的所有聚合根 |
|
|
|
删除具有给定 propertyPath 的聚合根引用的所有实体 |
|
|
|
按 ID 选择聚合根 |
|
|
|
选择所有聚合根 |
|
|
|
按 ID 值选择一组聚合根 |
|
|
|
选择一组被另一个实体引用的实体。引用实体的类型用于前缀。引用的实体类型用作后缀。此方法已弃用。 |
所有 |
|
|
选择由另一个实体通过属性路径引用的一组实体。 |
所有 |
|
|
选择所有聚合根,排序 |
|
|
|
选择聚合根的页面,可选择排序 |
|
|
|
计算用作前缀的类型的聚合根数 |
|
|
9.9。生命周期事件
Spring Data JDBC 触发事件,这些事件被发布到ApplicationListener
应用程序上下文中的任何匹配 bean。例如,在保存聚合之前调用以下侦听器:
@Bean
ApplicationListener<BeforeSaveEvent<Object>> loggingSaves() {
return event -> {
Object entity = event.getEntity();
LOG.info("{} is getting saved.", entity);
};
}
如果您只想处理特定域类型的事件,您可以从其中派生侦听器AbstractRelationalEventListener
并覆盖一个或多个onXXX
方法,其中XXX
代表事件类型。回调方法只会被与域类型及其子类型相关的事件调用,因此您不需要进一步转换。
class PersonLoadListener extends AbstractRelationalEventListener<Person> {
@Override
protected void onAfterLoad(AfterLoadEvent<Person> personLoad) {
LOG.info(personLoad.getEntity());
}
}
下表描述了可用的事件:
事件 | 什么时候发布 |
---|---|
在聚合根被删除之前。 |
|
在聚合根被删除后。 |
|
在聚合根被转换为执行 SQL 语句的计划之前,但在决定聚合是否是新的之后,即是否按顺序进行更新或插入。如果您想以编程方式设置 id,这是正确的事件。 |
|
在保存聚合根之前(即插入或更新,但在决定是否插入或更新之后)。 |
|
在保存聚合根之后(即插入或更新)。 |
|
在从数据库创建聚合根 |
|
在从数据库创建聚合根 |
生命周期事件依赖于 a ApplicationEventMulticaster ,在这种情况下SimpleApplicationEventMulticaster 可以配置 a TaskExecutor ,因此不保证何时处理事件。
|
9.9.1。特定于商店的 EntityCallbacks
Spring Data JDBC 使用EntityCallback
API 来提供审计支持,并对以下回调做出反应:
EntityCallback |
什么时候发布 |
---|---|
在聚合根被删除之前。 |
|
在聚合根被删除后。 |
|
在聚合根被转换为执行 SQL 语句的计划之前,但在决定聚合是否是新的之后,即是否按顺序进行更新或插入。如果您想以编程方式设置 id,这是正确的回调。 |
|
在保存聚合根之前(即插入或更新,但在决定是否插入或更新之后)。 在 Spring Data JDBC 的 2.x 版本中,您可以使用它来为实体创建 id。将来唯一正确使用回调的首选方式是 |
|
在保存聚合根之后(即插入或更新)。 |
|
在从数据库创建聚合根 |
|
在从数据库创建聚合根 |
9.10。实体回调
Spring Data 基础设施提供了在调用某些方法之前和之后修改实体的钩子。那些所谓EntityCallback
的实例提供了一种方便的方式来检查和潜在地修改回调风格的实体。
AnEntityCallback
看起来很像专门的ApplicationListener
. BeforeSaveEvent
一些 Spring Data 模块发布允许修改给定实体的存储特定事件(例如)。在某些情况下,例如使用不可变类型时,这些事件可能会导致麻烦。此外,事件发布依赖于ApplicationEventMulticaster
. 如果使用异步配置TaskExecutor
它可能会导致不可预测的结果,因为事件处理可以分叉到线程上。
实体回调提供具有同步和反应式 API 的集成点,以保证在处理链中定义明确的检查点按顺序执行,返回可能修改的实体或反应式包装器类型。
实体回调通常按 API 类型分隔。这种分离意味着同步 API 只考虑同步实体回调,而反应式实现只考虑反应式实体回调。
Spring Data Commons 2.2 引入了实体回调 API。这是应用实体修改的推荐方式。在调用可能已注册的实例之前 |
9.10.1。实现实体回调
AnEntityCallback
通过其泛型类型参数直接与其域类型相关联。每个 Spring Data 模块通常附带一组EntityCallback
涵盖实体生命周期的预定义接口。
EntityCallback
@FunctionalInterface
public interface BeforeSaveCallback<T> extends EntityCallback<T> {
/**
* Entity callback method invoked before a domain object is saved.
* Can return either the same or a modified instance.
*
* @return the domain object to be persisted.
*/
T onBeforeSave(T entity <2>, String collection <3>); (1)
}
1 | BeforeSaveCallback 在保存实体之前调用的特定方法。返回一个可能修改过的实例。 |
2 | 在持久化之前的实体。 |
3 | 许多存储特定参数,例如实体持久保存到的集合。 |
EntityCallback
@FunctionalInterface
public interface ReactiveBeforeSaveCallback<T> extends EntityCallback<T> {
/**
* Entity callback method invoked on subscription, before a domain object is saved.
* The returned Publisher can emit either the same or a modified instance.
*
* @return Publisher emitting the domain object to be persisted.
*/
Publisher<T> onBeforeSave(T entity <2>, String collection <3>); (1)
}
1 | BeforeSaveCallback 在保存实体之前,在订阅时调用的特定方法。发出一个可能修改过的实例。 |
2 | 在持久化之前的实体。 |
3 | 许多存储特定参数,例如实体持久保存到的集合。 |
可选的实体回调参数由实现的 Spring Data 模块定义,并从EntityCallback.callback() .
|
实现适合您的应用程序需求的接口,如下例所示:
BeforeSaveCallback
class DefaultingEntityCallback implements BeforeSaveCallback<Person>, Ordered { (2)
@Override
public Object onBeforeSave(Person entity, String collection) { (1)
if(collection == "user") {
return // ...
}
return // ...
}
@Override
public int getOrder() {
return 100; (2)
}
}
1 | 根据您的要求实现回调。 |
2 | 如果存在多个相同域类型的实体回调,则可能会订购实体回调。排序遵循最低优先级。 |
9.10.2。注册实体回调
EntityCallback
bean 被商店特定的实现拾取,以防它们在ApplicationContext
. 大多数模板 API 已经实现ApplicationContextAware
,因此可以访问ApplicationContext
以下示例说明了一组有效的实体回调注册:
EntityCallback
Bean 注册@Order(1) (1)
@Component
class First implements BeforeSaveCallback<Person> {
@Override
public Person onBeforeSave(Person person) {
return // ...
}
}
@Component
class DefaultingEntityCallback implements BeforeSaveCallback<Person>,
Ordered { (2)
@Override
public Object onBeforeSave(Person entity, String collection) {
// ...
}
@Override
public int getOrder() {
return 100; (2)
}
}
@Configuration
public class EntityCallbackConfiguration {
@Bean
BeforeSaveCallback<Person> unorderedLambdaReceiverCallback() { (3)
return (BeforeSaveCallback<Person>) it -> // ...
}
}
@Component
class UserCallbacks implements BeforeConvertCallback<User>,
BeforeSaveCallback<User> { (4)
@Override
public Person onBeforeConvert(User user) {
return // ...
}
@Override
public Person onBeforeSave(User user) {
return // ...
}
}
1 | BeforeSaveCallback 从@Order 注释中接收它的命令。 |
2 | BeforeSaveCallback Ordered 通过接口实现接收其订单。 |
3 | BeforeSaveCallback 使用 lambda 表达式。默认无序,最后调用。请注意,由 lambda 表达式实现的回调不会公开输入信息,因此使用不可分配的实体调用这些会影响回调吞吐量。使用class orenum 为回调 bean 启用类型过滤。 |
4 | 在单个实现类中组合多个实体回调接口。 |
9.11。自定义转化
Spring Data JDBC 允许注册自定义转换器以影响值在数据库中的映射方式。目前,转换器仅应用于属性级别。
9.11.1。使用已注册的 Spring 转换器编写属性
以下示例显示了Converter
从Boolean
对象转换为String
值的 a 的实现:
import org.springframework.core.convert.converter.Converter;
@WritingConverter
public class BooleanToStringConverter implements Converter<Boolean, String> {
@Override
public String convert(Boolean source) {
return source != null && source ? "T" : "F";
}
}
这里有几件事需要注意:Boolean
并且String
都是简单类型,因此 Spring Data 需要提示该转换器应该应用的方向(读取或写入)。通过注释这个转换器,你可以指示 Spring Data 像在数据库中一样@WritingConverter
写入每个Boolean
属性。String
9.11.2。使用 Spring 转换器读取
以下示例显示了Converter
从 a 转换String
为Boolean
值的 a 的实现:
@ReadingConverter
public class StringToBooleanConverter implements Converter<String, Boolean> {
@Override
public Boolean convert(String source) {
return source != null && source.equalsIgnoreCase("T") ? Boolean.TRUE : Boolean.FALSE;
}
}
这里有几件事需要注意:String
并且Boolean
都是简单类型,因此 Spring Data 需要提示该转换器应该应用的方向(读取或写入)。通过注释这个转换器,@ReadingConverter
你可以指示 Spring Data 转换String
数据库中应该分配给Boolean
属性的每个值。
9.11.3。注册 Spring 转换器JdbcConverter
class MyJdbcConfiguration extends AbstractJdbcConfiguration {
// …
@Override
protected List<?> userConverters() {
return Arrays.asList(new BooleanToStringConverter(), new StringToBooleanConverter());
}
}
在以前版本的 Spring Data JDBC 中,建议直接覆盖AbstractJdbcConfiguration.jdbcCustomConversions() . 这不再是必要的,甚至是不推荐的,因为该方法组合了用于所有数据库的转换Dialect 、用户注册的转换和用户注册的转换。如果您正在从旧版本的 Spring Data JDBC 迁移并且已经AbstractJdbcConfiguration.jdbcCustomConversions() 覆盖了您的转换,Dialect 则不会注册。
|
9.11.4。数据库值
值转换用于JdbcValue
丰富传播到具有java.sql.Types
类型的 JDBC 操作的值。如果您需要指定特定于 JDBC 的类型而不是使用类型派生,请注册自定义写入转换器。此转换器应将值转换为JdbcValue
具有值和实际字段的值JDBCType
。
下面的 SpringConverter
实现示例将 a 转换String
为自定义Email
值对象:
@ReadingConverter
public class EmailReadConverter implements Converter<String, Email> {
public Email convert(String source) {
return Email.valueOf(source);
}
}
如果您编写Converter
的源类型和目标类型都是本机类型,我们无法确定应该将其视为读取转换器还是写入转换器。将转换器实例注册为两者可能会导致不需要的结果。例如,a是模棱两可的,尽管在编写时尝试将所有实例转换为实例Converter<String, Long>
可能没有意义。为了让您强制基础架构仅以一种方式注册转换器,我们提供了要在转换器实现中使用的注释。String
Long
@ReadingConverter
@WritingConverter
转换器需要进行显式注册,因为不会从类路径或容器扫描中提取实例,以避免不必要的转换服务注册以及此类注册产生的副作用。转换器注册CustomConversions
为中央设施,允许根据源和目标类型注册和查询已注册的转换器。
CustomConversions
附带一组预定义的转换器注册:
-
JSR-310 用于在和类型之间
java.time
进行转换的转换器。java.util.Date
String
-
已弃用:用于在
org.joda.time
、JSR-310 和java.util.Date
. -
已弃用:ThreeTenBackport 转换器,用于在
org.joda.time
、JSR-310 和java.util.Date
.
本地时间类型(例如LocalDateTime to java.util.Date )的默认转换器依赖于系统默认时区设置在这些类型之间进行转换。您可以通过注册自己的转换器来覆盖默认转换器。
|
转换器消歧
通常,我们检查Converter
它们转换的源和目标类型的实现。根据其中一个是否是底层数据访问 API 可以本地处理的类型,我们将转换器实例注册为读取或写入转换器。以下示例显示了写入和读取转换器(请注意,区别在于 限定词的顺序Converter
):
// Write converter as only the target type is one that can be handled natively
class MyConverter implements Converter<Person, String> { … }
// Read converter as only the source type is one that can be handled natively
class MyConverter implements Converter<String, Person> { … }
9.12。日志记录
Spring Data JDBC 本身几乎没有日志记录。相反,JdbcTemplate
发出 SQL 语句的机制提供了日志记录。因此,如果您想检查运行了哪些 SQL 语句,请激活 SpringNamedParameterJdbcTemplate
或MyBatis的日志记录。
9.13。交易性
CrudRepository
实例的方法默认是事务性的。对于读取操作,事务配置readOnly
标志设置为true
。所有其他人都配置了一个简单的@Transactional
注释,以便应用默认的事务配置。有关详细信息,请参阅SimpleJdbcRepository
. 如果您需要调整存储库中声明的方法之一的事务配置,请在存储库接口中重新声明该方法,如下所示:
interface UserRepository extends CrudRepository<User, Long> {
@Override
@Transactional(timeout = 10)
List<User> findAll();
// Further query method declarations
}
上述导致该findAll()
方法以 10 秒的超时时间运行并且没有readOnly
标志。
另一种改变事务行为的方法是使用通常覆盖多个存储库的外观或服务实现。其目的是为非 CRUD 操作定义事务边界。以下示例显示了如何创建这样的外观:
@Service
public class UserManagementImpl implements UserManagement {
private final UserRepository userRepository;
private final RoleRepository roleRepository;
UserManagementImpl(UserRepository userRepository,
RoleRepository roleRepository) {
this.userRepository = userRepository;
this.roleRepository = roleRepository;
}
@Transactional
public void addRoleToAllUsers(String roleName) {
Role role = roleRepository.findByName(roleName);
for (User user : userRepository.findAll()) {
user.addRole(role);
userRepository.save(user);
}
}
前面的示例导致调用addRoleToAllUsers(…)
在事务中运行(参与现有事务或创建新事务,如果没有正在运行)。存储库的事务配置被忽略,因为外部事务配置决定了要使用的实际存储库。请注意,您必须显式激活<tx:annotation-driven />
或使用@EnableTransactionManagement
才能获得基于注释的配置以使外观正常工作。请注意,前面的示例假设您使用组件扫描。
9.13.1。事务查询方法
要让您的查询方法具有事务性,@Transactional
请在您定义的存储库接口处使用,如以下示例所示:
@Transactional(readOnly = true)
interface UserRepository extends CrudRepository<User, Long> {
List<User> findByLastname(String lastname);
@Modifying
@Transactional
@Query("delete from User u where u.active = false")
void deleteInactiveUsers();
}
通常,您希望将该readOnly
标志设置为 true,因为大多数查询方法只读取数据。与此相反,deleteInactiveUsers()
使用@Modifying
注释并覆盖事务配置。因此,该方法的readOnly
标志设置为false
。
强烈建议使查询方法具有事务性。这些方法可能会执行一个以上的查询以填充实体。没有公共事务,Spring Data JDBC 在不同的连接中执行查询。这可能会给连接池带来过大的压力,甚至可能在多个方法在保持一个连接的同时请求新连接时导致死锁。 |
通过设置标志来标记只读查询绝对是合理的readOnly 。但是,这并不能作为您不触发操纵查询的检查(尽管某些数据库拒绝INSERT 和UPDATE 声明在只读事务中)。相反,该readOnly 标志作为提示传播到底层 JDBC 驱动程序以进行性能优化。
|
9.14。审计
9.14.1。基本
Spring Data 提供了复杂的支持来透明地跟踪谁创建或更改了实体以及更改发生的时间。要从该功能中受益,您必须为实体类配备审计元数据,这些元数据可以使用注释或通过实现接口来定义。此外,必须通过 Annotation 配置或 XML 配置启用审计,以注册所需的基础架构组件。有关配置示例,请参阅特定于商店的部分。
仅跟踪创建和修改日期的应用程序不需要指定 |
基于注释的审计元数据
我们提供@CreatedBy
并@LastModifiedBy
捕获创建或修改实体的用户,以及@CreatedDate
捕获@LastModifiedDate
更改发生的时间。
class Customer {
@CreatedBy
private User user;
@CreatedDate
private Instant createdDate;
// … further properties omitted
}
如您所见,可以有选择地应用注释,具体取决于您要捕获的信息。DateTime
可以在 Joda-Time、 、legacy JavaDate
和Calendar
、JDK8 日期和时间类型以及long
或类型的属性上使用捕获更改时捕获的注释Long
。
审核元数据不一定需要存在于根级别实体中,但可以添加到嵌入式实体中(取决于实际使用的存储),如下面的片段所示。
class Customer {
private AuditMetadata auditingMetadata;
// … further properties omitted
}
class AuditMetadata {
@CreatedBy
private User user;
@CreatedDate
private Instant createdDate;
}
AuditorAware
如果您使用@CreatedBy
或@LastModifiedBy
,审计基础架构需要以某种方式了解当前主体。为此,我们提供了一个AuditorAware<T>
SPI 接口,您必须实现该接口来告诉基础设施当前与应用程序交互的用户或系统是谁。泛型类型T
定义了属性注释@CreatedBy
或@LastModifiedBy
必须是什么类型。
以下示例显示了使用 Spring SecurityAuthentication
对象的接口实现:
AuditorAware
基于 Spring Security的实现class SpringSecurityAuditorAware implements AuditorAware<User> {
@Override
public Optional<User> getCurrentAuditor() {
return Optional.ofNullable(SecurityContextHolder.getContext())
.map(SecurityContext::getAuthentication)
.filter(Authentication::isAuthenticated)
.map(Authentication::getPrincipal)
.map(User.class::cast);
}
}
实现访问Authentication
Spring Security 提供的对象并查找UserDetails
您在UserDetailsService
实现中创建的自定义实例。我们在这里假设您通过UserDetails
实现公开域用户,但根据Authentication
找到的结果,您也可以从任何地方查找它。
ReactiveAuditorAware
使用反应式基础架构时,您可能希望利用上下文信息来提供@CreatedBy
信息@LastModifiedBy
。我们提供了一个ReactiveAuditorAware<T>
SPI 接口,您必须实现该接口来告诉基础设施当前与应用程序交互的用户或系统是谁。泛型类型T
定义了属性注释@CreatedBy
或@LastModifiedBy
必须是什么类型。
以下示例显示了使用响应式 Spring SecurityAuthentication
对象的接口实现:
ReactiveAuditorAware
基于 Spring Security的实现class SpringSecurityAuditorAware implements ReactiveAuditorAware<User> {
@Override
public Mono<User> getCurrentAuditor() {
return ReactiveSecurityContextHolder.getContext()
.map(SecurityContext::getAuthentication)
.filter(Authentication::isAuthenticated)
.map(Authentication::getPrincipal)
.map(User.class::cast);
}
}
实现访问Authentication
Spring Security 提供的对象并查找UserDetails
您在UserDetailsService
实现中创建的自定义实例。我们在这里假设您通过UserDetails
实现公开域用户,但根据Authentication
找到的结果,您也可以从任何地方查找它。
9.15。JDBC 审计
为了激活审计,添加@EnableJdbcAuditing
到您的配置中,如以下示例所示:
@Configuration
@EnableJdbcAuditing
class Config {
@Bean
AuditorAware<AuditableUser> auditorProvider() {
return new AuditorAwareImpl();
}
}
如果您向 公开一个类型的 bean AuditorAware
,ApplicationContext
审计基础设施会自动选择它并使用它来确定要在域类型上设置的当前用户。如果您在 中注册了多个实现ApplicationContext
,则可以通过显式设置 的auditorAwareRef
属性来选择要使用的一个@EnableJdbcAuditing
。
9.16。JDBC 锁定
Spring Data JDBC 支持锁定派生查询方法。要启用对存储库中给定派生查询方法的锁定,请使用@Lock
. type 的 required 值LockMode
提供了两个值:PESSIMISTIC_READ
一个保证你正在读取的数据不会被修改,PESSIMISTIC_WRITE
另一个是获取一个锁来修改数据。一些数据库没有进行这种区分。在这种情况下,两种模式都等效于PESSIMISTIC_WRITE
。
interface UserRepository extends CrudRepository<User, Long> {
@Lock(LockMode.PESSIMISTIC_READ)
List<User> findByLastname(String lastname);
}
正如您在上面看到的,该方法findByLastname(String lastname)
将使用悲观读锁执行。如果您使用 MySQL 方言的数据库,这将导致例如以下查询:
Select * from user u where u.lastname = lastname LOCK IN SHARE MODE
替代LockMode.PESSIMISTIC_READ
你可以使用LockMode.PESSIMISTIC_WRITE
.
附录
附录 A:词汇表
- AOP
-
面向方面的编程
- CRUD
-
创建、读取、更新、删除 - 基本的持久化操作
- 依赖注入
-
将组件的依赖项从外部传递给组件的模式,释放组件以查找依赖项本身。有关详细信息,请参阅https://en.wikipedia.org/wiki/Dependency_Injection。
- JPA
-
Java 持久性 API
- Spring
-
Java应用框架—— https://projects.spring.io/spring-framework
附录 B:命名空间参考
<repositories />
元素_
该<repositories />
元素触发 Spring Data 存储库基础结构的设置。最重要的属性是base-package
,它定义了要扫描 Spring Data 存储库接口的包。请参阅“ XML 配置”。下表描述了<repositories />
元素的属性:
姓名 | 描述 |
---|---|
|
定义要扫描的包以 |
|
定义后缀以自动检测自定义存储库实现。名称以配置后缀结尾的类被视为候选。默认为 |
|
确定用于创建查找器查询的策略。有关详细信息,请参阅“查询查找策略”。默认为 |
|
定义搜索包含外部定义查询的属性文件的位置。 |
|
是否应考虑嵌套存储库接口定义。默认为 |
附录 C:Populators 命名空间参考
<populator /> 元素
该<populator />
元素允许通过 Spring Data 存储库基础结构填充数据存储。[ 1 ]
姓名 | 描述 |
---|---|
|
在哪里可以找到从存储库中读取对象的文件。 |
附录 D:存储库查询关键字
支持的查询方法主题关键字
下表列出了 Spring Data repository 查询派生机制通常支持的主题关键字来表达谓词。有关受支持关键字的确切列表,请参阅特定于商店的文档,因为此处列出的某些关键字可能在特定商店中不受支持。
关键词 | 描述 |
---|---|
|
一般查询方法通常返回存储库类型、 |
|
存在投影,通常返回 |
|
计数投影返回数字结果。 |
|
删除查询方法返回无结果 ( |
|
将查询结果限制为第一个 |
|
使用不同的查询仅返回唯一的结果。请查阅特定于商店的文档是否支持该功能。此关键字可以出现在 |
支持的查询方法谓词关键字和修饰符
下表列出了 Spring Data 存储库查询派生机制普遍支持的谓词关键字。但是,请参阅特定于商店的文档以获取受支持关键字的确切列表,因为此处列出的某些关键字可能在特定商店中不受支持。
逻辑关键字 | 关键字表达式 |
---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
除了过滤谓词之外,还支持以下修饰符列表:
关键词 | 描述 |
---|---|
|
与谓词关键字一起用于不区分大小写的比较。 |
|
忽略所有合适属性的大小写。在查询方法谓词的某处使用。 |
|
指定静态排序顺序,后跟属性路径和方向(例如 |
附录 E:存储库查询返回类型
支持的查询返回类型
下表列出了 Spring Data 存储库通常支持的返回类型。但是,请参阅特定于商店的文档以获取支持的返回类型的确切列表,因为此处列出的某些类型可能在特定商店中不受支持。
地理空间类型(例如GeoResult 、GeoResults 和GeoPage )仅适用于支持地理空间查询的数据存储。一些商店模块可能会定义自己的结果包装器类型。
|
返回类型 | 描述 |
---|---|
|
表示没有返回值。 |
原语 |
Java 原语。 |
包装器类型 |
Java 包装器类型。 |
|
一个独特的实体。期望查询方法最多返回一个结果。如果没有找到结果, |
|
一个 |
|
一个 |
|
一个 |
|
Java 8 或 Guava |
|
Scala 或 Vavr |
|
一个 Java 8 |
|
该直接的便利扩展 |
实现 |
公开构造函数或 |
Vavr |
Vavr 集合类型。有关详细信息,请参阅对 Vavr 集合的支持。 |
|
一个 |
|
一个 Java 8 |
|
一个 |
|
一定大小的数据块,指示是否有更多可用数据。需要 |
|
A |
|
带有附加信息的结果条目,例如到参考位置的距离。 |
|
带有附加信息的列表 |
|
A |
|
|
|
|
|
|
|
|
|
|