使用 Spring Data Neo4j 访问图数据库

本指南介绍如何使用 Spring Data Neo4j 构建应用,将数据存入图数据库 Neo4j 并查询出来。

将构建什么

使用 Neo4j 的 NoSQL 图数据存储,搭建 Neo4j 服务器,存储实体和关系,并编写查询。原文概述称其为“嵌入式 Neo4j 服务器”;下文实际演示的是独立启动的服务器或 Docker 容器,通过 Bolt 连接。

需要什么

也可以直接将代码导入 IDE:Spring Tool Suite(STS)、IntelliJ IDEA 或 VSCode。

如何完成本指南

与多数 Spring 入门指南一样,可以从零开始完成每一步,也可以跳过已经熟悉的基础设置。两种方式最终都能得到应用代码。

  • 从零开始:进入“从 Spring Initializr 开始”一节。
  • 跳过基础设置:
    1. 下载并解压源码仓库,或使用 Git 克隆:git clone https://github.com/spring-guides/gs-accessing-data-neo4j.git。
    2. 切换到 gs-accessing-data-neo4j/initial 目录。
    3. 跳到“定义一个简单实体”一节。

完成后,可用 gs-accessing-data-neo4j/complete 中的代码核对结果。

从 Spring Initializr 开始

可使用预初始化项目,点击 Generate 下载 ZIP。该项目已经按本指南示例配置。

若手动初始化:

  1. 访问 start.spring.io。这个服务会引入应用所需依赖,并完成大部分设置。
  2. 选择 Gradle 或 Maven,以及开发语言;本指南假定选择 Java。
  3. 点击 Dependencies,选择 Spring Data Neo4j。
  4. 点击 Generate。
  5. 下载生成的 ZIP,它包含按所选配置建立的应用。

若 IDE 集成了 Spring Initializr,也可以在 IDE 内完成。同样可在 GitHub fork 项目,再用 IDE 或编辑器打开。

启动 Neo4j 服务器

构建应用前,先设置 Neo4j 服务器。Neo4j 提供可免费安装的开源服务器,也可以通过 Docker 运行。

在已经安装 Homebrew 的 Mac 上,执行:

$ brew install neo4j

其他安装方式见 Neo4j Community Edition 下载页面。

安装后,以默认设置启动:

$ neo4j start

应看到类似以下的输出:

Starting Neo4j.
Started neo4j (pid 96416). By default, it is available at http://localhost:7474/
There may be a short delay until the server is ready.
See /usr/local/Cellar/neo4j/<version>/libexec/logs/neo4j.log for current status.

Neo4j 默认用户名和密码都是 neo4j,但要求更改新账户的密码。打开 Neo4j Browser,用默认凭据登录,就会收到设置新密码的提示。本指南使用 spring+neo4j 作为演示密码,不要在生产中这样做。完成后即可继续。

也可以使用 Neo4j Docker 镜像运行,通过 NEO4J_AUTH 环境变量修改密码:

docker run \
  --publish=7474:7474 --publish=7687:7687 \
  --volume=$HOME/neo4j/data:/data \
  --env NEO4J_AUTH=neo4j/spring+neo4j \
  neo4j

定义一个简单实体

Neo4j 同时记录实体及其关系,两者同等重要。假设要为每个人保存一条记录,也希望追踪此人推荐了谁,本文称这些关系为 referrals。Spring Data Neo4j 只需一些简单注解就能表示这些内容。下面是 src/main/java/com/example/accessingdataneo4j/Person.java:

package com.example.accessingdataneo4j;

import java.util.Collections;
import java.util.HashSet;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;

import org.jspecify.annotations.Nullable;
import org.springframework.data.neo4j.core.schema.GeneratedValue;
import org.springframework.data.neo4j.core.schema.Id;
import org.springframework.data.neo4j.core.schema.Node;
import org.springframework.data.neo4j.core.schema.Relationship;

@Node
public class Person {

  @Id
  @GeneratedValue
  private @Nullable Long id;

  private String name;

  private Person() {
    // Empty constructor required as of Neo4j API 2.0.5
  };

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

  @Relationship(type = "REFERRED")
  public Set<Person> referrals;

