跟着班导师做项目的时候遇到社交网络的部分,而传统的关系数据库不能很好解决数据之间的关系,因此采用图数据的方式进行存储。Neo4j算是主流的图数据库,多应用于知识图谱、社交网络等。
这两天学习SpringBoot时碰到了很多问题
看了(128条消息) 【最新】Neo4j官方建议Spring Boot 2.4.及以上版本用Neo4j Java Driver代替The Spring Boot starter_学到一寸是一寸的博客-CSDN博客的博客后才知道是因为SpringBoot版本较高(2.4以上)时集成的Neo4j的API规则方法在变化。但是他的博客还是没有说明如何使用类似@Node的注解,自己特意去查了Neo4j官方文档和Spring Neo4j的官方文档并进行总结整理发出来。
本文将解决的问题
TODO
本文将会结合官网的一个 【导演-电影-演员】关系图来进行实现。以新海诚导演的《你的名字》为例。
[
{
"n": {
"identity": 10,
"labels": [
"Movie"
],
"properties": {
"tagline": "影片讲述了男女高中生在梦中相遇,并寻找彼此的故事。",
"title": "你的名字"
}
}
},
{
"n": {
"identity": 11,
"labels": [
"Person"
],
"properties": {
"born": 1997,
"name": "上白石萌音"
}
}
},
{
"n": {
"identity": 12,
"labels": [
"Person"
],
"properties": {
"born": 1993,
"name": "神木隆之介"
}
}
},
{
"n": {
"identity": 13,
"labels": [
"Person"
],
"properties": {
"born": 1973,
"name": "新海诚"
}
}
}
]
注:本文所使用SpringBoot版本为2.7.4,Java8;(Java8+均可),neo4j的安装不再赘述,请提前安装配置好
指定对应的SpringBoot版本(注意本文适配2.4版本及以上)和相应的依赖,当然也可以后续pom依赖中添加坐标(注意pom有两个neo4j相关的依赖)
<parent>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-parentartifactId>
<version>2.7.4version>
<relativePath/>
parent>
<groupId>com.examplegroupId>
<artifactId>Neo4jDemoartifactId>
<version>0.0.1-SNAPSHOTversion>
<name>Neo4jDemoname>
<description>Neo4jDemodescription>
<properties>
<java.version>8java.version>
properties>
<dependencies>
<dependency>
<groupId>org.neo4j.drivergroupId>
<artifactId>neo4j-java-driverartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-neo4jartifactId>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
dependencies>
spring:
neo4j:
uri: bolt://>:7687
authentication:
username: >
password: >
# 指定数据库
data:
neo4j:
database: >
创建utils
包,并在该包下创建ExampleCommandLineRunner
来装配Driver和Session
package com.example.neo4jdemo.utils;
import lombok.extern.slf4j.Slf4j;
import org.neo4j.driver.*;
import org.springframework.boot.CommandLineRunner;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;
@Component
@Slf4j
public class ExampleCommandLineRunner implements CommandLineRunner {
private final Driver driver;
private final ConfigurableApplicationContext applicationContext;
public final Session session;
@Bean
Session session(){
return session;
}
// Autowire the Driver bean by constructor injection
public ExampleCommandLineRunner(Driver driver, ConfigurableApplicationContext applicationContext) {
this.driver = driver;
this.applicationContext = applicationContext;
this.session = driver.session();
}
@Override
public void run(String... args) throws Exception {
}
}
节点介绍:
创建entity
包,添加实体类:PersonEntity
和MovieEntity
package com.example.neo4jdemo.entity;
import lombok.Data;
import org.springframework.data.neo4j.core.schema.GeneratedValue;
import org.springframework.data.neo4j.core.schema.Id;
import org.springframework.data.neo4j.core.schema.Node;
@Node("Person")
@Data
public class PersonEntity {
@Id
@GeneratedValue
private Long id;
private String name;
private Integer born;
public PersonEntity(Integer born, String name) {
this.name = name;
this.born = born;
}
}
package com.example.neo4jdemo.entity;
import lombok.Data;
import org.springframework.data.neo4j.core.schema.*;
import java.util.ArrayList;
import java.util.List;
/**
* Movie实体类,一个neo4j的节点
*/
@Node(labels = "Movie") // 标签名,labels可以缺省
@Data
public class MovieEntity {
@Id
@GeneratedValue // Id自增
private Long id;
private final String title;
@Property("tagline") // 映射到neo4j的属性名
private final String description;
public MovieEntity(String title, String description) {
this.id = null;// 生成node时自动生成
this.title = title;
this.description = description;
}
// 用户指定特定的Id
public MovieEntity withId(Long id) {
if (this.id!= null && this.id.equals(id)) {
return this;
} else {
MovieEntity newObject = new MovieEntity(this.title, this.description);
newObject.id = id;
return newObject;
}
}
}
1.withId在需要指定节点id而非自动生成时使用。
2.注意到除了id外的属性均被final修饰,一种构造优化,提高执行效率:(来自Spring Data Neo4j)
This gives us a roundabout 25% performance boost over reflection. For the domain class to be eligible for such optimization, it needs to adhere to a set of constraints:
- Types must not reside in the default or under the
java
package.- Types and their constructors must be
public
- Types that are inner classes must be
static
.- The used Java Runtime must allow for declaring classes in the originating
ClassLoader
. Java 9 and newer impose certain limitations.By default, Spring Data attempts to use generated property accessors and falls back to reflection-based ones if a limitation is detected.
关系解释:(以ACTED_IN即参演关系为例)
创建Roles
和完善MovieEntity
package com.example.neo4jdemo.entity;
import org.springframework.data.neo4j.core.schema.RelationshipId;
import org.springframework.data.neo4j.core.schema.RelationshipProperties;
import org.springframework.data.neo4j.core.schema.TargetNode;
import java.util.List;
/**
* 定义一个关系属性
*/
@RelationshipProperties
public class Roles {
@RelationshipId
private Long id;
private final List<String> roles;
@TargetNode // 相当于@StartNode
private final PersonEntity person;
// 参数1是目标关系实体节点 参数2是关系属性
// Roles 参数1:Person实体,演员的出生年和姓名;参数2:演员名字列表(考虑到一个演员可能参演多个角色)
public Roles(PersonEntity person, List<String> roles) {
this.person = person;
this.roles = roles;
}
public List<String> getRoles() {
return roles;
}
}
注意这些关系**@TargetNode
修饰的是关系箭头的尾部, 最终的箭头指向是当前实体**,即TargetNode(PersonEntity)->当前定义Relationship的实体(MovieEntity)
package com.example.neo4jdemo.entity;
import lombok.Data;
import org.springframework.data.neo4j.core.schema.*;
import java.util.ArrayList;
import java.util.List;
/**
* Movie实体类,一个neo4j的节点
*/
@Node(labels = "Movie") // 标签名,labels可以缺省
@Data
public class MovieEntity {
...(上面 1.节点 有)
// 定义一个关系(参演)[direction]
@Relationship(type = "ACTED_IN", direction = Relationship.Direction.INCOMING)
private List<Roles> actorsAndRoles = new ArrayList<>();
// 定义一个关系(导演)
@Relationship(type = "DIRECTED", direction = Relationship.Direction.INCOMING)
private List<PersonEntity> directors = new ArrayList<>();
// 注意这些关系最终的箭头指向是当前实体,即TargetNode(PersonEntity)->当前定义Relationship的实体(MovieEntity)
}
了解即可,有封装好的api使用,可以先跳过不看,看3.b部分。
- 创建
# 查询name等于 $name 的 label为Person的实体类集合 MATCH (n:Person {name: $name}) RETURN n 或者 MATCH (n:Person) WHERE n.name = $name RETURN n
n是一个变量
$name 对应map的key
{ } 是 where 筛选的简写
Return 返回符合筛选条件的变量n
查找
# 查询Person和Movie之间 关系种类为ACTED_IN且关系属性role = $roles 的实体类集合 MATCH (person:Person) -[ relation:ACTED_IN ]-> (movie:Movie) WHERE relation.roles = $roles RETURN person 等价于 MATCH (person:Person) -[ relation:ACTED_IN ]-> (:Movie) WHERE relation.roles = $roles RETURN person
person、relation、movie是变量
:Person 、:Movie 用:后面接Label,
person:Person
有点类似变量声明,如果不使用完全可以省略不写用-[]-> 来表示关系的type以及指向
修改
# 新增属性(Person本来只有name和born属性,新增age属性,一般不用) MATCH (n:Person) WHERE n.name = '新海诚' SET n.age = 50 # 修改属性 MATCH (n:Person) WHERE n.name = "新津城" SET n.name = "新海诚" # 新增/修改多个属性 MATCH (n:Person) WHERE n.name = '新海诚' SET n.age = 50, n.name="新津城"
- 删除
# 删除属性(一般不用) MATCH (n:Person) WHERE n.name = "新津城" REMOVE n.age # 删除关系(新海诚的导演关系删除) MATCH (n:Person) -[k:DIRECTED]-> (m:Movie) WHERE n="新海诚" DELETE k # 删除节点 MATCH (n:Person) WHERE n.name = "新海诚" DELETE n // 当该实体仍有关系时会报错,必须先删除关系再删除节点 # 删除节点(会删除节点实体和他关联的属性) MATCH (n:Person) WHERE n.name = "新海诚" DETACH DELETE n
// 创建节点实体
MovieEntity movie = new MovieEntity("你的名字","影片讲述了男女高中生在梦中相遇,并寻找彼此的故事。");// 电影实体节点
// 定义(参演)关系
// new Roles 参数1:Person实体,演员的出生年和姓名;参数2:演员名字列表(考虑到一个演员可能参演多个角色)
// 参数1是目标关系实体节点 参数2是关系属性
Roles roles1 = new Roles(new PersonEntity(1998,"上白石萌音"), Collections.singletonList("宫水三叶"));
Roles roles2 = new Roles(new PersonEntity(1993,"神木隆之介"), Collections.singletonList("立花泷"));
PersonEntity director = new PersonEntity(1973,"新海诚");
// 添加movie的演员实体,加入(参演)关系
movie.getActorsAndRoles().add(roles1);
movie.getActorsAndRoles().add(roles2);
movie.getDirectors().add(director);
// 存入图数据库持久化
neo4jTemplate.save(movie);
结果:
// 查询(不太推荐用Neo4jTemplate进行过滤查询,因为需要手动写cypherQuery,需要开发人员了解一下cypherQuery的写法)
Optional<PersonEntity> person;
// 1. 通过id查询
person = neo4jTemplate.findById(12, PersonEntity.class);
System.out.println("id为12号的Person节点:\n"+person);
// 2. 通过属性查询节点,如name 需要手写cypherQuery语句
Map<String,Object> map = new HashMap<>();
map.put("name","新海诚");
// 两种写法都对,看个人喜好 n是一个变量随意取,{}或者where填写query的filter过滤条件
person = neo4jTemplate.findOne("MATCH (n:Person {name: $name}) RETURN n",map, PersonEntity.class);
// person = neo4jTemplate.findOne("MATCH (n:Person) WHERE n.name = $name RETURN n",map, PersonEntity.class);
System.out.println("\n查询名字为新海诚的Person节点:\n"+person);
// 3. 通过属性关系查询节点
map = new HashMap<>();
map.put("roles",Collections.singletonList("宫水三叶"));
// 方法1.使用toExecutableQuery查询
QueryFragmentsAndParameters parameters = new QueryFragmentsAndParameters(
"MATCH (person:Person) -[ relation:ACTED_IN]-> (movie:Movie) \n" +
"WHERE relation.roles = $roles\n" +
"RETURN person",map);
List<PersonEntity> roles = neo4jTemplate.toExecutableQuery(PersonEntity.class, parameters).getResults();
// 方法2.使用findOne查询
// Optional roles = neo4jTemplate.findOne(
// "MATCH (person:Person) -[ relation:ACTED_IN]-> (movie:Movie) \n" +
// "WHERE relation.roles = $roles\n" +
// "RETURN person",map,PersonEntity.class);
System.out.println("\n查询角色为“宫水三叶”的演员:\n"+roles);
结果:
Long userId = person.get().getId();// 记录当前查询的"新海诚"的节点id
// 更新①---------更新“新海诚”的name为曾用名“新津诚”(这是他的曾用名)
map.put("name","新海诚");
map.put("usedName","新津诚");
QueryFragmentsAndParameters queryFragmentsAndParameters =
new QueryFragmentsAndParameters(
"MATCH (n:Person{name: $name}) SET n.name = $usedName",
map);
neo4jTemplate.toExecutableQuery(
PersonEntity.class,
queryFragmentsAndParameters).getResults();
Optional<PersonEntity> person1 = neo4jTemplate.findById(userId, PersonEntity.class);
System.out.println("\n更新“新海诚”的name为曾用名“新津诚”(这是他的曾用名):\n"+person1);
// 更新②---------更新“新津诚”的name为“新海诚”
person.get().setName("新海诚");
neo4jTemplate.save(person.get());
Optional<PersonEntity> person2 = neo4jTemplate.findById(userId, PersonEntity.class);
System.out.println("\n更新“新津诚”的name为“新海诚”:\n"+person2);
结果:
// 删除所有节点和关系(删除节点会响应删除关联关系)[也可以用cypherQuery执行,不再赘述]
neo4jTemplate.deleteAll(MovieEntity.class);
neo4jTemplate.deleteAll(PersonEntity.class);
上方的api根据实际情况使用,不再赘述。
/**
* 没有Repository情况下使用Neo4jTemplate操作数据
* @param neo4jTemplate
*/
@Test
void TestNoRepository(@Autowired Neo4jTemplate neo4jTemplate){
// 删除所有节点和关系(删除节点会响应删除关联关系),避免后续创建节点重复影响
neo4jTemplate.deleteAll(MovieEntity.class);
neo4jTemplate.deleteAll(PersonEntity.class);
// 创建节点实体
MovieEntity movie = new MovieEntity("你的名字","影片讲述了男女高中生在梦中相遇,并寻找彼此的故事。");
// new Roles 参数1:Person实体,演员的出生年和姓名;参数2:演员名字列表(考虑到一个演员可能参演多个角色)
// 参数1是目标关系实体节点 参数2是关系属性
Roles roles1 = new Roles(new PersonEntity(1998,"上白石萌音"), Collections.singletonList("宫水三叶"));
Roles roles2 = new Roles(new PersonEntity(1993,"神木隆之介"), Collections.singletonList("立花泷"));
PersonEntity director = new PersonEntity(1973,"新海诚");
// 添加movie的演员实体,加入(参演)关系
movie.getActorsAndRoles().add(roles1);
movie.getActorsAndRoles().add(roles2);
movie.getDirectors().add(director);
// 存入图数据库持久化
neo4jTemplate.save(movie);
// 查询(不太推荐用Neo4jTemplate进行过滤查询,因为需要手动写cypherQuery,需要开发人员了解一下cypherQuery的写法)
Optional<PersonEntity> person;
// 1. 通过id查询
person = neo4jTemplate.findById(12, PersonEntity.class);
System.out.println("id为12号的Person节点:\n"+person);
// 2. 通过属性查询节点,如name 需要手写cypherQuery语句
Map<String,Object> map = new HashMap<>();
map.put("name","新海诚");
// 两种写法都对,看个人喜好 n是一个变量随意取,{}或者where填写query的filter过滤条件
person = neo4jTemplate.findOne("MATCH (n:Person {name: $name}) RETURN n",map, PersonEntity.class);
// person = neo4jTemplate.findOne("MATCH (n:Person) WHERE n.name = $name RETURN n",map, PersonEntity.class);
System.out.println("\n查询名字为新海诚的Person节点:\n"+person);
// 3. 通过属性关系查询节点
map = new HashMap<>();
map.put("roles",Collections.singletonList("宫水三叶"));
// 方法1.使用toExecutableQuery查询
QueryFragmentsAndParameters parameters = new QueryFragmentsAndParameters(
"MATCH (person:Person) -[ relation:ACTED_IN]-> (movie:Movie) \n" +
"WHERE relation.roles = $roles\n" +
"RETURN person",map);
List<PersonEntity> roles = neo4jTemplate.toExecutableQuery(PersonEntity.class, parameters).getResults();
// 方法2.使用findOne查询
// Optional roles = neo4jTemplate.findOne(
// "MATCH (person:Person) -[ relation:ACTED_IN]-> (movie:Movie) \n" +
// "WHERE relation.roles = $roles\n" +
// "RETURN person",map,PersonEntity.class);
System.out.println("\n查询角色为“宫水三叶”的演员:\n"+roles);
Long userId = person.get().getId();// 记录当前查询的"新海诚"的节点id
// 更新①---------更新“新海诚”的name为曾用名“新津诚”(这是他的曾用名)
map.put("name","新海诚");
map.put("usedName","新津诚");
QueryFragmentsAndParameters queryFragmentsAndParameters =
new QueryFragmentsAndParameters(
"MATCH (n:Person{name: $name}) SET n.name = $usedName",
map);
neo4jTemplate.toExecutableQuery(
PersonEntity.class,
queryFragmentsAndParameters).getResults();
Optional<PersonEntity> person1 = neo4jTemplate.findById(userId, PersonEntity.class);
System.out.println("\n更新“新海诚”的name为曾用名“新津诚”(这是他的曾用名):\n"+person1);
// 更新②---------更新“新津诚”的name为“新海诚”
person.get().setName("新海诚");
neo4jTemplate.save(person.get());
Optional<PersonEntity> person2 = neo4jTemplate.findById(userId, PersonEntity.class);
System.out.println("\n更新“新津诚”的name为“新海诚”:\n"+person2);
}
新建repository
包,创建PersonRepository
和MovieRepository
package com.example.neo4jdemo.repository;
import com.example.neo4jdemo.entity.PersonEntity;
import org.springframework.data.neo4j.repository.Neo4jRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface PersonRepository extends Neo4jRepository<PersonEntity, Long> {
}
package com.example.neo4jdemo.repository;
import com.example.neo4jdemo.entity.MovieEntity;
import org.springframework.data.neo4j.repository.Neo4jRepository;
import org.springframework.data.neo4j.repository.query.Query;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface MovieRepository extends Neo4jRepository<MovieEntity, Long> {
}
// 创建节点
MovieEntity movie = new MovieEntity("你的名字","影片讲述了男女高中生在梦中相遇,并寻找彼此的故事。");
Roles roles1 = new Roles(new PersonEntity(1998,"上白石萌音"), Collections.singletonList("宫水三叶"));
Roles roles2 = new Roles(new PersonEntity(1993,"神木隆之介"), Collections.singletonList("立花泷"));
PersonEntity director = new PersonEntity(1973,"新海诚");
// 添加关系
movie.getActorsAndRoles().add(roles1);
movie.getActorsAndRoles().add(roles2);
movie.getDirectors().add(director);
// 存入图数据库持久化
movieRepository.save(movie);
需求:根据Person的名字查询对应节点
在PersonRepository
中添加方法:
@Repository
public interface PersonRepository extends Neo4jRepository<PersonEntity, Long> {
PersonEntity findPersonEntityByName(String name);
}
在MovieRepository
中添加方法:
@Repository
public interface MovieRepository extends Neo4jRepository<MovieEntity, Long> {
// @Query("MATCH (n:Movie) WHERE id(n) = $0 RETURN n") 这种方法是自己写Query语句进行查询
List<MovieEntity> findMovieEntitiesById(Long id);
MovieEntity findMovieEntityByTitle(String title);
}
查询:
// 查询
// 查询
PersonEntity person = personRepository.findPersonEntityByName("上白石萌音");
System.out.println("查询名字为“上白石萌音”的PersonEntity:"+person);
MovieEntity movieQueried = movieRepository.findMovieEntityByTitle("你的名字");
System.out.println("查询名字为“你的名字”的MovieEntity:"+movieQueried);
结果:
查询名字为“上白石萌音”的PersonEntity:
PersonEntity(id=15, name=上白石萌音, born=1998)
查询名字为“你的名字”的MovieEntity:
MovieEntity(id=14, title=你的名字, description=影片讲述了男女高中生在梦中相遇,并寻找彼此的故事。, actorsAndRoles=[com.example.neo4jdemo.entity.Roles@d902300, com.example.neo4jdemo.entity.Roles@2db33feb], directors=[PersonEntity(id=17, name=新海诚, born=1973)])
// 更新(更新主要是三步:1.获取实体id;2.修改实体属性;3.更新实体)
// 注意:repository的save方法【对应的实体若id一致】则为修改,否则为新建。
Long personId = person.getId();
person.setBorn(1997);
personRepository.save(person);
person = personRepository.findPersonEntityByName("上白石萌音");
System.out.println(personId == person.getId()?"\n更新“上白石萌音”出生日期为1997信息成功!:\n"+person:"更新信息失败!");
// 删除所有节点和关系
movieRepository.deleteAll();
personRepository.deleteAll();
/**
* 使用repository操作图数据
*/
@Test
void testByRepository(@Autowired MovieRepository movieRepository, @Autowired PersonRepository personRepository){
// 删除所有节点和关系(删除节点会响应删除关联关系),避免后续创建节点重复影响
movieRepository.deleteAll();
personRepository.deleteAll();
// 创建节点
MovieEntity movie = new MovieEntity("你的名字","影片讲述了男女高中生在梦中相遇,并寻找彼此的故事。");
Roles roles1 = new Roles(new PersonEntity(1998,"上白石萌音"), Collections.singletonList("宫水三叶"));
Roles roles2 = new Roles(new PersonEntity(1993,"神木隆之介"), Collections.singletonList("立花泷"));
PersonEntity director = new PersonEntity(1973,"新海诚");
// 添加关系
movie.getActorsAndRoles().add(roles1);
movie.getActorsAndRoles().add(roles2);
movie.getDirectors().add(director);
// 存入图数据库持久化
movieRepository.save(movie);
// 查询
PersonEntity person = personRepository.findPersonEntityByName("上白石萌音");
System.out.println("\n查询名字为“上白石萌音”的PersonEntity:\n"+person);
MovieEntity movieQueried = movieRepository.findMovieEntityByTitle("你的名字");
System.out.println("\n查询名字为“你的名字”的MovieEntity:\n"+movieQueried);
// 更新(更新主要是三步:1.获取实体id;2.修改实体属性;3.更新实体)
// 注意:repository的save方法【对应的实体若id一致】则为修改,否则为新建。
Long personId = person.getId();
person.setBorn(1997);
personRepository.save(person);
person = personRepository.findPersonEntityByName("上白石萌音");
System.out.println(personId == person.getId()?"\n更新“上白石萌音”出生日期为1997信息成功!:\n"+person:"更新信息失败!");
}
案例来自官方文档:
https://docs.spring.io/spring-data/neo4j/docs/current/reference/html/#conversions https://neo4j.com/docs/aura/auradb/connecting-applications/spring-boot/
源码戳这里:
https://github.com/WuYiheng-Og/neo4j_springboot