Spring Data Neo4j

对于使用 Spring Framework 或 Spring Boot 并希望利用反应式开发原则的 Java 开发人员,本指南通过 Spring Data Neo4j 项目介绍了 Spring 集成。该库提供了对 Neo4j 的便捷访问,包括对象映射、Spring Data 存储库、转换、事务处理、反应式支持等。

反应式开发

Neo4j(4.0+ 版)通过驱动程序将反应式宣言的原则融入到数据库和客户端之间的数据传输中。开发人员可以利用反应式方法处理查询并返回结果。这意味着驱动程序和数据库之间的通信可以根据客户端的数据需求动态管理和调整。

反应式编程原则允许消费方(应用程序和其他系统)指定在特定时间窗口内接收的数据量。Neo4j 的数据库驱动程序还将维护从服务器请求数据的速率限制,从而在整个 Neo4j 堆栈中提供流量控制。

无论事务或数据量有多大(即使在活动频繁时),系统都可以根据可用资源限制一次发送和接收的数量。这可以防止过载、崩溃或故障,以及在停机期间丢失传输或稍后追赶负载。

Project Reactor是许多反应式开发实现的核心基础,包括Spring 的实现。Neo4j 使用 Project Reactor 组件的 Spring 实现,在相关应用程序中提供对图数据库的反应式支持。

Spring Data Neo4j

Spring Data Neo4j 6 是 Spring Data Neo4j 项目的新主要版本。它的功能优势之一是支持反应式事务,尽管还有其他改进和增加,例如完全不可变实体和基于Java record的映射支持。

虽然 SDN 提供命令式和反应式应用程序开发,但本指南将重点介绍反应式实现。SDN 中命令式应用程序代码和文档可在Github 项目中找到。

我们可以在下面看到 SDN 库中一些最突出的功能和更改。

功能

  • 支持命令式和反应式应用程序开发

  • 通过内置 OGM(对象图映射)库实现轻量级映射

  • 不可变实体(适用于 Java 和 Kotlin 语言)

  • 新的 Neo4j 客户端和反应式客户端功能,用于模板-驱动架构

SDN 完全支持众所周知的命令式编程模型(与 Spring Data JDBC 或 JPA 非常相似)。它还提供对基于Reactive Streams的新型反应式编程的全面支持,包括反应式事务。两种功能都包含在同一二进制文件中。

反应式编程模型要求 Neo4j 实例版本为 4.0+(早期版本不支持反应式驱动程序),并且应用程序端需要反应式 Spring。

SDN 6 与 Spring Data Neo4j 之前版本的一个关键区别是 OGM(对象图映射)层不再是一个单独的库。相反,Spring Data 基础设施现在处理 OGM 的功能。

入门

在接下来的几节中,我们将逐步完成创建反应式应用程序的所有步骤。

准备数据库

在这个例子中,我们将使用 Neo4j 标准的电影图数据集,因为它随每个 Neo4j 实例免费提供,并且体积小巧。

如果您还没有,请下载 Neo4j Desktop创建/启动数据库

您可以使用 URL https://:7474 在网络浏览器中与数据库交互并加载数据。注意提示中已准备好运行的命令 (:play movies)。执行该命令,一个交互式幻灯片演示将出现在命令行下方。在该指南的第二张幻灯片上,执行冗长的 Cypher 语句以用我们的电影测试数据填充您的数据库。

创建一个新的 Spring Boot 项目

设置 Spring Boot 项目最简单的方法是使用 Spring Initializr(网址为start.spring.io)。它也集成在主要的 IDE 中,如果您不喜欢使用网站,也可以选择使用 IDE。

然后,您可以更改项目的默认组、构件、名称和描述。接下来,我们可以选择项目依赖项。我们可以搜索并添加 Neo4jSpring Reactive Web 启动器,以获取创建反应式、基于 Spring 的 Web 应用程序所需的一切。

完成这些步骤后,我们可以点击底部的 Generate 按钮来创建项目的骨架并下载它。Spring Initializr 将负责为您创建项目结构,其中包含所选构建工具的基本文件和设置。

其他依赖项

如果您在 Github 上查看项目,您可能会注意到 pom.xml 中还有其他一些依赖项。其中两个用于向项目添加测试,一个用于开发人员工具,另外两个用于测试容器。