  public void referred(Person person) {
    if (referrals == null) {
      referrals = new HashSet<>();
    }
    referrals.add(person);
  }

  public String toString() {

    return this.name + " referred => "
      + Optional.ofNullable(this.referrals).orElse(
          Collections.emptySet()).stream()
            .map(Person::getName)
            .collect(Collectors.toList());
  }

  public String getName() {
    return name;
  }

  public void setName(String name) {
    this.name = name;
  }
}

Person 类有一个属性 name,并带有 @Node 注解;Neo4j 存储它时会创建新节点。类还有一个带 @Id 和 @GeneratedValue 注解的 id,Neo4j 在内部用它追踪数据。

另一个重要部分是 referrals 集合。它是简单的 Set<Person>,但标注了 @Relationship,表示集合的每个成员也应独立存在为一个 Person 节点。默认方向为 OUTGOING:REFERRED 关系从当前人指向他推荐的每个人。通过 referred() 方法,就能记录谁推荐了谁。最后,便捷的 toString() 方法用于输出此人姓名及推荐对象。

创建简单查询

Spring Data Neo4j 专注于在 Neo4j 存储数据,但也继承 Spring Data Commons 的功能,包括派生查询。因此,不必先学习 Neo4j 查询语言,只需编写几个方法,框架就能生成查询。

下面建立一个查询 Person 节点的接口,文件为 src/main/java/com/example/accessingdataneo4j/PersonRepository.java:

package com.example.accessingdataneo4j;

import java.util.List;

import org.jspecify.annotations.Nullable;
import org.springframework.data.neo4j.repository.Neo4jRepository;

public interface PersonRepository extends Neo4jRepository<Person, Long> {

  @Nullable
  Person findByName(String name);

  List<Person> findByReferralsName(String name);
}

PersonRepository 扩展 Neo4jRepository,传入操作类型 Person。这个接口已经提供很多操作,包括标准的创建、读取、更新、删除(CRUD)。也可以通过声明方法签名定义其他查询。

findByName 寻找 Person 类型中 name 匹配的节点;findByReferralsName 则反向遍历 REFERRED 关系:给定一个人的姓名,返回所有曾推荐此人的 Person 节点。

访问 Neo4j 的权限

Neo4j Community Edition 要求访问凭据。可在 src/main/resources/application.properties 中设置:

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

其中包括默认用户名 neo4j,以及之前设置的演示密码 spring+neo4j。不要将真实凭据存入源码仓库。应在运行时通过 Spring Boot 属性覆盖机制配置。完成后即可将这些部分连接起来。

创建应用类

Spring Initializr 会生成一个简单应用类,本例位于 src/main/java/com/example/accessingdataneo4j/AccessingDataNeo4jApplication.java:

package com.example.accessingdataneo4j;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class AccessingDataNeo4jApplication {

  public static void main(String[] args) {
    SpringApplication.run(AccessingDataNeo4jApplication.class, args);
  }

}

@SpringBootApplication 是组合注解,包含:

  • @Configuration:将类标记为应用上下文的 bean 定义来源。
  • @EnableAutoConfiguration:让 Spring Boot 根据 classpath、其他 bean 和各种属性设置添加 bean。例如 classpath 包含 spring-webmvc 时,会将应用识别为 web 应用,启用设置 DispatcherServlet 等关键行为。
  • @ComponentScan:让 Spring 查找 com/example 包下的组件、配置和服务,从而发现控制器。

main() 使用 Spring Boot 的 SpringApplication.run() 启动应用。整个示例没有一行 XML,也没有 web.xml;配置使用纯 Java,不必手动处理底层基础设施。

只要仓储位于 @SpringBootApplication 类相同的包或子包内,Spring Boot 就会自动处理。要更细地控制注册,可使用 @EnableNeo4jRepositories。默认情况下,它扫描当前包内扩展 Spring Data 仓储接口的接口。如果项目结构涉及多个项目,框架未找到仓储,可通过 basePackageClasses=MyRepository.class,按类型安全地指定另一个扫描根包。

启动会显示日志输出,服务应在几秒内运行。接着自动装配之前定义的 PersonRepository 实例。Spring Data Neo4j 动态实现该接口,并加入满足接口要求的查询代码。

