Spring Data Neo4j
对于使用 Spring 框架或 Spring Boot 的 Java 开发人员,并且希望利用响应式开发原则,本指南介绍了通过 Spring Data Neo4j 项目进行的 Spring 集成。该库提供了对 Neo4j 的便捷访问,包括对象映射、Spring Data 存储库、转换、事务处理、响应式支持等。
-
了解 Spring 的一些知识/经验。了解Spring Data和Spring Boot都将是您工具箱中的极佳补充。
-
对于此库,请使用 JDK 11 或更高版本以及您喜欢的 IDE。
响应式开发
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。
然后,您可以更改项目的默认组、构件、名称和描述。接下来,我们可以选择我们的项目依赖项。我们可以搜索并添加 Neo4j
和 Spring 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 驱动程序自动配置,因此我们不需要为驱动程序添加任何其他配置。
创建领域
在定义了项目依赖项并设置了配置后,我们就可以开始为数据领域定义实体了!领域层应该完成两件事
-
将图形映射到对象。
-
提供对这些对象的访问。
我们的数据包含电影和人员实体,展示了人们如何参与各种电影,例如谁主演、导演、编剧、制片等。我们需要为每个实体定义一个领域类 - Movie
和 Person
。
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
,用于显示哪些人员参演了特定电影。下面的两行定义了 MovieEntity
和 PersonEntity
之间的另一种关系,用于那些执导电影的人。
然后,下一个代码块定义了实体的构造函数,其中包含节点的属性(title
和 description
)。
如上所述,您可以将 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
。
如果构建的是命令式应用程序,则可以扩展 |
因为我们的存储库正在实现反应式功能,所以我们可以访问Mono 和Flux 来自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 类型。一旦我们在这里添加了 MovieEntity
和 String
(我们的电影 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
电影)