Spring Data(数据)Neo4j

时间:2022-11-24 15:20:38

版本 7.0.0

Spring Data(数据)Neo4j

1. 您浏览本文档的方式

本文档试图在广泛的可能用户之间架起桥梁:

  • 所有Spring生态系统的新手,包括Spring Framework,Spring Data,具体模块(在本例中为Spring Data Neo4j) 和 Neo4j。
  • 经验丰富的 Neo4j 开发人员,刚接触 Spring 数据并希望充分利用他们的 Neo4j 知识,但不熟悉 例如,使用声明式事务以及如何将后者与 Neo4j 集群要求合并。
  • 经验丰富的 Spring 数据开发人员,他们不熟悉此特定模块和 Neo4j,需要学习如何构建块 一起互动。虽然这个模块的编程范式与Spring Data JDBC,Mongo和其他模块非常一致, 查询语言(Cypher)、事务和聚类行为是不同的,不能抽象出来。

以下是我们如何满足这些不同的需求:

许多Neo4j的具体问题可以在常见问题中找到。这些问题是 特别适合那些非常了解Neo4j特定要求并想知道如何解决它们的人 与Spring Data Neo4j。

如果您已经熟悉 Spring 数据的核心概念,请直接阅读第 7 章。 本章将引导您完成配置应用程序以连接到 Neo4j 实例的不同选项以及如何对您的域进行建模。

在大多数情况下,您将需要一个域。 转到第 8 章,了解如何将节点和关系映射到域模型。

之后,您将需要一些方法来查询域。 选择是 Neo4j 存储库、Neo4j 模板或较低级别的 Neo4j 客户端。 所有这些都以反应方式提供。 除了分页机制之外,标准存储库的所有功能都可以在反应式变体中使用。

如果你来自旧版本的Spring Data Neo4j(通常缩写为SDN+OGM或SDN5)。 您很可能会对SDN的介绍感兴趣,尤其是SDN + OGM与当前SDN之间的关系。在同一章中,您将了解SDN6的构建块。

要了解有关存储库的一般概念的更多信息,请转到第 9 章。

您当然可以继续阅读,继续阅读序言和温和的入门指南。

2. 介绍 Neo4j

图形数据库是一种存储引擎,专门用于存储和检索庞大的信息网络。 它有效地将数据存储为与其他节点甚至相同节点具有关系的节点,从而允许对这些结构进行高性能检索和查询。 可以将属性添加到节点和关系中。 节点可以由零个或多个标签标记,关系始终是定向和命名的。

图形数据库非常适合存储大多数类型的域模型。 在几乎所有领域,都有某些事物与其他事物相连。 在大多数其他建模方法中,事物之间的关系被简化为没有标识和属性的单个链接。 图形数据库允许将源自域的丰富关系在数据库中保持同样良好的表示,而无需将关系建模为“事物”。 将现实生活中的域放入图形数据库时,几乎没有“阻抗不匹配”。

Neo4j是一个开源的NoSQL图形数据库。 它是一个完全事务数据库(ACID),存储结构化为由节点组成的图形的数据,由关系连接。 受现实世界结构的启发,它允许对复杂数据进行高查询性能,同时为开发人员保持直观和简单。

了解Neo4j的起点是neo4j.com。 以下是有用资源的列表:

  • Neo4j文档介绍了Neo4j,并包含入门指南,参考文档和教程的链接。
  • 在线沙盒结合在线教程提供了一种与 Neo4j 实例交互的便捷方式。
  • Neo4j Java Bolt Driver
  • 几本书可供购买,视频可供观看。

3. 弹簧数据简介

Spring Data使用Spring Framework的核心功能,例如IoC容器,类型转换系统,表达式语言,JMX集成和可移植的DAO异常层次结构。 虽然没有必要了解所有的Spring API,但了解它们背后的概念是必要的。 至少,IoC背后的想法应该是熟悉的。

要了解有关 Spring 的更多信息,您可以参考详细解释 Spring 框架的综合文档。 有很多关于这个问题的文章,博客条目和书籍 - 请查看Spring Framework主页以获取更多信息。

Spring Data的美妙之处在于它将相同的编程模型应用于各种不同的存储,例如JPA,JDBC。 蒙戈等。出于这个原因,本文档中包含了部分常规 Spring 数据文档,尤其是 关于使用 Spring 数据存储库的一般章节。如果您还没有,请务必查看一下 过去使用过 Spring 数据模块。

4. 介绍 Spring Data Neo4j

Spring Data Neo4j或简称SDN是下一代Spring Data模块,由Neo4j,Inc.创建和维护。与VMware的Spring Data Team密切合作。 它支持所有官方支持的Neo4j版本,包括Neo4j AuraDB。 Spring Data Neo4j项目将上述Spring Data概念应用于使用Neo4j图形数据存储开发解决方案。

SDN完全依赖于Neo4j Java驱动程序,而无需在映射框架和驱动程序之间引入另一个“驱动程序”或“传输”层。Neo4j Java驱动程序 - 有时被称为Bolt或Bolt驱动程序 - 被用作协议,就像JDBC与关系数据库一样。

SDN是一个对象-图形-映射(OGM)库。 OGM 将图中的节点和关系映射到域模型中的对象和引用。 对象实例映射到节点,而对象引用使用关系映射,或序列化为属性(例如,对日期的引用)。 JVM 基元映射到节点或关系属性。 OGM 抽象化数据库,并提供一种方便的方法,将域模型保留在图形中并查询它,而无需直接使用低级别驱动程序。 它还为开发人员提供了灵活性,可以在 SDN 生成的查询不足的情况下提供自定义查询。

SDN 是先前 SDN 版本 5 的正式继承者,本文档将其称为 SDN+OGM。 SDN版本5使用了一个单独的对象映射框架,就像Spring Data JPA与JPA的关系一样。 这个单独的层又名Neo4j-OGM(Neo4j对象图映射器)现在包含在这个模块本身中。 Spring Data Neo4j本身是一个对象映射器,专用于Spring和Spring Boot应用程序以及一些受支持的Jakarta EE环境。 它不需要也不支持对象映射器的单独实现。

将当前SDN版本与以前的SDN + OGM区分开来的值得注意的功能是

  • SDN本身就是一个完整的OGM
  • 完全支持不可变实体,因此完全支持 Kotlin 的数据类
  • 完全支持 Spring 框架本身和 Spring Data 中的响应式编程模型
  • Neo4j客户端和反应式客户端功能,复活了模板而不是普通驱动程序的想法,简化了数据库访问

我们提供存储库作为高级抽象,用于存储和查询文档,以及用于通用域访问或通用查询执行的模板和客户端。 所有这些都与Spring的应用程序事务集成在一起。

Neo4j支持的核心功能可以直接使用,通过理论或理论或其反应变体。 所有这些都提供与Spring的应用程序级事务的集成。 在较低的级别上,您可以获取 Bolt 驱动程序实例,但在这些情况下您必须管理自己的事务。​​Neo4jClient​​​​Neo4jTemplate​

您仍然可以使用 Neo4j-OGM,即使在现代 Spring Boot 应用程序中也是如此。 但是您不能将其与 SDN 6 一起使用。 如果您尝试过,您将在两个不同且不相关的持久性上下文中拥有两组不同的实体。 因此,如果你想坚持使用Neo4j-OGM 3.2.x,你可以使用Spring Boot实例化的Java驱动程序,并将其传递给Neo4j-OGM会话。 仍然支持 Neo4j-OGM 3.2.x,我们建议在 Quarkus 等框架中使用它。 然而,在 Spring 启动应用程序中,你的主要选择应该是 SDN 6。

请务必阅读常见问题解答,其中我们解决了有关映射决策的许多反复出现的问题,以及如何显着改善与 Neo4j 集群实例(如Neo4j AuraDB和本地集群部署)的交互。

理解的重要概念是 Neo4j 书签,合并适当的重试机制(如 Spring 重试或 Resilience4j)的潜在需求(我们建议后者,因为这些知识也适用于Spring之外)以及在Neo4j 集群上下文中只读与写入查询的重要性。

5. Spring Data Neo4j 的构建块

5.1. 概述

SDN 由可组合的构建块组成。 它建立在Neo4j Java驱动程序之上。 Java 驱动程序的实例是通过 Spring Boot 的自动配置本身提供的。 驱动程序的所有配置选项都可以在命名空间中访问。 驱动程序 Bean 提供了命令式、异步和响应式方法来与 Neo4j 交互。spring.neo4j

您可以使用驱动程序在该 Bean 上提供的所有事务方法,例如自动提交事务、事务函数和非托管事务。 请注意,这些交易与正在进行的春季交易并不严格。

与Spring Data和Spring的平台或反应式事务管理器的集成始于Neo4j客户端。 客户端是SDN的一部分,通过单独的启动器进行配置。 该启动器的配置命名空间是。​​spring-boot-starter-data-neo4j​​​​spring.data.neo4j​

客户端与映射无关。 它不知道您的域类,您负责将结果映射到适合您需求的对象。

下一个更高级别的抽象是 Neo4j 模板。 它知道您的域,您可以使用它来查询任意域对象。 该模板在具有大量域类或自定义查询的方案中会派上用场,而您不希望为每个域类或自定义查询创建额外的存储库抽象。

*别的抽象是 Spring 数据存储库。

SDN的所有抽象都有命令式和响应式。 不建议在同一应用程序中混合使用这两种编程样式。 反应式基础架构需要一个 Neo4j 4.0+ 数据库。

Spring Data(数据)Neo4j

模板机制类似于其他商店的模板。 在我们的常见问题解答​中查找有关它的更多信息。 Neo4j客户端本身是SDN独有的。 您可以在附录中找到其文档。

5.2. 在软件包级别

描述

​org.springframework.data.neo4j.config​