有关测试功能的更多信息,请参阅文档

测试和开发工具依赖项
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>io.projectreactor</groupId>
    <artifactId>reactor-test</artifactId>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.testcontainers</groupId>
    <artifactId>junit-jupiter</artifactId>
    <version>1.17.6</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.testcontainers</groupId>
    <artifactId>neo4j</artifactId>
    <version>1.17.6</version>
    <scope>test</scope>
</dependency>

添加配置

现在,我们需要添加一些配置以连接到数据库。我们可以找到 application.properties 文件并配置所需内容。

spring.neo4j.uri=neo4j+s://abcd.databases.neo4j.io
spring.neo4j.authentication.username=neo4j
spring.neo4j.authentication.password=secret

您需要将密码调整为您创建 Neo4j 实例时设置的密码。

前三行是我们的 Neo4j 数据库 URI 和凭据。您在此处输入的用户名和密码应与您的个人数据库匹配。这是连接到 Neo4j 实例所需的最基本配置。

由于 SDN 6 开箱即用提供了 Spring Boot 驱动程序自动配置,我们无需为驱动程序添加任何其他配置。

其他配置

日志

还有一个我们可以定义的额外属性。这不是必需属性,但它允许我们查看 Cypher 语句并更好地了解应用程序的运行情况。

logging.level.org.springframework.data.neo4j=DEBUG

数据库选择

自 4.0 版本起,Neo4j 是多租户。我们可以通过提供一个属性来静态选择数据库

spring.data.neo4j.database = my-database

对于更高级的用例,可以执行动态选择,如此处所记录。

创建领域

定义了项目依赖项并设置了配置后,我们就可以开始为数据域定义实体了!领域层应完成两件事

  1. 将图映射到对象。

  2. 提供对这些对象的访问。

我们的数据包含电影和人物实体,这些实体展示了人物如何参与各种电影,例如谁出演、导演、编剧、制作等。我们需要为我们的每个实体(MoviePerson)定义一个领域类。

SDN 支持 Neo4j Java 驱动程序支持的所有数据类型。要了解如何将 Neo4j 类型映射到原生语言类型,请参阅文档中此部分

电影实体

@Node("Movie")
public class MovieEntity {
	@Id
	private final String title;
	@Property("tagline")
	private final String description;
	@Relationship(type = "ACTED_IN", direction = INCOMING)
	private Set<PersonEntity> actors = new HashSet<>();
	@Relationship(type = "DIRECTED", direction = INCOMING)
	private Set<PersonEntity> directors = new HashSet<>();
	public MovieEntity(String title, String description) {
		this.title = title;
		this.description = description;
	}
	//Getters omitted for brevity
}

在第一行中,@Node 注解用于将类标记为受管实体。它还配置了 Neo4j 标签,默认为类的名称,但您也可以定义自定义标签。

类定义中的前几行将实体的 ID 字段设置为 title 属性。在此领域中,标题是一个唯一的业务键,但如果其他领域中没有唯一键,您可以使用字段上的 @Id@GeneratedValue 注解组合来生成唯一的技​​术键。还提供了用于 UUID 的生成器。

下面的两行设置了 tagline(或 description)属性。@Property 注解用于将字段名称与图属性名称进行不同的映射。这样,您可以映射应用程序实体和数据库域之间的差异。

在下一个注解中,@Relationship 定义了电影和人物实体之间具有 ACTED_IN 类型的关系,以显示哪些人出演了特定电影。下面的两行定义了 MovieEntityPersonEntity 之间的另一个关系,用于那些导演电影的人。

然后,下一个代码块定义了一个实体的构造函数,其中包含节点的属性(titledescription)。

如上所述,您可以将 SDN 与 Kotlin 一起使用,并使用 Kotlin 的数据类来建模您的领域。Project Lombok 也可用于快捷定义和样板代码,如果您希望或需要纯粹使用 Java。

人物实体

@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;
	}
    //Getters omitted
}

这个人物实体类与我们上面的 MovieEntity 类非常相似。@Node 注解定义了它是一个数据库域实体。识别了一个唯一的键字段(在本例中为 name 属性),并且 born 属性被定义为该类的另一个属性。类的构造函数遵循属性。

