本章带你用Spring Data Neo4j构建一个应用,从Neo4j(一个基于图形的NoSQL数据库)中保存和取出数据。
本文目标
我们将会使用Spring Data Neo4j创建一个内置的Neo4j服务器,来存储实体和关系,并开发查询。
你需要
- 15分钟左右
- IntelliJ IDEA
- JDK 1.8+
- Maven 3.2+
用Spring Initializr生成项目代码
对于所有的Spring应用,你都可以使用Spring Initializr生成基本的项目代码。Initializr提供了一个快速的方式去引入所有你需要的依赖,并且为你做了很多设置。当前例子只需要Spring Data Neo4j依赖。具体设置如下图:
如上图所示,我们选择了Maven作为编译工具。你也可以选择Gradle来进行编译。然后我们分别把Group和Artifact设置为“com.hansy”和“accessing-data-neo4j”。
生成的pom.xml文件内容如下:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.2.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.hansy</groupId>
<artifactId>accessing-data-neo4j</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>accessing-data-neo4j</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-neo4j</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
启动一个Neo4j服务器
在我们实现应用的其他功能之前,我们需要设置一个Neo4j服务器。
Neo4j有一个开源的服务器,我们可以免费安装。
在官网下载链接中选择你的系统的对应版本,进行安装。
安装成功之后,访问http://localhost:7474,显示如下界面:
默认情况下,Neo4j服务器的用户名和密码是:neo4j和neo4j。登入进去之后,需要设置新的密码。为了方便测试,我们把密码设置为secret(生产环境需要更复杂的密码)。
完成以上设置之后,我们可以开始使用Neo4j了。
定义一个简单的实体
Neo4j包含实体和关系,他们有相同的重要性。可以想象一个这样的场景,你想要为每个人储存一条记录。但是,你还想记录每个人的同事(本例中的teammates字段)。使用Spring Data Neo4j,你可以利用几个简单的注解,实现该功能:
package com.hansy.accessingdataneo4j;
import org.neo4j.ogm.annotation.GeneratedValue;
import org.neo4j.ogm.annotation.Id;
import org.neo4j.ogm.annotation.NodeEntity;
import org.neo4j.ogm.annotation.Relationship;
import java.util.Collections;
import java.util.HashSet;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
@NodeEntity
public class Person {
@Id
@GeneratedValue
private Long id;
private String name;
private Person() {
//Empty constructor required of Neo4j API 2.0.5
}
public Person(String name) {
this.name = name;
}
/**
* Neo4j doesn't REALLY have bi-directional relationships. It just means when querying
* * to ignore the direction of the relationship.
* * https://dzone.com/articles/modelling-data-neo4j
*/
@Relationship(type = "TEAMMATE", direction = Relationship.UNDIRECTED)
public Set<Person> teammates;
public void workWith(Person person) {
if (teammates == null) {
teammates = new HashSet<>();
}
teammates.add(person);
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return this.name + "'s teammates => "
+ Optional.ofNullable(this.teammates).orElse(
Collections.emptySet()).stream()
.map(Person::getName)
.collect(Collectors.toList());
}
}
这里的Person类只有一个属性:name。
Person类上面添加了@NodeEntity注解。当Neo4j存储Person对象时,会创建一个新的节点。Person类还有一个标记了@Id注解的id字段。Neo4j使用@Id在内部标识区分数据。
下一个重要的点是teammates集合。类型是Set,并添加了@Relationship注解。意味着这个集合的每个成员都是一个单独的Person节点。然后@Relationship的方向设置为了UNDIRECTED。这使得当你查询TEAMMATE的关系, Spring Data Neo4j会忽略关系的方向。
使用worksWith()方法,你可以简单得建立Person之间的关系。
最后,toString()方法可以方便的打印出Person的名字和Person的同事。
创建简单的查询
Spring Data Neo4j聚焦于用Neo4j存储数据。 但是它继承了Spring Data Commons项目的功能,包括获得查询的能力。也就是说,你不用学习Neo4j的查询语言,只需要编写少量方法就能实现查询功能。
为了看一下使用方法,我们创建一个查询Person节点的接口。代码如下:
package com.hansy.accessingdataneo4j;
import org.springframework.data.neo4j.repository.Neo4jRepository;
import java.util.List;
public interface PersonRepository extends Neo4jRepository<Person,Long> {
Person findByName(String name);
List<Person> findByTeammatesName(String name);
}
PersonRepository继承了Neo4jRepository接口,并且放入了将要操作的类:Person。这个接口附带了很多的操作,包括标准的CRUD(Create、Read、Update和Delete)操作。
你还可以定义其他的查询操作通过声明方法签名。在本例中,我们添加了findByName方法,来查找Person类型的节点,根据name字段是否符合传入的参数。还添加了findByTeammatesName方法,同样用来查找Person类型的节点,不过是根据teammates里面的Person对象的name字段是否有符合条件来判断的。
设置访问Neo4j的权限
Neo4j社区版本在访问时需要凭证。你可以用一组属性配置这些凭证(在src/main/resources/application.properties里面),如下:
spring.data.neo4j.username=neo4j
spring.data.neo4j.password=secret
这里面包括默认用户名(neo4j)和我们刚才新设置的密码(secret)。
以上只是演示,实际开发中不要在源代码库中保存真实的凭证。而应该在你的运行时进行配置。
修改Application类
添加了 @SpringBootApplication 的类的包和子包中,Spring Boot会自动扫描到我们定义的Repository接口。为了更好地控制这个注册过程,我们可以使用 @EnableNeo4jRepositories 注解。
默认情况下,@EnableNeo4jRepositories会扫描当前包下面的所有继承了Spring Data的Repository接口的接口。如果我们是多项目的结构的话,我们可以使用basePackageClasses=MyRepository.class属性,告诉Spring Data Neo4j去扫描一个不同包下面的接口。
…
Application类的代码如下:
package com.hansy.accessingdataneo4j;
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;
import java.util.Arrays;
import java.util.List;
@SpringBootApplication
@EnableNeo4jRepositories
public class AccessingDataNeo4jApplication {
private final static Logger log = LoggerFactory.getLogger(AccessingDataNeo4jApplication.class);
public static void main(String[] args) {
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.forEach(person -> log.info("\t" + person.toString()));
personRepository.save(greg);
personRepository.save(roy);
personRepository.save(craig);
greg = personRepository.findByName(greg.getName());
greg.workWith(roy);
greg.workWith(craig);
personRepository.save(greg);
roy = personRepository.findByName(roy.getName());
roy.workWith(craig);
//We already know that roy works with greg
personRepository.save(roy);
//We already know craig works with roy and greg
log.info("Lookup each person by name...");
team.forEach(person -> log.info("\t" + personRepository.findByName(person.getName()).toString()));
List<Person> teammates = personRepository.findByTeammatesName(greg.getName());
log.info("The following have Greg as a teammate...");
teammates.forEach(person -> log.info("\t" + person.getName()));
};
}
}
main方法使用Spring Boot的SpringApplication.run()方法来加载应用,并调用CommandLineRunner来构建实体之间的关系。
在本例中,我们创建了三个本地的Person对象:Greg,Roy和Craig。最初他们只存在在内存中。并且他们之间还没有伙伴关系。
首先,我们查询Greg,并指出他跟Roy和Craig工作在一起,然后保存Greg。注意,伙伴关系标识为UNDIRECTED(也就是双向的意思)。这意味着Roy和Craig也会被更新。
这也是为什么我们在更新Roy之前,需要首先从Neo4j中获取最新的数据的原因。在把Craig添加到关系列表之前,我们需要Roy关系的最新状态。
为什么没有从Neo4j中获取Craig,并添加任何关系?因为我们已经有了!Greg在之前就已经标识Craig为伙伴了,Roy也是。这意味着没有必要在更新Craig的关系。我们可以看到这些关系,通过迭代每个成员,并打印出他们的信息到控制台。
最后,我们直接查询Greg的伙伴,并显示。
运行程序
运行程序,在控制台里面,可以看到如下信息:
Before linking up with Neo4j...
Greg's teammates => []
Roy's teammates => []
Craig's teammates => []
Lookup each person by name...
Greg's teammates => [Roy, Craig]
Roy's teammates => [Craig, Greg]
Craig's teammates => [Roy, Greg]
The following have Greg as a teammate...
Roy
Craig
我们可以看到,最开始所有人之间都没有关系。然后,在我们添加关系之后,他们绑定到了一起。最后,我们可以看到可以很方便得通过关系查找伙伴。
小结
我们配置好了一个内置的Neo4j服务器,储存了一些简单的关系实体,开发了一些快速查询。