此包包含与配置相关的支持类,可用于特定于应用程序的注释 配置类。如果你不依赖 Spring Boot 的自动配置,抽象基类会很有帮助。 该包提供了一些启用审核的其他注释。

​org.springframework.data.neo4j.core​

此包包含用于创建可执行查询的命令式或反应式客户端的核心基础结构。 标记为可安全使用的包。核心包提供对两者的访问 客户端和模板的命令式和反应式变体。​​@API(status = API.Status.STABLE)​

​org.springframework.data.neo4j.core.convert​

提供一组 SDN 支持的简单类型。允许引入额外的定制 变换 器。​​Neo4jConversions​

​org.springframework.data.neo4j.core.support​

此包提供了几个可能对您的域有用的支持类,例如谓词 指示某些事务可能会重试,并且可以使用其他转换器和 ID 生成器。

​org.springframework.data.neo4j.core.transaction​

包含将非托管 Neo4j 事务转换为 Spring 托管事务的核心基础结构。公开 命令式和反应性。​​TransactionManager​​​​Neo4jTransactionManager​​​​ReactiveNeo4jTransactionManager​

​org.springframework.data.neo4j.repository​

此软件包提供了 Neo4j 命令式和反应式存储库 API。

​org.springframework.data.neo4j.repository.config​

Neo4j 特定存储库的配置基础架构,尤其是启用命令式的专用注释 和反应式 Spring Data Neo4j 存储库。

​org.springframework.data.neo4j.repository.support​

这个包提供了几个公共支持类,用于构建自定义命令式和反应式Spring Data Neo4j 存储库基类。支持类与 SDN 本身使用的类相同。

6. 依赖关系

由于各个 Spring 数据模块的开始日期不同,因此它们中的大多数都带有不同的主要和次要版本号。找到兼容版本的最简单方法是依靠我们随附的与定义的兼容版本一起提供的春季数据发布列车 BOM。在 Maven 项目中,您将在 POM 的部分中声明此依赖项,如下所示:​​<dependencyManagement />​

例 1.使用弹簧数据发布列车物料清单

<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-bom</artifactId>
<version>2022.0.0</version>
<scope>import</scope>
<type>pom</type>
</dependency>
</dependencies>
</dependencyManagement>

当前发布训练版本是。火车版本使用带有图案的犊牛。 对于 GA 版本和服务版本,版本名称如下,对于所有其他版本,版本名称如下:,其中可以是以下之一:​​2022.0.0​​​​YYYY.MINOR.MICRO​​​​${calver}​​​​${calver}-${modifier}​​​​modifier​

  • ​SNAPSHOT​​:当前快照
  • ​M1​​,,等等:里程碑M2
  • ​RC1​​,,等等:发布候选版本RC2

您可以在我们的Spring 数据示例存储库中找到使用 BOM 的工作示例。有了这个,你可以声明你想要使用的 Spring 数据模块,而无需在块中有一个版本,如下所示:​​<dependencies />​

例 2.声明对 Spring 数据模块的依赖关系

<dependencies>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-jpa</artifactId>
</dependency>
<dependencies>

6.1. 使用 Spring 引导进行依赖管理

Spring Boot 会为你选择最新版本的 Spring 数据模块。如果仍要升级到较新版本,请将 要使用的训练版本和迭代的属性。​​spring-data-releasetrain.version​

6.2. 弹簧框架

Spring 数据模块的当前版本需要 Spring Framework 6.0.0 或更高版本。这些模块还可以使用该次要版本的较旧错误修复版本。但是,强烈建议使用该代中的最新版本。

参考文档

谁应该读这篇文章?

本手册适用于:

  • 企业架构师正在研究 Neo4j 的 Spring 集成。
  • 使用 Neo4j 开发基于 Spring Data 的应用程序的工程师。

7. 入门

我们为SDN提供了一个Spring Boot启动器。 例如,请通过依赖关系管理包含入门模块,并配置要使用的 bolt URL。 启动器假定服务器已禁用身份验证。 由于SDN启动器依赖于Java驱动程序的启动器,因此此处所述的有关配置的所有内容也适用于此处。 有关可用属性的参考,请在命名空间中使用 IDE 自动完成功能。​​spring.neo4j.uri=bolt://localhost:7687​​​​spring.neo4j​

SDN 支持

  • 众所周知和理解的命令式编程模型(很像Spring Data JDBC或JPA)
  • 基于反应式流的响应式编程,包括对反应式事务的完全支持。

这些都包含在同一个二进制文件中。 响应式编程模型在数据库端需要一个 4+ Neo4j 服务器,另一方面需要反应式 Spring 服务器。

7.1. 准备数据库

对于这个例子,我们停留在电影图中,因为它在每个 Neo4j 实例中都是免费的。

如果您没有正在运行的数据库,但安装了 Docker,请运行:

清单 1.在 Docker 中启动一个本地 Neo4j 实例。

docker run --publish=7474:7474 --publish=7687:7687 -e 'NEO4J_AUTH=neo4j/secret' neo4j:4.4.8

您现在可以访问​​http://localhost:7474​​。 上述命令将服务器的密码设置为。 请注意准备在提示符下运行的命令 ()。 执行它以用一些测试数据填充您的数据库。​​secret​​​​:play movies​

7.2. 创建一个新的 Spring 引导项目

设置 Spring Boot 项目的最简单方法是start.spring.io(如果您不想使用该网站,它也集成在主要的 IDE 中)。

选择“Spring Web Starter”以获取创建基于 Spring 的 Web 应用程序所需的所有依赖项。 Spring Initializr 将负责为您创建一个有效的项目结构,并为选定的构建工具准备好所有文件和设置。

7.2.1. 使用 Maven

您可以针对 Spring 初始值设定项发出curl请求以创建基本的 Maven 项目:

清单 2.使用 Spring Initializr 创建一个基本的 Maven 项目

curl https://start.spring.io/starter.tgz \
-d dependencies=webflux,data-neo4j \
-d bootVersion=2.6.3 \
-d baseDir=Neo4jSpringBootExample \
-d name=Neo4j%20SpringBoot%20Example | tar -xzvf -

这将创建一个新文件夹。 由于此启动器尚未在初始值设定项上,因此您必须手动将以下依赖项添加到您的依赖项中:​​Neo4jSpringBootExample​​​​pom.xml​

清单 3.在Maven项目中包含spring-data-neo4j-spring-boot-starter

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-neo4j</artifactId>
</dependency>

对于现有项目,您还可以手动添加依赖项。

7.2.2. 使用 Gradle

思路是一样的,只需生成一个 Gradle 项目:

清单 4.使用 Spring Initializr 创建一个基本的 Gradle 项目

curl https://start.spring.io/starter.tgz \
-d dependencies=webflux,data-neo4j \
-d type=gradle-project \
-d bootVersion=2.6.3 \
-d baseDir=Neo4jSpringBootExampleGradle \
-d name=Neo4j%20SpringBoot%20Example | tar -xzvf -

Gradle 的依赖项如下所示,必须添加到:​​build.gradle​

清单 5.在 Gradle 项目中包含 spring-data-neo4j-spring-boot-starter

dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-neo4j'
}

对于现有项目,您还可以手动添加依赖项。

7.3. 配置项目

现在,在您喜欢的 IDE 中打开这些项目中的任何一个。 查找并配置您的 Neo4j 凭据:​​application.properties​

spring.neo4j.uri=bolt://localhost:7687
spring.neo4j.authentication.username=neo4j
spring.neo4j.authentication.password=secret

这是连接到 Neo4j 实例所需的最低限度。

使用此启动器时,无需添加驱动程序的任何编程配置。 SDN 存储库将由此启动器自动启用。

7.4. 在模块路径上运行 (Java 9+)

Spring Data Neo4j可以在模块路径上运行。它的自动模块名称是。 由于当前 Spring 数据构建设置中的限制,它本身不提供模块。 因此,它使用自动但稳定的模块名称。但是,它确实取决于 一个模块化的库(Cypher-DSL)。没有由于 上述限制,我们不能代表您表达对该库的要求。​​spring.data.neo4j​​​​module-info.java​

因此,在您的项目中,在模块路径上运行 Spring Data Neo4j 6.1+ 所需的最低要求 如下:​​module-info.java​

清单 6.是一个应该在模块路径上使用Spring Data Neo4j的项目​​module-info.java​

module your.module {

requires org.neo4j.cypherdsl.core;

requires spring.data.commons;
requires spring.data.neo4j;

opens your.domain to spring.core;

exports your.domain;
}

Spring Data Neo4j使用Spring Data Commons及其反射功能,因此 您至少需要打开您的域包。​​spring.core​

我们假设这里还包含存储库:必须导出这些存储库才能由 和 访问。如果您不想将它们出口到世界各地, 您可以将它们限制为这些模块。​​your.domain​​​​spring.beans​​​​spring.context​​​​spring.data.commons​

7.5. 创建您的域名

我们的域层应该完成两件事:

  • 将图形映射到对象
  • 提供对那些

7.5.1. 示例节点实体

SDN 完全支持不可修改的实体,适用于 Java 和 Kotlin 中的类。 因此,我们将在这里重点介绍不可变实体,清单 7显示了这样一个实体。​​data​

SDN支持Neo4j Java驱动程序支持的所有数据类型,请参阅“密码类型系统”一章中的将Neo4j类型映射到本地语言类型。 未来版本将支持其他转换器。

清单 7.电影实体.java

@Node("Movie") 
public class MovieEntity {

@Id
private final String title;

@Property("tagline")
private final String description;

@Relationship(type = "ACTED_IN", direction = Direction.INCOMING)
private List<Roles> actorsAndRoles;

@Relationship(type = "DIRECTED", direction = Direction.INCOMING)
private List<PersonEntity> directors = new ArrayList<>();

public MovieEntity(String title, String description) {
this.title = title;
this.description = description;
}

// Getters omitted for brevity
}