请注意,我们尚未定义人物到电影的关系。在我们的用例中,我们只想检索电影以及其中涉及的人物。我们的应用程序不需要我们单独拉取人物实体的信息,因此我们不需要定义反向关系。

如果一个领域需要同时拉取两边的相关实体,我们就需要从两边添加注解和属性。

定义 Spring Data 存储库

应用程序中的存储库将扩展一个开箱即用的存储库,名为 ReactiveNeo4jRepository

如果构建一个命令式应用程序,您可以扩展 Neo4jRepository。此外,虽然技术上不禁止,但强烈不建议也不支持在同一个应用程序中混合使用命令式和反应式数据库访问。

因为我们的存储库正在实现反应式功能,所以我们可以访问来自Project ReactorMonoFlux反应式类型作为方法返回。Mono 类型返回 0 或 1 个结果,而 Flux 返回 0 或 n 个结果。如果我们期望从查询中返回单个对象,则使用 Mono 返回类型;如果期望从查询中返回多个对象,则使用 Flux 类型。

电影存储库

public interface MovieRepository extends ReactiveNeo4jRepository<MovieEntity, String> {
	Mono<MovieEntity> findOneByTitle(String title);
}

对于我们的应用程序,我们需要与 Neo4j 图数据库交互,因此我们将创建一个扩展 Neo4j 存储库的接口。

由于我们希望为应用程序使用反应式特性,我们将扩展 ReactiveNeo4jRepository,它在几个扩展的 Spring 存储库之上提供了反应式、Neo4j 特定的实现细节。ReactiveNeo4jRepository 需要指定两种类型——我们的类类型及其 ID 类型。一旦我们在此处添加了 MovieEntityString(我们的电影 ID 字段是 title)值,我们就可以开始定义我们想要使用的方法。

在接口定义中,我们将定义一个 findOneByTitle() 方法。此方法将允许我们根据电影标题搜索数据库,并且我们期望返回我们感兴趣的电影的单个电影或不返回任何电影。

为了获得 0 或 1 个返回结果,我们可以使用 Mono<MovieEntity> 的反应式返回类型。我们还将向方法传递一个标题(字符串),因为我们希望允许用户输入任何电影标题作为搜索值。

人物存储库

尽管 Github 代码中有一个 PersonRepository 接口,但它仅用于该应用程序的测试目的,因此我们在此不详细介绍。有关使用此应用程序在 SDN 中进行测试的更多信息,请参阅文档

但是,它确实演示了如何使用自定义查询和 Flux 返回类型,因此它可能作为示例或用于其他应用程序的模板而引起兴趣。

设置控制器

通过存储库,我们有了访问数据库中电影数据的方法。现在,让我们定义允许用户访问这些方法并查询数据库的端点。

控制器充当数据层和用户界面之间的信使,负责接受用户的请求并返回响应。这里通常放置代码逻辑和数据操作,根据接收到的输入类型协调不同的响应。

因为我们的用例范围关注电影,所以我们只需要创建一个控制器来访问电影数据。

MovieController.java

@RestController
@RequestMapping("/movies")
public class MovieController {
	private final MovieRepository movieRepository;
	public MovieController(MovieRepository movieRepository) {
		this.movieRepository = movieRepository;
	}
	//method implementations with walkthroughs below
}

首先,我们需要一些注解来声明这是一个用于 REST 请求的控制器 (@RestController) 并将请求映射到特定路径的控制器方法 (@RequestMapping,端点为 /movies)。

在我们的类定义中,我们首先注入我们的存储库接口并为其创建一个构造函数。这使我们能够从我们的存储库接口和域类访问数据层。

现在我们需要添加更多代码来定义端点并实现我们的数据方法。

@PutMapping
Mono<MovieEntity> createOrUpdateMovie(@RequestBody MovieEntity newMovie) {
	return movieRepository.save(newMovie);
}

首先是 createOrUpdateMovie() 的实现。我们首先使用 @PutMapping 注解来指定一个 put 请求(覆盖或替换一个对象)。我们希望指定一个电影来覆盖或创建,所以我们使用 Mono 返回类型并传入具有所有预期字段的电影对象。在方法内部,我们将通过调用电影存储库的 save() 方法来保存新的或更新的电影。