main 方法用 SpringApplication.run() 启动应用,并调用构建关系的 CommandLineRunner。这里创建 Greg、Roy、Craig 三个本地 Person 实例,最初仅在内存中,尚无人推荐别人。

先找到 Greg,记录他推荐了 Roy 和 Craig,再持久化 Greg。由于关系方向为 OUTGOING,此时只保存 Greg 向外的 REFERRED 关系。接着从 Neo4j 重新读取 Roy,确保得到节点最新状态,记录 Roy 推荐了 Craig,再持久化 Roy。Craig 在本例尚未推荐别人,因此不需要更多代码。

遍历每个人并将信息打印到控制台,就能查看结果。最后使用另一个反向查询,回答“谁推荐了 Craig?”完成后的应用类如下:

package com.example.accessingdataneo4j;

import java.util.Arrays;
import java.util.List;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.data.neo4j.repository.config.EnableNeo4jRepositories;

@SpringBootApplication
@EnableNeo4jRepositories
public class AccessingDataNeo4jApplication {

	private final static Logger log = LoggerFactory.getLogger(AccessingDataNeo4jApplication.class);

	public static void main(String[] args) throws Exception {
		SpringApplication.run(AccessingDataNeo4jApplication.class, args);
		System.exit(0);
	}

	@Bean
	CommandLineRunner demo(PersonRepository personRepository) {
		return args -> {

			personRepository.deleteAll();

			Person greg = new Person("Greg");
			Person roy = new Person("Roy");
			Person craig = new Person("Craig");

			List<Person> team = Arrays.asList(greg, roy, craig);

			log.info("Before linking up with Neo4j...");

			team.stream().forEach(person -> log.info("\t" + person.toString()));

			personRepository.save(greg);
			personRepository.save(roy);
			personRepository.save(craig);

			greg = personRepository.findByName(greg.getName());
			greg.referred(roy);
			greg.referred(craig);
			personRepository.save(greg);

			roy = personRepository.findByName(roy.getName());
			roy.referred(craig);
			personRepository.save(roy);

			// Craig has no referrals yet

			log.info("Lookup each person by name...");
			team.stream().forEach(person -> log.info(
					"\t" + personRepository.findByName(person.getName()).toString()));

			List<Person> referrers = personRepository.findByReferralsName(craig.getName());
			log.info("The following referred Craig...");
			referrers.stream().forEach(person -> log.info("\t" + person.getName()));
		};
	}

}

构建可执行 JAR

可以通过 Gradle 或 Maven 从命令行运行应用,也可以构建包含全部依赖、类和资源的单个可执行 JAR,再运行它。这便于在开发生命周期中跨环境交付、版本管理和部署服务。

使用 Gradle 时,运行 ./gradlew bootRun;或者通过 ./gradlew build 构建 JAR,再运行:

java -jar build/libs/gs-accessing-data-neo4j-0.0.1-SNAPSHOT.jar

使用 Maven 时,运行 ./mvnw spring-boot:run;或者通过 ./mvnw clean package 构建 JAR,再运行:

java -jar target/gs-accessing-data-neo4j-0.0.1-SNAPSHOT.jar

应看到类似以下输出,也会有查询等其他日志:

Before linking up with Neo4j...
	Greg referred => []
	Roy referred => []
	Craig referred => []

Lookup each person by name...
	Greg referred => [Roy, Craig]
	Roy referred => [Craig]
	Craig referred => []

The following referred Craig...
	Greg
	Roy

输出显示最初没人推荐别人;建立关系后,可以看出谁推荐了谁;最后,反向查询找到了推荐某个特定人的全部人员。

总结

至此,你已设置 Neo4j 服务器,存储一些具有关系的简单实体,并建立快速查询。原文总结使用“嵌入式服务器”这一说法;按本指南实际步骤运行时,应用连接的是单独运行的 Neo4j。

若希望以少量工作为 Neo4j 仓储提供基于超媒体的 RESTful 前端,可继续阅读通过 REST 访问 Neo4j 数据。

另请参阅

若想编写新指南或贡献现有指南,可查看贡献指南。

获取代码

示例源码仓库。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容