​@Node​​​用于将此类标记为托管实体。 它还用于配置 Neo4j 标签。 标签默认为类的名称,如果您只是使用普通。​​@Node​


每个实体都必须有一个 ID。 此处显示的电影类使用 attributeas 唯一的业务键。 如果你没有这样的唯一密钥,你可以使用组合的and来配置SDN以使用Neo4j的内部ID。 我们还为 UUID 提供发电机。​​title​​​​@Id​​​​@GeneratedValue​


这显示为一种为字段使用与图形属性不同的名称的方法。​​@Property​


这定义了与类型和关系类型的类的关系​​PersonEntity​​​​ACTED_IN​


这是应用程序代码要使用的构造函数。

作为一般评论:使用内部生成的ID的不可变实体有点矛盾,因为SDN需要一种方法来使用数据库生成的值设置字段。

如果找不到好的业务密钥,或者不想对 ID 使用生成器,下面是使用内部生成的 id 以及常规构造函数和 SDN 使用的所谓wither-Method 的同一实体:

清单 8.电影实体.java

@Node("Movie")
public class MovieEntity {

@Id @GeneratedValue
private Long id;

private final String title;

@Property("tagline")
private final String description;

public MovieEntity(String title, String description) {
this.id = null;
this.title = title;
this.description = description;
}

public MovieEntity withId(Long id) {
if (this.id.equals(id)) {
return this;
} else {
MovieEntity newObject = new MovieEntity(this.title, this.description);
newObject.id = id;
return newObject;
}
}
}

这是应用程序代码要使用的构造函数。 它将 id 设置为 null,因为永远不应操作包含内部 id 的字段。

这就是所谓的属性枯萎。 它创建一个新实体并相应地设置字段,而不修改原始实体,从而使其不可变。​​id​

当然,您可以将 SDN 与 Kotlin 一起使用,并使用Kotlin的数据类对您的域进行建模。Project Lombok是另一种选择,如果你想或需要纯粹留在 Java 中。

7.5.2. 声明 Spring 数据存储库

您在这里基本上有两个选择: 您可以使用 SDN 以与商店无关的方式工作,并使您的域特定扩展之一

  • ​org.springframework.data.repository.Repository​
  • ​org.springframework.data.repository.CrudRepository​
  • ​org.springframework.data.repository.reactive.ReactiveCrudRepository​
  • ​org.springframework.data.repository.reactive.ReactiveSortingRepository​

相应地选择命令式和反应式。

虽然技术上不禁止,但不建议在同一应用程序中混合使用命令式和反应式数据库访问。 对于此类方案,我们将不支持你。

另一种选择是确定特定于商店的实现,并获得我们开箱即用支持的所有方法。 这种方法的优点也是它最大的缺点:一旦推出,所有这些方法都将成为 API 的一部分。 大多数时候,拿走一些东西比事后添加东西更难。 此外,使用商店详细信息会将您的商店泄漏到您的域中。 从性能的角度来看,没有惩罚。

适合上述任何电影实体的反应式存储库如下所示:

清单 9.电影存储库.java

public interface MovieRepository extends ReactiveNeo4jRepository<MovieEntity, String> {

Mono<MovieEntity> findOneByTitle(String title);
}

测试反应式代码是用 a 完成的。 查看Project Reactor 的相应文档​或查看我们的示例代码。​​reactor.test.StepVerifier​

8. 对象映射

以下部分将解释图形和域之间的映射过程。 它分为两部分。 第一部分介绍实际映射和可用工具,以描述如何将节点、关系和属性映射到对象。 第二部分将介绍Spring Data的对象映射基础知识。 它提供了有关常规映射的宝贵提示,为什么您应该更喜欢不可变的域对象以及如何使用 Java 或 Kotlin 对它们进行建模。

8.1. 基于元数据的映射

若要充分利用 SDN 中的对象映射功能,应使用注释对映射的对象进行注释。 尽管映射框架不必具有此注释(即使没有任何注释,您的 POJO 也已正确映射),但它允许类路径扫描程序查找并预处理域对象以提取必要的元数据。 如果不使用此注释,则应用程序在首次存储域对象时性能会略有下降,因为映射框架需要构建其内部元数据模型,以便它了解域对象的属性以及如何持久化它们。​​@Node​

8.1.1. 映射标注概述

从 SDN
  • ​@Node​​:在类级别应用,以指示此类是映射到数据库的候选项。
  • ​@Id​​:在字段级别应用,以标记用于标识目的的字段。
  • ​@GeneratedValue​​:在字段级别应用,并指定应如何生成唯一标识符。@Id
  • ​@Property​​:应用于字段级别以修改从属性到属性的映射。
  • ​@CompositeProperty​​:在字段级别应用于 Map 类型的属性,这些属性应作为复合回读。请参阅复合属性。
  • ​@Relationship​​:在字段级别应用以指定关系的详细信息。
  • ​@DynamicLabels​​:在字段级别应用以指定动态标签的来源。
  • ​@RelationshipProperties​​:在类级别应用,以指示此类作为关系属性的目标。
  • ​@TargetNode​​:应用于带注释的类的字段,以从另一端的角度标记该关系的目标。@RelationshipProperties

以下注释用于指定转换并确保与 OGM 的向后兼容性。

  • ​@DateLong​
  • ​@DateString​
  • ​@ConvertWith​

有关此内容的更多信息,请参阅转化。

来自春季数据共享
  • ​@org.springframework.data.annotation.Id​​事实上,与SDN相同,使用Spring Data Common的Id注释进行注释。@Id@Id
  • ​@CreatedBy​​:在字段级别应用以指示节点的创建者。
  • ​@CreatedDate​​:在字段级别应用以指示节点的创建日期。
  • ​@LastModifiedBy​​:在字段级别应用,以指示上次更改节点的作者。
  • ​@LastModifiedDate​​:在字段级别应用以指示节点的上次修改日期。
  • ​@PersistenceCreator​​:应用于一个构造函数,以在读取实体时将其标记为首选构造函数。
  • ​@Persistent​​:在类级别应用,以指示此类是映射到数据库的候选项。
  • ​@Version​​:应用于字段级别,用于乐观锁定,并检查保存操作的修改。 初始值为零,每次更新时都会自动增加。
  • ​@ReadOnlyProperty​​:在字段级别应用以将属性标记为只读。该属性将在数据库读取期间冻结, 但不受写入。在关系上使用时,请注意该集合中的任何相关实体都不会持久化 如果不是其他相关。

请查看第 12 章,了解有关审计支持的所有注释。

8.1.2. 基本构建块:​​@Node​

注释用于将类标记为托管域类,受映射上下文的类路径扫描的约束。​​@Node​

要将 Object 映射到图中的节点,反之亦然,我们需要一个标签来标识要映射到和从中映射的类。

​@Node​​具有一个属性允许您配置一个或多个标签,以便在读取和写入带批注的类的实例时使用。 属性是 的别名。 如果未指定标签,则简单类名将用作主标签。 如果要提供多个标签,可以:​​labels​​​​value​​​​labels​

  1. 向属性提供数组。 数组中的第一个元素将被视为主标签。labels
  2. 提供值并将其他标签放入。primaryLabellabels

主标签应始终是反映您的域类的最具体的标签。

对于通过存储库或 Neo4j 模板编写的注释类的每个实例,将写入图形中至少具有主标签的一个节点。 反之亦然,所有具有主标签的节点都将映射到带注释的类的实例。

关于类层次结构的说明

注释不是从超类型和接口继承的。 但是,您可以在每个继承级别单独批注域类。 这允许多态查询:您可以传入基类或中间类,并为节点检索正确的具体实例。 这仅适用于带有注释的抽象基础。 在此类上定义的标签将与具体实现的标签一起用作附加标签。​​@Node​​​​@Node​

对于某些方案,我们还支持域类层次结构中的接口:

清单 10.域模型位于单独的模块中,与接口名称相同的主标签

public interface SomeInterface { 

String getName();

SomeInterface getRelated();
}

@Node("SomeInterface")
public static class SomeInterfaceEntity implements SomeInterface {

@Id @GeneratedValue private Long id;

private final String name;

private SomeInterface related;

public SomeInterfaceEntity(String name) {
this.name = name;
}

@Override
public String getName() {
return name;
}

@Override
public SomeInterface getRelated() {
return related;
}
}

只是普通的界面名称,就像您命名域一样

由于我们需要同步主标签,我们放置了实现类,它 可能在另一个模块中。请注意,该值与接口名称完全相同 实现。无法重命名。​​@Node​

也可以使用不同的主标签代替接口名称:

清单 11.不同的主标签

@Node("PrimaryLabelWN") 
public interface SomeInterface2 {

String getName();

SomeInterface2 getRelated();
}

public static class SomeInterfaceEntity2 implements SomeInterface2 {

// Overrides omitted for brevity
}


将注释放在界面上​​@Node​

也可以使用接口的不同实现并具有多态域模型。 执行此操作时,至少需要两个标签:一个用于确定接口的标签和一个确定具体类的标签:

清单 12.多种实现

@Node("SomeInterface3") 
public interface SomeInterface3 {

String getName();

SomeInterface3 getRelated();
}

@Node("SomeInterface3a")
public static class SomeInterfaceImpl3a implements SomeInterface3 {

// Overrides omitted for brevity
}
@Node("SomeInterface3b")
public static class SomeInterfaceImpl3b implements SomeInterface3 {

// Overrides omitted for brevity
}

@Node
public static class ParentModel {

@Id
@GeneratedValue
private Long id;

private SomeInterface3 related1;

private SomeInterface3 related2;
}

在此方案中,需要显式指定标识接口的标签