现在,如果您滚动回我们上面定义的MovieRepository接口,您可能会注意到我们没有在那里定义保存方法。这是因为 Spring Data 存储库为我们开箱即用地提供了几个默认方法。save()findAll() 等方法是几乎每个应用程序都想要或需要的方法,因此 Spring 提供了它们,我们每次创建数据访问时都不必实现这些基本方法。

让我们向控制器添加另一个方法 getMovies()

@GetMapping(value = { "", "/" }, produces = MediaType.TEXT_EVENT_STREAM_VALUE)
Flux<MovieEntity> getMovies() {
	return movieRepository.findAll();
}

@GetMapping 注解告诉我们只从数据库检索数据,而不是修改或插入。我们有两个注解参数,其中我们传递 URL 路径上的任何额外深度(在本例中,没有额外深度 - 只有 /movies),并且我们希望返回一个文本事件流。这是我们的媒体类型,因为我们期望 Flux 的结果(0 到 n 个),并且我们希望它们在传入时返回(反应流),而不是一次聚合并返回所有结果(命令式 json 对象)。就像我们之前的方法一样,我们调用电影存储库并访问开箱即用的 findAll() 方法以返回数据库中的所有电影。

下一个方法是我们定义在 MovieRepository 接口中的方法。

@GetMapping("/by-title")
Mono<MovieEntity> byTitle(@RequestParam String title) {
	return movieRepository.findOneByTitle(title);
}

开头的 @GetMapping 指定了 /by-title 的子路径。由于我们正在搜索一部电影,用户将输入标题作为搜索字符串,我们期望返回 0 或 1 个结果,类型为 Mono,并将用户定义的电影标题参数传入方法。在返回中,我们再次调用电影存储库并访问我们定义的 findOneByTitle() 方法,传入搜索标题。

对于最后一个方法定义,我们希望允许用户从我们的数据库中删除一部电影。

@DeleteMapping("/\{id\}")
Mono<Void> delete(@PathVariable String id) {
	return movieRepository.deleteById(id);
}

我们使用 @DeleteMapping 注解并将子路径端点指定为 /movies/{id}(其中 id 代表我们要删除的电影的 id)。我们只希望一次删除一部电影,并且不期望返回对象(因为它将被删除且不再在数据库中),因此我们将 Mono<Void> 指定为返回类型。该方法已定义,并传入一个路径变量(用户输入定义 URL 路径)用于要删除的电影的 id,然后使用开箱即用的 deleteById() 方法和电影 id 调用电影存储库。

运行应用程序

我们已经准备好所有的代码,现在应该可以构建并运行我们的应用程序,并尝试我们设置的端点了!我们可以运行应用程序(从 IDE 的菜单选项或从命令行),然后打开网页浏览器或命令行与端点交互。本例中,我们将展示如何从命令行角度进行交互。

无论您如何连接,我们都将使用 localhost:8080/movies 路径来访问 findAll() 方法并检索数据库中的所有电影,然后添加任何定义的子路径以深入到其他方法。我们可以访问下面显示的每个端点并验证一切是否按预期工作。

从命令行交互

以下是每个端点从命令行操作的语法

  • localhost:8080/movies 用于 getMovies() 方法

curl https://:8080/movies

结果:检索数据库中的所有电影

  • localhost:8080/movies <movieToUpdateOrCreate> 用于 createOrUpdateMovie() 方法

curl -X "PUT" "https://:8080/movies" \
     -H 'Content-Type: application/json; charset=utf-8' \
     -d $'{
  "title": "Aeon Flux",
  "description": "Reactive is the new cool"
}'

结果:在数据库中创建新电影 Aeon Flux

  • localhost:8080/movies/by-title 用于 byTitle() 方法

curl https://:8080/movies/by-title\?title\=Aeon%20Flux

结果:检索特定电影的信息(在此查询中为 Aeon Flux

  • localhost:8080/movies/{id} 用于 delete() 方法

curl -X DELETE https://:8080/movies/847

结果:使用电影 ID 删除电影(在本例中为 Aeon Flux 电影)

© . All rights reserved.