Spring Data Neo4j

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

响应式开发

Neo4j(版本 4.0+)结合了响应式宣言的原则,用于在数据库和客户端之间传递数据。开发人员可以利用响应式方法来处理查询并返回结果。这意味着可以在驱动程序和数据库之间的通信进行动态管理和调整,以满足客户端的数据需求。

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

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

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

Spring Data Neo4j

Spring Data Neo4j 6 是 Spring Data Neo4j 项目的新主版本。其功能优势之一是能够支持响应式事务,当然还有其他改进和新增功能,例如完全不可变的实体和基于Java 记录的映射支持。

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

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

功能

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

  • 使用内置 OGM(对象图映射)库进行轻量级映射

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

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

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

响应式编程模型需要 4.0+ 版本的 Neo4j 实例(以前的版本不支持响应式驱动程序)以及应用程序端上的响应式 Spring。

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

入门

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

准备数据库

在本例中,我们将使用 Neo4j 标准电影图数据集,因为它随每个 Neo4j 实例免费提供,并且大小较小。

如果您尚未安装,请下载 Neo4j 桌面版创建/启动数据库

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

创建一个新的 Spring Boot 项目

设置 Spring Boot 项目最简单的方法是使用位于start.spring.io的 Spring Initializr。它也集成在主要的 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。此外,虽然从技术上讲并不禁止,但不建议或不支持在同一应用程序中混合命令式和反应式数据库访问。

因为我们的存储库正在实现反应式功能,所以我们可以访问MonoFlux 来自Project Reactor 的反应式类型作为方法返回值。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 http://localhost:8080/movies

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

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

curl -X "PUT" "http://localhost: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 http://localhost:8080/movies/by-title\?title\=Aeon%20Flux

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

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

curl -X DELETE http://localhost:8080/movies/847

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