这适用于第一个...

以及第二次实施

这是一个客户端或父模型,透明地用于两个关系​​SomeInterface3​

未指定具体类型

下面的测试显示了所需的数据结构。OGM 也会写同样的内容:

清单 13.使用多个不同接口实现所需的数据结构

Long id;
try (Session session = driver.session(bookmarkCapture.createSessionConfig()); Transaction transaction = session.beginTransaction()) {
id = transaction.run("" +
"CREATE (s:ParentModel{name:'s'}) " +
"CREATE (s)-[:RELATED_1]-> (:SomeInterface3:SomeInterface3b {name:'3b'}) " +
"CREATE (s)-[:RELATED_2]-> (:SomeInterface3:SomeInterface3a {name:'3a'}) " +
"RETURN id(s)")
.single().get(0).asLong();
transaction.commit();
}

Optional<Inheritance.ParentModel> optionalParentModel = transactionTemplate.execute(tx ->
template.findById(id, Inheritance.ParentModel.class));

assertThat(optionalParentModel).hasValueSatisfying(v -> {
assertThat(v.getName()).isEqualTo("s");
assertThat(v).extracting(Inheritance.ParentModel::getRelated1)
.isInstanceOf(Inheritance.SomeInterfaceImpl3b.class)
.extracting(Inheritance.SomeInterface3::getName)
.isEqualTo("3b");
assertThat(v).extracting(Inheritance.ParentModel::getRelated2)
.isInstanceOf(Inheritance.SomeInterfaceImpl3a.class)
.extracting(Inheritance.SomeInterface3::getName)
.isEqualTo("3a");
});

接口无法定义标识符字段。 因此,它们不是存储库的有效实体类型。

动态或“运行时”托管标签

通过简单类名隐式定义或通过注释显式定义的所有标签都是静态的。 它们在运行时无法更改。 如果需要可在运行时操作的其他标签,则可以在字段级别 use.is 注释,并将类型(aor)的属性标记为动态标签的源。​​@Node​​​​@DynamicLabels​​​​@DynamicLabels​​​​java.util.Collection<String>​​​​List​​​​Set​

如果存在此注释,则节点上存在的所有标签(而不是通过类名静态映射)将在加载期间收集到该集合中。 在写入过程中,节点的所有标签都将替换为静态定义的标签以及集合的内容。​​@Node​

如果有其他应用程序向节点添加其他标签,请不要使用。 在托管实体上存在 IFIS,生成的标签集将是写入数据库的“真相”。​​@DynamicLabels​​​​@DynamicLabels​

8.1.3. 识别实例:​​@Id​

在类和具有特定标签的节点之间创建映射时,我们还需要在该类(对象)的各个实例和节点实例之间建立连接。​​@Node​

这就是发挥作用的地方。将类的一个属性标记为对象的唯一标识符。 该唯一标识符在最佳环境中是唯一的业务键,换句话说,是自然键。可用于具有受支持的简单类型的所有属性。​​@Id​​​​@Id​​​​@Id​

然而,自然钥匙很难找到。 例如,人们的名字很少是唯一的,随着时间的推移而变化或更糟,不是每个人都有名字和姓氏。

因此,我们支持两种不同类型的代理键

在类型器的属性上,可以使用。 这会将 Neo4j 内部 ID(它不是节点或关系上的属性,通常不可见)映射到属性,并允许 SDN 检索类的各个实例。​​long​​​​Long​​​​@Id​​​​@GeneratedValue​

​@GeneratedValue​​提供属性。可用于指定类实现。 Anis 一个功能接口,它包含主标签和实例来生成 Id。 我们支持开箱即用的实现。​​generatorClass​​​​generatorClass​​​​IdGenerator​​​​IdGenerator​​​​generateId​​​​UUIDStringGenerator​

您还可以从应用程序上下文中指定 Spring Bean。 该 Bean 也需要实现,但可以利用上下文中的所有内容,包括 Neo4j 客户端或模板与数据库进行交互。​​@GeneratedValue​​​​generatorRef​​​​IdGenerator​

不要跳过第 8.2 节中有关 ID 处理的重要说明

8.1.4. 乐观锁定:​​@Version​

Spring Data Neo4j通过在非类型字段上使用注释来支持乐观锁定。 此属性将在更新期间自动递增,不得手动修改。​​@Version​​​​Long​

例如,如果不同线程中的两个事务想要使用版本修改同一对象,则第一个操作将成功持久化到数据库中。 此时,版本字段将递增,因此。 第二个操作将失败,因为它想要使用数据库中不再存在的版本修改对象。 在这种情况下,需要重试操作,首先从数据库中重新获取具有当前版本的对象。​​x​​​​x+1​​​​OptimisticLockingFailureException​​​​x​

如果使用企业 ID,则该属性也是必需的。 Spring Data Neo4j将检查此字段,以确定该实体是新的还是以前已经保留的。​​@Version​

8.1.5. 映射属性:​​@Property​

带注释的类的所有属性都将保留为 Neo4j 节点和关系的属性。 如果没有进一步的配置,Java 或 Kotlin 类中的属性名称将用作 Neo4j 属性。​​@Node​

如果您正在使用现有的 Neo4j 模式,或者只是想根据自己的需求调整映射,则需要使用。 用于指定数据库中属性的名称。​​@Property​​​​name​

8.1.6. 连接节点:​​@Relationship​

注释可用于非简单类型的所有属性。 它适用于其他类型的属性注释或集合及其地图。​​@Relationship​​​​@Node​

属性允许配置关系的类型,允许指定方向。 SDN 中的默认方向是。​​type​​​​value​​​​direction​​​​Relationship.Direction#OUTGOING​

我们支持动态关系。 动态关系表示为 aor。 在这种情况下,与其他域类的关系类型由 maps 键给出,不得通过 进行配置。​​Map<String, AnnotatedDomainClass>​​​​Map<Enum, AnnotatedDomainClass>​​​​@Relationship​

映射关系属性

Neo4j不仅支持在节点上定义属性,还支持在关系上定义属性。 为了在模型中表达这些属性,SDN提供应用于简单的Java类。 在属性类中,必须只有一个标记为 as 的字段来定义关系指向的实体。 或者,在关系的背景下,来自。​​@RelationshipProperties​​​​@TargetNode​​​​INCOMING​

关系属性类及其用法可能如下所示:

清单 14.关系属性​​Roles​

@RelationshipProperties
public class Roles {

@RelationshipId
private Long id;

private final List<String> roles;

@TargetNode
private final PersonEntity person;

public Roles(PersonEntity person, List<String> roles) {
this.person = person;
this.roles = roles;
}

public List<String> getRoles() {
return roles;
}
}

必须为生成的内部 ID () 定义属性,以便 SDN 可以在保存期间确定哪些关系 可以安全地覆盖而不会丢失属性。 如果 SDN 找不到用于存储内部节点 ID 的字段,则在启动过程中将失败。​​@RelationshipId​

清单 15.定义实体的关系属性

@Relationship(type = "ACTED_IN", direction = Direction.INCOMING) 
private List<Roles> actorsAndRoles;
关系查询备注

通常,创建查询的关系/跃点没有限制。 SDN 从建模节点解析整个可访问图。

也就是说,当有双向映射关系的想法时,这意味着您在实体的两端定义关系, 你可能会得到比你期望的更多。

考虑一个示例,其中一部电影有演员,并且您想获取某部电影及其所有演员。 如果从电影演员的关系只是单向的,这不会有问题。 在双向方案中,SDN 将获取特定电影、其参与者以及根据关系定义为此参与者定义的其他电影。 在最坏的情况下,这将级联到获取单个实体的整个图形。

8.1.7. 一个完整的例子

将所有这些放在一起,我们可以创建一个简单的域。 我们使用电影和不同角色的人:

例 3.这​​MovieEntity​

@Node("Movie") 
public class MovieEntity {

@Id
private final String title;

@Property("tagline")
private final String description;

@Relationship(type = "ACTED_IN", direction = Direction.INCOMING)
private List<Roles> actorsAndRoles;

@Relationship(type = "DIRECTED", direction = Direction.INCOMING)
private List<PersonEntity> directors = new ArrayList<>();

public MovieEntity(String title, String description) {
this.title = title;
this.description = description;
}

// Getters omitted for brevity
}

​@Node​​​用于将此类标记为托管实体。 它还用于配置 Neo4j 标签。 标签默认为类的名称,如果您只是使用普通。​​@Node​

每个实体都必须有一个 ID。 我们使用电影的名称作为唯一标识符。

这显示为一种为字段使用与图形属性不同的名称的方法。​​@Property​

这将配置与人员的传入关系。

这是应用程序代码和 SDN 要使用的构造函数。

人们在这里被映射为两个角色,并且。 域类是相同的:​​actors​​​​directors​

例 4.这​​PersonEntity​

@Node("Person")
public class PersonEntity {

@Id private final String name;

private final Integer born;

public PersonEntity(Integer born, String name) {
this.born = born;
this.name = name;
}

public Integer getBorn() {
return born;
}

public String getName() {
return name;
}

}

我们还没有在两个方向上模拟电影和人之间的关系。 为什么? 我们将 thes 视为聚合根,拥有关系。 另一方面,我们希望能够从数据库中提取所有人,而无需选择与他们关联的所有电影。 在尝试将数据库中的每个关系映射到各个方向之前,请考虑应用程序的用例。 虽然您可以这样做,但您最终可能会在对象图中重建图形数据库,这不是映射框架的意图。 如果您必须对循环域或双向域进行建模,并且不想获取整个图, 您可以使用投影​定义要提取的数据的细化描述。​​MovieEntity​

8.2. 唯一 ID 的处理和配置

8.2.1. 使用内部 Neo4j id

为域类提供唯一标识符的最简单方法是组合 andon 一个类型字段(最好是对象,而不是标量,因为文字是更好地指示实例是否是新的):​​@Id​​​​@GeneratedValue​​​​Long​​​​long​​​​null​

例 5.具有内部 Neo4j id 的可变电影实体

@Node("Movie")
public class MovieEntity {

@Id @GeneratedValue
private Long id;

private String name;

public MovieEntity(String name) {
this.name = name;
}
}

您不需要为字段提供资源库,SDN 将使用反射来分配字段,但如果有,请使用资源库。 如果要使用内部生成的 id 创建不可变实体,则必须提供wither

例 6.具有内部 Neo4j id 的不可变电影实体

@Node("Movie")
public class MovieEntity {

@Id @GeneratedValue
private final Long id;

private String name;

public MovieEntity(String name) {
this(null, name);
}

private MovieEntity(Long id, String name) {
this.id = id;
this.name = name;
}

public MovieEntity withId(Long id) {
if (this.id.equals(id)) {
return this;
} else {
return new MovieEntity(id, this.title);
}
}
}

指示生成值的不可变最终 id 字段

公共构造函数,由应用程序和 Spring 数据使用

内部使用的构造函数

这就是所谓的属性枯萎。 它创建一个新实体并相应地设置字段,而不修改原始实体,从而使其不可变。​​id​

如果你想拥有,你必须为 id 属性提供一个二传手或类似wither 的东西

  • 优点:很明显,id 属性是代理业务密钥,使用它不需要进一步的努力或配置。
  • 缺点:它与 Neo4js 内部数据库 ID 相关联,这在我们的应用程序实体中并不是仅在数据库生命周期内唯一的。
  • 缺点:创建不可变实体需要更多精力

8.2.2. 使用外部提供的代理键

注释可以采用类实现作为参数。 SDN提供(默认)和开箱即用。 后者为每个实体生成新的 UUID 并将它们返回为。 使用它的应用程序实体如下所示:​​@GeneratedValue​​​​org.springframework.data.neo4j.core.schema.IdGenerator​​​​InternalIdGenerator​​​​UUIDStringGenerator​​​​java.lang.String​

例 7.具有外部生成的代理项键的可变电影实体

@Node("Movie")
public class MovieEntity {

@Id @GeneratedValue(UUIDStringGenerator.class)
private String id;

private String name;
}

关于优点和缺点,我们必须讨论两件独立的事情。 任务本身和 UUID 策略。 通用唯一标识符旨在出于实际目的而是唯一的。 引用*: “因此,任何人都可以创建一个UUID,并使用它来识别某些东西,几乎可以肯定的是,该标识符不会重复已经或将要创建的标识符来识别其他东西。我们的策略使用 Java 内部 UUID 机制,采用加密强大的伪随机数生成器。 在大多数情况下,这应该可以正常工作,但您的里程可能会有所不同。

这就留下了作业本身:

  • 优点:应用程序处于完全控制之中,可以生成一个唯一密钥,该密钥对于应用程序的目的来说足够唯一。 生成的值将是稳定的,以后不需要更改它。
  • 缺点:生成的策略应用于事物的应用端。 在那些日子里,大多数应用程序将部署在多个实例中,以便很好地扩展。 如果您的策略容易生成重复项,则插入将失败,因为主键的唯一性属性将被违反。 因此,虽然在此方案中不必考虑唯一的业务密钥,但必须更多地考虑要生成的内容。

您有多种选择来推出自己的 ID 生成器。 一个是实现生成器的 POJO:

例 8.朴素序列生成器

public class TestSequenceGenerator implements IdGenerator<String> {

private final AtomicInteger sequence = new AtomicInteger(0);

@Override
public String generateId(String primaryLabel, Object entity) {
return StringUtils.uncapitalize(primaryLabel) +
"-" + sequence.incrementAndGet();
}
}

另一种选择是提供额外的春豆,如下所示:

例 9.Neo4j基于客户端的ID生成器

@Component
class MyIdGenerator implements IdGenerator<String> {

private final Neo4jClient neo4jClient;

public MyIdGenerator(Neo4jClient neo4jClient) {
this.neo4jClient = neo4jClient;
}

@Override
public String generateId(String primaryLabel, Object entity) {
return neo4jClient.query("YOUR CYPHER QUERY FOR THE NEXT ID")
.fetchAs(String.class).one().get();
}
}

完全使用所需的查询或逻辑。

上面的生成器将被配置为 Bean 引用,如下所示:

例 10.使用Spring Bean作为ID生成器的可变电影实体

@Node("Movie")
public class MovieEntity {

@Id @GeneratedValue(generatorRef = "myIdGenerator")
private String id;

private String name;
}

8.2.3. 使用业务密钥

我们一直在完整示例的sand PersonEntity 中使用业务密钥。 人员的姓名在施工时由您的应用程序分配,并在通过 Spring 数据加载时分配。​​MovieEntity​

只有当您找到一个稳定、唯一的业务密钥,但会成为伟大的不可变域对象时,这才有可能。

  • 优点:使用业务键或自然键作为主键是很自然的。 有问题的实体被清楚地识别出来,并且在您的域的进一步建模中大多数时候感觉恰到好处。
  • 缺点:一旦您意识到找到的密钥并不像您想象的那么稳定,作为主密钥的业务密钥将很难更新。 通常事实证明,即使另有承诺,它也可以改变。 除此之外,很难找到对事物真正唯一的标识符。

请记住,在Spring Data Neo4j处理业务密钥之前,始终在域实体上设置业务密钥。 这意味着它无法确定实体是否是新的(它总是假设实体是新的), 除非还提供了 a@Version字段。

8.3. Spring 数据对象映射基础

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

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

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

8.3.1. 对象创建

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

  1. 如果存在无参数构造函数,则将使用它。 其他构造函数将被忽略。
  2. 如果只有一个构造函数接受参数,则将使用它。
  3. 如果有多个构造函数采用参数,则必须对Spring Data要使用的构造函数进行注释。@PersistenceCreator

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

对象创建内部

为了避免反射的开销,Spring Data 对象创建默认使用运行时生成的工厂类,该工厂类将直接调用域类构造函数。 即对于此示例类型:

class Person {
Person(String firstname, String lastname) { … }
}

我们将在运行时创建一个语义等同于此工厂类的工厂类:

class PersonObjectInstantiator implements ObjectInstantiator {

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

这为我们提供了大约 10% 的性能提升。 要使域类符合此类优化的条件,它需要遵守一组约束:

  • 它不能是私有类
  • 它不能是非静态内部类
  • 它不能是 CGLib 代理类
  • Spring Data 要使用的构造函数不得是私有的

如果这些条件中的任何一个匹配,Spring 数据将通过反射回退到实体实例化。

8.3.2. 财产人口

创建实体的实例后,Spring Data 将填充该类的所有剩余持久属性。 除非已由实体的构造函数填充(即通过其构造函数参数列表使用),否则将首先填充标识符属性以允许解析循环对象引用。 之后,将在实体实例上设置构造函数尚未填充的所有非瞬态属性。 为此,我们使用以下算法:

  1. 如果属性是不可变的,但公开了一个wither方法(见下文),我们使用wither创建一个具有新属性值的新实体实例。
  2. 如果定义了属性访问(即通过 getter 和 setter 的访问),我们将调用 setter 方法。
  3. 默认情况下,我们直接设置字段值。

属性人口内部

与对象构造中的优化类似,我们还使用 Spring 数据运行时生成的访问器类与实体实例进行交互。

class Person {

private final Long id;
private String firstname;
private @AccessType(Type.PROPERTY) String lastname;

Person() {
this.id = null;
}

Person(Long id, String firstname, String lastname) {
// Field assignments
}

Person withId(Long id) {
return new Person(id, this.firstname, this.lastame);
}

void setLastname(String lastname) {
this.lastname = lastname;
}
}

例 11.生成的属性访问器

class PersonPropertyAccessor implements PersistentPropertyAccessor {

private static final MethodHandle firstname;

private Person person;

public void setProperty(PersistentProperty property, Object value) {

String name = property.getName();

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

PropertyAccessor 保存基础对象的可变实例。 这是为了启用其他不可变属性的突变。

默认情况下,Spring 数据使用字段访问来读取和写入属性值。 根据字段的可见性规则,用于与字段进行交互。​​private​​​​MethodHandles​

该类公开用于设置标识符的方法,例如,当实例插入数据存储并生成标识符时。 调用创建一个新对象。 所有后续突变都将发生在新实例中,而之前的突变保持不变。​​withId(…)​​​​withId(…)​​​​Vertex​

使用属性访问允许直接调用方法,而无需使用。​​MethodHandles​

这为我们提供了大约 25% 的性能提升。 要使域类符合此类优化的条件,它需要遵守一组约束:

  • 类型不得驻留在默认值或包下。java
  • 类型及其构造函数必须是public
  • 内部类的类型必须是。static
  • 使用的 Java 运行时必须允许在原始文件中声明类。 Java 9 及更高版本施加了某些限制。ClassLoader

默认情况下,Spring Data 尝试使用生成的属性访问器,如果检测到限制,则回退到基于反射的属性访问器。

让我们看一下以下实体:

例 12.示例实体

class Person {

private final @Id Long id;
private final String firstname, lastname;
private final LocalDate birthday;
private final int age;

private String comment;
private @AccessType(Type.PROPERTY) String remarks;

static Person of(String firstname, String lastname, LocalDate birthday) {

return new Person(null, firstname, lastname, birthday,
Period.between(birthday, LocalDate.now()).getYears());
}

Person(Long id, String firstname, String lastname, LocalDate birthday, int age) {

this.id = id;
this.firstname = firstname;
this.lastname = lastname;
this.birthday = birthday;
this.age = age;
}

Person withId(Long id) {
return new Person(id, this.firstname, this.lastname, this.birthday);
}

void setRemarks(String remarks) {
this.remarks = remarks;
}
}

标识符属性是最终的,但在构造函数中设置为。 该类公开用于设置标识符的方法,例如,当实例插入数据存储并生成标识符时。 创建新实例时,原始实例保持不变。 相同的模式通常应用于存储管理的其他属性,但可能必须更改持久性操作。​​null​​​​withId(…)​​​​Vertex​

和属性是普通的不可变属性,可能通过 getter 公开。​​firstname​​​​lastname​

属性是不可变的,但派生自属性。 按照所示的设计,数据库值将胜过默认值,因为 Spring Data 使用唯一声明的构造函数。 即使意图是首选计算,重要的是此构造函数也采用 as 参数(可能忽略它),否则属性填充步骤将尝试设置 age 字段并失败,因为它是不可变的并且不存在枯萎。​​age​​​​birthday​​​​age​

属性是可变的,通过直接设置其字段来填充。​​comment​

属性是可变的,并通过直接设置 thefield 或通过调用 setter 方法来填充​​remarks​​​​comment​

该类公开用于创建对象的工厂方法和构造函数。 这里的核心思想是使用工厂方法而不是其他构造函数,以避免通过构造函数消除歧义的需要。 相反,属性的默认值在工厂方法中处理。​​@PersistenceCreator​

8.3.3. 一般建议

  • 尝试坚持使用不可变对象 - 不可变对象很容易创建,因为具体化对象只需调用其构造函数即可。 此外,这可以防止域对象充斥着允许客户端代码操作对象状态的 setter 方法。 如果需要这些,最好使它们受到包保护,以便只能由有限数量的共存类型调用它们。 仅构造函数具体化比属性填充快 30%。
  • 提供全参数构造函数 — 即使不能或不想将实体建模为不可变值,提供将实体的所有属性(包括可变属性)作为参数的构造函数仍然有价值,因为这允许对象映射跳过属性填充以获得最佳性能。
  • 使用工厂方法而不是重载的构造函数来避免​​@PersistenceCreator​​ — 使用最佳性能所需的全参数构造函数,我们通常希望公开更多特定于应用程序用例的构造函数,这些构造函数省略了自动生成的标识符等内容。 这是一种既定模式,而是使用静态工厂方法来公开 all-args 构造函数的这些变体。
  • 确保遵守允许使用生成的实例化器和属性访问器类的约束
  • 对于要生成的标识符,仍将最终字段与 wither 方法结合使用
  • 使用 Lombok 避免样板代码 — 由于持久性操作通常需要构造函数获取所有参数,因此它们的声明变成了对字段分配的繁琐重复,而使用 Lombok 可以最好地避免这些参数。@AllArgsConstructor
关于不可变映射的说明

尽管我们建议尽可能使用不可变映射和构造,但在映射方面存在一些限制。 给定一个双向关系,其中有一个构造函数引用和一个引用,或者一个更复杂的场景。 这种先有蛋的情况对于Spring Data Neo4j来说是无法解决的。 在实例化期间,它急切地需要一个完全实例化,另一方面,它需要一个实例(准确地说,是相同的实例)。 SDN通常允许这样的模型,但如果从数据库返回的数据包含如上所述的星座,则会抛出aat运行时。 在这种情况下,您无法预见返回的数据是什么样子,因此更适合为关系使用可变字段。​​A​​​​B​​​​B​​​​A​​​​A​​​​B​​​​A​​​​MappingException​

8.3.4. Kotlin 支持

Spring Data 调整了 Kotlin 的细节,以允许对象创建和更改。

Kotlin 对象创建

Kotlin 类支持实例化,默认情况下所有类都是不可变的,并且需要显式属性声明来定义可变属性。 请考虑以下类:​​data​​​​Vertex​

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 检测到参数默认值的构造函数时,如果数据存储不提供值(或只是返回),则这些参数将保留为不存在,以便 Kotlin 可以应用参数默认值。 请考虑以下应用参数默认值的类​​null​​​​name​

data class Person(var id: String, val name: String = "unknown")

每次参数不是结果的一部分或其值是时,则默认为。​​name​​​​null​​​​name​​​​unknown​

Kotlin 数据类的属性填充

在 Kotlin 中,默认情况下所有类都是不可变的,并且需要显式属性声明来定义可变属性。 请考虑以下类:​​data​​​​Vertex​

data class Person(val id: String, val name: String)

此类实际上是不可变的。 它允许在 Kotlin 生成方法时创建新实例,该方法创建新的对象实例,从现有对象复制所有属性值,并将作为参数提供的属性值应用于该方法。​​copy(…)​

Spring Data(数据)Neo4j

9. 使用 Spring 数据存储库

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


Spring 数据存储库文档和您的模块



本章解释了 Spring 数据存储库的核心概念和接口。 本章中的信息来自 Spring 数据共享模块。 它使用 Jakarta 持久性 API (JPA) 模块的配置和代码示例。 如果要使用 XML 配置,则应调整 XML 命名空间声明和要扩展的类型,以使用的特定模块的等效项。“[repository.namespace-reference]​”涵盖了XML配置,所有支持存储库API的Spring Data模块都支持XML配置。 “附录 B”涵盖了存储库抽象通常支持的查询方法关键字。 有关模块特定功能的详细信息,请参阅本文档有关该模块的章节。


9.1. 核心概念

Spring 数据存储库抽象中的中心接口是。 它采用要管理的域类以及域类的 ID 类型作为类型参数。 此接口主要充当标记接口,用于捕获要使用的类型,并帮助您发现扩展此接口的接口。 CrudRepository 和ListCrudRepository接口为正在管理的实体类提供了复杂的 CRUD 功能。​​Repository​

例 13.界面​​CrudRepository​

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

<S extends T> S save(S entity);

Optional<T> findById(ID primaryKey);

Iterable<T> findAll();

long count();

void delete(T entity);

boolean existsById(ID primaryKey);

// … more functionality omitted.
}

保存给定的实体。

返回由给定 ID 标识的实体。

返回所有实体。

返回实体数。

删除给定实体。

指示具有给定 ID 的实体是否存在。

​ListCrudRepository​​提供等效的方法,但它们返回方法返回 an。​​List​​​​CrudRepository​​​​Iterable​

我们还提供特定于持久性技术的抽象,例如 asor。 这些接口扩展并公开了底层持久性技术的功能,以及相当通用的持久性技术无关的接口,例如。​​JpaRepository​​​​MongoRepository​​​​CrudRepository​​​​CrudRepository​

除此之外,还有一个PagingAndSortingRepository抽象,它添加了其他方法来简化对实体的分页访问:​​CrudRepository​

例 14.接口​​PagingAndSortingRepository​

public interface PagingAndSortingRepository<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));

除了查询方法之外,还可以对计数查询和删除查询进行查询派生。 以下列表显示了派生计数查询的接口定义:

例 15。派生计数查询

interface UserRepository extends CrudRepository<User, Long> {

long countByLastname(String lastname);
}

以下清单显示了派生删除查询的接口定义:

例 16。派生删除查询

interface UserRepository extends CrudRepository<User, Long> {

long deleteByLastname(String lastname);

List<User> removeByLastname(String lastname);
}

9.2. 查询方法

标准 CRUD 功能存储库通常对基础数据存储具有查询。 使用 Spring Data,声明这些查询变成了一个四步过程:

  1. 声明扩展存储库或其子接口之一的接口,并将其键入应处理的域类和 ID 类型,如以下示例所示:
interface PersonRepository extends Repository<Person, Long> { … }
  1. 在接口上声明查询方法。
interface PersonRepository extends Repository<Person, Long> {
List<Person> findByLastname(String lastname);
}
  1. 设置 Spring 以使用JavaConfig或XML 配置为这些接口创建代理实例。

    清单 16.爪哇岛
    清单 17..XML
@EnableJpaRepositories
class Config { … }

此示例中使用 JPA 命名空间。 如果将存储库抽象用于任何其他存储,则需要将其更改为存储模块的相应命名空间声明。 换句话说,您应该交换支持,例如,。jpamongodb

请注意,JavaConfig 变体不会显式配置包,因为缺省情况下使用带注释的类的包。 要自定义要扫描的包,请使用特定于数据存储的存储库注释的属性之一。basePackage…@EnableJpaRepositories

  1. 注入存储库实例并使用它,如以下示例所示:

class SomeClient {

private final PersonRepository repository;

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

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

9.3. 定义存储库接口

要定义存储库接口,首先需要定义特定于域类的存储库接口。 接口必须扩展并键入域类和 ID 类型。 如果要公开该域类型的 CRUD 方法,可以扩展或其变体之一,而不是。​​Repository​​​​CrudRepository​​​​Repository​

9.3.1. 微调存储库定义

您可以通过几种变体开始使用存储库界面。

典型的方法是扩展,这为您提供了 CRUD 功能的方法。 CRUD 代表 创建、读取、更新、删除。 在 3.0 版中,我们还引入了这与 但是对于那些返回多个实体的方法,它返回的是您可能发现更容易使用的方法。​​CrudRepository​​​​ListCrudRepository​​​​CrudRepository​​​​List​​​​Iterable​

如果您使用的是反应式存储,则可以选择,或者取决于您使用的反应式框架。​​ReactiveCrudRepository​​​​RxJava3CrudRepository​

如果你正在使用 Kotlin,你可以选择哪个利用 Kotlin 的协程。​​CoroutineCrudRepository​

此外,您还可以扩展,,,或者如果您需要允许指定抽象或在第一种情况下指定抽象的方法。 请注意,各种排序存储库不再像在 Spring 数据版本 3.0 之前那样扩展其各自的 CRUD 存储库。 因此,如果需要这两个接口的功能,则需要扩展这两个接口。​​PagingAndSortingRepository​​​​ReactiveSortingRepository​​​​RxJava3SortingRepository​​​​CoroutineSortingRepository​​​​Sort​​​​Pageable​

如果您不想扩展 Spring 数据接口,您还可以使用注释存储库接口。 扩展其中一个 CRUD 存储库接口会公开一组完整的方法来操作实体。 如果您希望对要公开的方法有选择性,请将要公开的方法从 CRUD 存储库复制到域存储库中。 执行此操作时,可以更改方法的返回类型。 如果可能,Spring 数据将遵循返回类型。 例如,对于返回多个实体的方法,您可以选择 VAVR 列表。​​@RepositoryDefinition​​​​Iterable<T>​​​​List<T>​​​​Collection<T>​

如果应用程序中的许多存储库应具有相同的方法集,则可以定义自己的基本接口进行继承。 必须对这样的接口进行注释。 这可以防止 Spring Data 尝试直接创建它的实例并失败,因为它无法确定该存储库的实体,因为它仍然包含一个通用类型变量。​​@NoRepositoryBean​

下面的示例演示如何有选择地公开 CRUD 方法(在本例中为):​​findById​​​​save​

例 17.有选择地公开 CRUD 方法

@NoRepositoryBean
interface MyBaseRepository<T, ID> extends Repository<T, ID> {

Optional<T> findById(ID id);

<S extends T> S save(S entity);
}

interface UserRepository extends MyBaseRepository<User, Long> {
User findByEmailAddress(EmailAddress emailAddress);
}

在前面的示例中,您为所有域存储库和公开以及定义了通用基本接口。这些方法被路由到 Spring Data 提供的您选择的存储的基本存储库实现中(例如,如果您使用 JPA,则实现是),因为它们与方法签名匹配。 因此,现在可以保存用户,按ID查找单个用户,并触发查询以按电子邮件地址查找。​​findById(…)​​​​save(…)​​​​SimpleJpaRepository​​​​CrudRepository​​​​UserRepository​​​​Users​

中间存储库接口带有注释。 确保将该注释添加到 Spring Data 在运行时不应为其创建实例的所有存储库接口。​​@NoRepositoryBean​

9.3.2. 使用具有多个 Spring 数据模块的存储库

在应用程序中使用唯一的 Spring 数据模块使事情变得简单,因为定义范围内的所有存储库接口都绑定到 Spring 数据模块。 有时,应用程序需要使用多个 Spring 数据模块。 在这种情况下,存储库定义必须区分持久性技术。 当它在类路径上检测到多个存储库工厂时,Spring Data 进入严格的存储库配置模式。 严格配置使用存储库或域类的详细信息来决定存储库定义的 Spring 数据模块绑定:

  1. 如果存储库定义扩展了特定于模块的存储库,则它是特定 Spring 数据模块的有效候选者。
  2. 如果域类使用特定于模块的类型注释进行注释,则它是特定 Spring 数据模块的有效候选者。 Spring Data 模块接受第三方注释(例如 JPA)或提供自己的注释(例如 Spring Data MongoDB 和 Spring Data Elasticsearch)。@Entity@Document

以下示例显示了使用特定于模块的接口(在本例中为 JPA)的存储库:

例 18。使用特定于模块的接口的存储库定义

interface MyRepository extends JpaRepository<User, Long> { }

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

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

​MyRepository​​并扩展其类型层次结构。 它们是 Spring Data JPA 模块的有效候选者。​​UserRepository​​​​JpaRepository​

以下示例显示了使用通用接口的存储库:

例 19。使用通用接口的存储库定义

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

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

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

​AmbiguousRepository​​并仅在其类型层次结构中扩展。 虽然这在使用唯一的 Spring 数据模块时很好,但多个模块无法区分这些存储库应该绑定到哪个特定的 Spring 数据。​​AmbiguousUserRepository​​​​Repository​​​​CrudRepository​

以下示例显示了一个使用带有注释的域类的存储库:

例 20。使用带有注释的域类的存储库定义

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

@Entity
class Person { … }

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

@Document
class User { … }

​PersonRepository​​引用,它用JPAannotation注释,所以这个存储库显然属于Spring Data JPA.references,它用Spring Data MongoDB的sannotation注释。​​Person​​​​@Entity​​​​UserRepository​​​​User​​​​@Document​

以下错误示例显示了一个使用具有混合注释的域类的存储库:

例 21。使用具有混合注释的域类的存储库定义

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

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

@Entity
@Document
class Person { … }

这个例子展示了一个同时使用JPA和Spring Data MongoDB注释的域类。 它定义了两个存储库,并且。 一个用于JPA,另一个用于MongoDB使用。 Spring 数据不再能够区分存储库,这会导致未定义的行为。​​JpaPersonRepository​​​​MongoDBPersonRepository​

存储库类型详细信息和区分域类注释用于严格的存储库配置,以识别特定 Spring 数据模块的存储库候选者。 可以对同一域类型使用多个特定于持久性技术的注释,并允许跨多个持久性技术重用域类型。 但是,Spring Data 无法再确定绑定存储库的唯一模块。

区分存储库的最后一种方法是确定存储库基础包的范围。 基本包定义扫描存储库接口定义的起点,这意味着存储库定义位于相应的包中。 默认情况下,注释驱动的配置使用配置类的包。 基于 XML 的配置中的基本包是必需的。

以下示例显示了基本包的注释驱动配置:

例 22。注释驱动的基本包配置

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

9.4. 定义查询方法

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

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

可用选项取决于实际商店。 但是,必须有一个策略来决定创建什么实际查询。 下一节介绍可用选项。

9.4.1. 查询查找策略

存储库基础结构可以使用以下策略来解析查询。 使用 XML 配置,您可以通过属性在命名空间中配置策略。 对于 Java 配置,您可以使用注释的属性。 特定数据存储可能不支持某些策略。​​query-lookup-strategy​​​​queryLookupStrategy​​​​EnableJpaRepositories​

  • ​CREATE​​尝试从查询方法名称构造特定于存储的查询。 一般方法是从方法名称中删除一组给定的已知前缀,并分析方法的其余部分。 您可以在“第 9.4.2 节”中阅读有关查询构造的更多信息。
  • ​USE_DECLARED_QUERY​​尝试查找已声明的查询,如果找不到查询,则会引发异常。 查询可以通过某处的注释定义,也可以通过其他方式声明。 请参阅特定商店的文档以查找该商店的可用选项。 如果存储库基础结构在引导时找不到该方法的声明查询,则会失败。
  • ​CREATE_IF_NOT_FOUND​​(默认值)组合沙。 它首先查找已声明的查询,如果未找到已声明的查询,则会创建一个基于名称的自定义方法查询。 这是默认的查找策略,因此,如果未显式配置任何内容,则使用此方法。 它允许按方法名称快速定义查询,但也允许根据需要引入声明的查询来自定义调整这些查询。CREATEUSE_DECLARED_QUERY

9.4.2. 查询创建

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

下面的示例演示如何创建多个查询:

例 23。从方法名称创建查询

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);
}

分析查询方法名称分为主语和谓语。 第一部分 (,) 定义查询的主题,第二部分构成谓词。 引言子句(主语)可以包含进一步的表达式。 (或其他引入关键字)之间的任何文本都被视为描述性的,除非使用结果限制关键字之一,例如 ato 在要创建的查询上设置不同的标志或Top/First以限制查询结果。find…Byexists…ByfindByDistinct

附录包含查询方法主题关键字和查询方法谓词关键字的完整列表,包括排序和字母大小写修饰符。 但是,第一个充当分隔符来指示实际条件谓词的开始。 在非常基本的级别上,您可以定义实体属性的条件并将它们与 and 连接起来。​​By​​​​And​​​​Or​

分析方法的实际结果取决于为其创建查询的暂留存储。 但是,有一些一般事项需要注意:

  • 表达式通常是属性遍历与可以连接的运算符相结合。 可以将属性表达式与 and 组合在一起。 您还可以获得对运算符的支持,例如,,, 和属性表达式。 支持的运算符可能因数据存储而异,因此请参阅参考文档的相应部分。ANDORBetweenLessThanGreaterThanLike
  • 方法解析器支持为单个属性(例如)或支持忽略大小写的类型的所有属性(通常是实例 — 例如,)设置 anflag。 是否支持忽略案例可能因商店而异,因此请参阅特定于商店的查询方法的参考文档中的相关部分。IgnoreCasefindByLastnameIgnoreCase(…)StringfindByLastnameAndFirstnameAllIgnoreCase(…)
  • 可以通过将 anclause 追加到引用属性的查询方法并提供排序方向 (or) 来应用静态排序。 要创建支持动态排序的查询方法,请参阅“第 9.4.4 节”。OrderByAscDesc

9.4.3. 属性表达式

属性表达式只能引用托管实体的直接属性,如前面的示例所示。 在创建查询时,已确保分析的属性是托管域类的属性。 但是,也可以通过遍历嵌套属性来定义约束。 请考虑以下方法签名:

List<Person> findByAddressZipCode(ZipCode zipCode);

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

尽管这应该适用于大多数情况,但算法可能会选择错误的属性。 假设该类也有属性。 算法将在第一轮拆分中匹配,选择错误的属性,然后失败(因为类型可能没有属性)。​​Person​​​​addressZip​​​​addressZip​​​​code​

要解决这种歧义,您可以在方法名称中使用手动定义遍历点。 所以我们的方法名称如下:​​_​

List<Person> findByAddress_ZipCode(ZipCode zipCode);

由于我们将下划线字符视为保留字符,因此强烈建议遵循标准的 Java 命名约定(即,不要在属性名称中使用下划线,而是使用驼峰大小写)。

9.4.4. 特殊参数处理

若要处理查询中的参数,请定义方法参数,如前面的示例所示。 除此之外,基础架构还可以识别某些特定类型,例如and,以动态地将分页和排序应用于您的查询。 以下示例演示了这些功能:​​Pageable​​​​Sort​

例 24。使用 、 和 in 查询方法​​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()​​​​Pageable.unpaged()​

第一种方法允许您将实例传递给查询方法,以动态地将分页添加到静态定义的查询中。 了解可用元素和页面的总数。 它通过基础结构触发计数查询来计算总数。 由于这可能很昂贵(取决于所使用的商店),因此您可以改为返回 a。 仅知道 next是否可用,这在遍历较大的结果集时可能就足够了。​​org.springframework.data.domain.Pageable​​​​Page​​​​Slice​​​​Slice​​​​Slice​

排序选项也通过实例处理。 如果只需要排序,请向方法添加参数。 如您所见,返回 ais 也是可能的。 在这种情况下,不会创建构建实际实例所需的其他元数据(这反过来意味着不会发出所需的其他计数查询)。 相反,它将查询限制为仅查找给定范围的实体。​​Pageable​​​​org.springframework.data.domain.Sort​​​​List​​​​Page​

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

分页和排序

可以使用属性名称定义简单的排序表达式。 您可以连接表达式以将多个条件收集到一个表达式中。

例 25。定义排序表达式

Sort sort = Sort.by("firstname").ascending()
.and(Sort.by("lastname").descending());

有关定义排序表达式的更类型安全的方法,请从定义排序表达式的类型开始,并使用方法引用定义要排序的属性。

例 26。使用类型安全的 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 本机等工具时可能会干扰本机映像编译。

如果您的商店实现支持 Querydsl,您还可以使用生成的元模型类型来定义排序表达式:

例 27。使用 Querydsl API 定义排序表达式

QSort sort = QSort.by(QPerson.firstname.asc())
.and(QSort.by(QPerson.lastname.desc()));

9.4.5. 限制查询结果

可以使用 theor关键字来限制查询方法的结果,这些关键字可以互换使用。 您可以附加一个可选的数值来指定要返回的最大结果大小。 如果省略该数字,则假定结果大小为 1。 以下示例演示如何限制查询大小:​​first​​​​top​​​​top​​​​first​

例 28。限制查询的结果大小​​Top​​​​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);

限制表达式还支持支持不同查询的数据存储的关键字。 此外,对于将结果集限制为一个实例的查询,支持使用 thekeyword 将结果包装到其中。​​Distinct​​​​Optional​

如果分页或切片应用于限制查询分页(以及可用页数的计算),则会在有限的结果中应用分页或切片。

通过使用参数限制结果和动态排序,可以表示“K”最小元素和“K”最大元素的查询方法。​​Sort​

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

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

使用可流式处理作为查询方法返回类型

您可以将其用作任何集合类型的替代方法。 它提供了访问非并行(缺少)的便捷方法,以及直接覆盖元素并将元素连接到其他元素的能力:​​Streamable​​​​Iterable​​​​Stream​​​​Iterable​​​​….filter(…)​​​​….map(…)​​​​Streamable​

例 29。使用可流式处理合并查询方法结果

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"));
返回自定义可流式传输包装器类型

为集合提供专用包装器类型是一种常用模式,用于为返回多个元素的查询结果提供 API。 通常,通过调用返回类似集合类型的存储库方法并手动创建包装器类型的实例来使用这些类型。 您可以避免该额外步骤,因为如果满足以下条件,Spring Data 允许您将这些包装器类型用作查询方法返回类型:

  1. 类型实现。Streamable
  2. 该类型公开构造函数或名为 dorthat 的静态工厂方法作为参数。of(…)valueOf(…)Streamable

下面的清单显示了一个示例:

class Product {                                         
MonetaryAmount getPrice() { … }
}

@RequiredArgsConstructor(staticName = "of")
class Products implements Streamable<Product> {

private final Streamable<Product> streamable;

public MonetaryAmount getTotal() {
return streamable.stream()
.map(Priced::getPrice)
.reduce(Money.of(0), MonetaryAmount::add);
}


@Override
public Iterator<Product> iterator() {
return streamable.iterator();
}
}

interface ProductRepository implements Repository<Product, Long> {
Products findAllByDescriptionContaining(String text);
}

公开 API 以访问产品价格的实体。​​Product​

可以使用(使用龙目岛注释创建的工厂方法)构造的包装器类型。 一个标准的构造函数也这样做。​​Streamable<Product>​​​​Products.of(…)​​​​Streamable<Product>​

包装器类型公开一个额外的 API,计算新值。​​Streamable<Product>​

实现接口并委托给实际结果。​​Streamable​

该包装器类型可以直接用作查询方法返回类型。 您无需在存储库客户端中查询后返回并手动包装它。​​Products​​​​Streamable<Product>​

支持 Vavr 集合

Vavr是一个包含Java函数式编程概念的库。 它附带一组可用作查询方法返回类型的自定义集合类型,如下表所示:

Vavr 采集类型

使用的 Vavr 实现类型

有效的 Java 源类型

​io.vavr.collection.Seq​

​io.vavr.collection.List​

​java.util.Iterable​

​io.vavr.collection.Set​

​io.vavr.collection.LinkedHashSet​

​java.util.Iterable​

​io.vavr.collection.Map​

​io.vavr.collection.LinkedHashMap​

​java.util.Map​

您可以使用第一列中的类型(或其子类型)作为查询方法返回类型,并获取第二列中的类型用作实现类型,具体取决于实际查询结果(第三列)的 Java 类型。 或者,您可以声明(Vavr等效),然后我们从实际返回值中派生实现类。 也就是说,ais 变成了 Vavror,变成了 Vavr,依此类推。​​Traversable​​​​Iterable​​​​java.util.List​​​​List​​​​Seq​​​​java.util.Set​​​​LinkedHashSet​​​​Set​

9.4.7. 存储库方法的空处理

从Spring Data 2.0开始,返回单个聚合实例的存储库CRUD方法使用Java 8来指示可能缺少值。 除此之外,Spring Data 还支持在查询方法上返回以下包装器类型:​​Optional​

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

或者,查询方法可以选择根本不使用包装器类型。 然后通过返回来指示缺少查询结果。 返回集合、集合替代项、包装器和流的存储库方法保证永远不会返回,而是返回相应的空表示形式。 详见“附录C”。​​null​​​​null​

可为空性注释

您可以使用 Spring Framework 的可空性注释来表达存储库方法的可空性约束。 它们提供了一种工具友好的方法,并在运行时进行选择加入,如下所示:​​null​

  • @NonNullApi:在包级别用于声明参数和返回值的默认行为分别是既不接受也不生成值。null
  • @NonNull:用于不得使用的参数或返回值(在适用的参数和返回值上不需要)。null@NonNullApi
  • @Nullable:用于可以的参数或返回值。null

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

例 30。声明 中的非可空性​​package-info.java​

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

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

以下示例显示了刚才描述的许多技术:

例 31。使用不同的可为空性约束

package com.acme;                                                       

interface UserRepository extends Repository<User, Long> {

User getByEmailAddress(EmailAddress emailAddress);

@Nullable
User findByEmailAddress(@Nullable EmailAddress emailAdress);

Optional<User> findOptionalByEmailAddress(EmailAddress emailAddress);
}

存储库驻留在我们为其定义了非空行为的包(或子包)中。

当查询未生成结果时引发。 扔一个当手到方法就是。​​EmptyResultDataAccessException​​​​IllegalArgumentException​​​​emailAddress​​​​null​

当查询未生成结果时返回。 也接受作为值。​​null​​​​null​​​​emailAddress​

当查询未生成结果时返回。 扔一个当手到方法就是。​​Optional.empty()​​​​IllegalArgumentException​​​​emailAddress​​​​null​

基于 Kotlin 的存储库中的可空性

Kotlin 将可空性约束的定义融入到语言中。 Kotlin 代码编译为字节码,字节码不通过方法签名表示可空性约束,而是通过编译的元数据来表达可空性约束。 确保在您的项目中包含 JAR,以便能够内省 Kotlin 的可空性约束。 Spring 数据存储库使用语言机制来定义这些约束以应用相同的运行时检查,如下所示:​​kotlin-reflect​

例 32。在 Kotlin 存储库上使用可空性约束

interface UserRepository : Repository<User, String> {

fun findByUsername(username: String): User

fun findByFirstname(firstname: String?): User?
}

该方法将参数和结果定义为不可为空(Kotlin 默认值)。 Kotlin 编译器拒绝传递给该方法的方法调用。 如果查询产生空结果,则引发 anis 。​​null​​​​EmptyResultDataAccessException​

此方法接受参数并返回查询未产生结果。​​null​​​​firstname​​​​null​

9.4.8. 流式查询结果

您可以使用 Java 8 作为返回类型以增量方式处理查询方法的结果。 不是将查询结果包装在 中,而是使用特定于数据存储的方法执行流式处理,如以下示例所示:​​Stream<T>​​​​Stream​

例 33。使用 Java 8 流式传输查询结果​​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);

可能会包装特定于基础数据存储的资源,因此必须在使用后关闭。 您可以使用该方法或使用 Java 7block 手动关闭,如以下示例所示:​​Stream​​​​Stream​​​​close()​​​​try-with-resources​

例 34。在块中使用结果​​Stream<T>​​​​try-with-resources​

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

并非所有 Spring 数据模块当前都支持返回类型。​​Stream<T>​

9.4.9. 异步查询结果

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

@Async
Future<User> findByFirstname(String firstname);

@Async
CompletableFuture<User> findOneByFirstname(String firstname);

用作返回类型。​​java.util.concurrent.Future​

使用 Java 8 作为返回类型。​​java.util.concurrent.CompletableFuture​