我们先来看下neo4j的三种连接方式
打开neo4j的配置文件
总过有三种连接方式
常用的有两种,一种是http的连接方式【端口:7474】,一种是Bolt的连接方式【端口:7687】
http的连接方式本篇不再讲了,本篇主要讲第二种连接方式,并结合neo4j提供的原生JavaAPI驱动进行节点的创建和关系的添加
最终实现的效果是
一、项目目录结构
二、项目Pom依赖
<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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.appleyk</groupId>
<artifactId>Spring-Boot-Neo4jAPI</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>war</packaging>
<description>Spring-Boot 集成Neo4j,实现原生JavaAPI的节点、关系操作</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.12.RELEASE</version>
</parent>
<!-- 使用Java8,嘗試使用新特新【stream和lambda】 -->
<properties>
<java.version>1.8</java.version>
<janino.version>3.0.8</janino.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 添加热部署 devtools:监听文件变动 -->
<!-- 当Java文件改动时,Spring-boo会快速重新启动 -->
<!-- 最简单的测试,就是随便找一个文件Ctrl+S一下,就可以看到效果 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<!-- optional=true,依赖不会传递 -->
<!-- 本项目依赖devtools;若依赖本项目的其他项目想要使用devtools,需要重新引入 -->
<optional>true</optional>
</dependency>
<!-- Spring 单元测试 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- JUnit单元测试 -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/org.neo4j.driver/neo4j-java-driver -->
<dependency>
<groupId>org.neo4j.driver</groupId>
<artifactId>neo4j-java-driver</artifactId>
<version>1.6.1</version>
</dependency>
</dependencies>
</project>
三、项目属性配置文件properties
application.properties
server.port=8080
server.session.timeout=10
server.tomcat.uri-encoding=utf8
#在application.properties文件中引入日志配置文件
#===================================== log =============================
logging.config=classpath:logback-boot.xml
#Neo4j配置
#连接器:dbms.connector.bolt 协议:Bolt二进制协议
spring.neo4j.url=bolt://localhost:7687
#账户名称
spring.neo4j.username=neo4j
#账户密码
spring.neo4j.password=n123
四、neo4j图形数据库数据结构组成说明
(1)基本数据组成:节点
match(n) return n == 查询所有节点,不管节点的性质【分类】是什么
上图中红色框圈出来的就是节点的标签,也就是节点的性质是什么,再说直白点就是节点的分类,比如,节点是动物啊还是人类啊,人类中再继续往下分...etc
match(n:Cat) return n == 查询节点性质【分类,标签】等于Cat【猫】的所有节点
(2)关系
何为关系, 这里我就不做文章了,最简单的关系,就是两个节点之间的关系,抽象的表达式就是
(n)-[r]-(b)
抽象图就是
其中start node 代表表达式(n)中的n,relationship代表表达式[r]中的关系r,end node代表表达式(b)中的b
翻译一下就是:节点n,关系r,节点b,具体哪种性质的节点n,哪种关系r,哪种性质的节点b,这里我们需要再指定,三者只要确定一个,就能match出n,r,b的结果,并返回n,r,b具体对应的节点和节点n和b之间的关系r
比如,博主和猫之间的关系查询如下
match(n)-[r:Have]-(b) return n,r,b
我们来分析一下这个查询语句
首先,这个语句只确定了关系r是Have性质的,节点n和b我们并没有指定Label,但是我们却可以通过cypher语句查询出来结果,我们看下这个结果对应的table结构是什么样的
总过三列,没毛病吧,很好理解,因为我们return的结果就是n,r,b,这里关系r是没有属性值的
上面说过,n,r , b 三者确定一个就可以匹配结果,不信的话,我们下面再查询一把,这次我们确定节点n
match(n:Coder)-[r]-(b) return n,r,b
翻译成文字就是:查询所有和Coder性质的节点n有关系r的节点b,并返回n,r, b
上述语句的查询效果就是如下
如果我们只想要节点b,不想要节点b和关系r出现在返回的结果里,我们可以只返回b就行了,效果如下
由于节点b包含的有自己的关系,因此,查出来的结果带有关系也就不足为奇了
好了,扯了那么多,我们回归正题,如何实现文章中提到的爸爸节点和儿子节点二者之间的关系呢?
五、注入neo4jsession
(1)
package com.appleyk.config;
import org.neo4j.driver.v1.AuthTokens;
import org.neo4j.driver.v1.Driver;
import org.neo4j.driver.v1.GraphDatabase;
import org.neo4j.driver.v1.Session;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class Neo4jSourceConfig {
@Value("${spring.neo4j.url}")
private String url;
@Value("${spring.neo4j.username}")
private String username;
@Value("${spring.neo4j.password}")
private String password;
@Bean(name = "session")
public Session neo4jSession() {
Driver driver = GraphDatabase.driver(url, AuthTokens.basic(username, password));
return driver.session();
}
}
六、节点+关系实体构建
(1)基类RObject
package com.appleyk.node.base;
import java.util.HashMap;
import java.util.Map;
/**
* 节点和关系对象的基类,含公共部分的id、标签label以及属性properties
* @author yukun24@126.com
* @blob
* @date 2018年5月12日-上午9:55:19
*/
public class RObject {
/**
* 节点标签名称 == Node Labels
*/
private String label;
/**
* 节点属性键值对 == Property Keys
*/
private Map<String, Object> properties;
public RObject(){
properties = new HashMap<>();
}
public String getLabel() {
return label;
}
public void setLabel(String label) {
this.label = label;
}
public Map<String, Object> getProperties() {
return properties;
}
/**
* 添加属性
* @param key
* @param value
*/
public void addProperty(String key,Object value){
properties.put(key, value);
}
/**
* 拿到属性
* @param key
* @return
*/
public Object getProperty(String key){
return properties.get(key);
}
/**
* 移除属性
* @param key
*/
public void removeProperty(String key){
properties.remove(key);
}
public void setProperties(Map<String, Object> properties) {
this.properties = properties;
}
}
(2)节点子类RNode
package com.appleyk.node;
import com.appleyk.node.base.RObject;
import com.appleyk.node.relation.REdge;
public class RNode extends RObject{
/**
* 节点的uuid == 对应其他数据库中的主键
*/
private Long uuid;
/**
* 节点里面是否包含有边 == 关系
*/
private REdge edge;
public Long getUuid() {
return uuid;
}
public void setUuid(Long uuid) {
this.uuid = uuid;
}
public REdge getEdge() {
return edge;
}
public void setEdge(REdge edge) {
this.edge = edge;
}
}
(3)节点关系REdge
package com.appleyk.node.relation;
import com.appleyk.node.RNode;
import com.appleyk.node.base.RObject;
/**
* 边 == 关系
* @author yukun24@126.com
* @blob
* @date 2018年5月12日-上午9:54:55
*/
public class REdge extends RObject{
/**
* 关系的ID == 聚合、连接、属于、包括等,这些关系可能是枚举字典,因此记录关系ID是有必要的
*/
private Long relationID;
/**
* 关系名称
*/
private String name;
/**
* 关系指向哪一个节点 == 可能这个节点还有关系【节点关系递增下去】
*/
private RNode rNode;
public Long getRelationID() {
return relationID;
}
public void setRelationID(Long relationID) {
this.relationID = relationID;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public RNode getrNode() {
return rNode;
}
public void setrNode(RNode rNode) {
this.rNode = rNode;
}
}
七、删除所有关系等于父亲的节点【先清空数据】
match(n)-[r:父亲]-(b) delete n,r,b
八、单元测试
(1)
import org.junit.Test;
import org.junit.runner.RunWith;
import org.neo4j.driver.v1.Record;
import org.neo4j.driver.v1.Session;
import org.neo4j.driver.v1.StatementResult;
import org.neo4j.driver.v1.Value;
import org.neo4j.driver.v1.types.Node;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import com.appleyk.Application;
import com.appleyk.node.RNode;
import com.appleyk.node.relation.REdge;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.ObjectMapper;
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = Application.class)
public class Neo4jSessionTest {
final static ObjectMapper mapper = new ObjectMapper();
static {
/**
* 使用neo4j的session执行条件语句statement,一定要使用这个反序列化对象为json字符串
* 下面的设置的作用是,比如对象属性字段name="李二明",正常反序列化json为 == "name":"李二明"
* 如果使用下面的设置后,反序列name就是 == name:"appleyk"
* 而session执行语句create (:儿子{"name":"李二明","uuid":3330,"age":12,"height":"165cm"})会报错
* 因此,......etc
*/
mapper.configure(JsonGenerator.Feature.QUOTE_FIELD_NAMES, false);
}
@Autowired
private Session session;
@Test
public void connect() {
System.out.println("连接是否成功:" + session.isOpen());
}
@Test
public void createNodeAndRelation() throws Exception {
/**
* 建立一个爸爸与儿子的关系
*/
// 1.首先创建爸爸节点
RNode rDad = new RNode();
rDad.setUuid(1110L);
rDad.setLabel("爸爸");
// 2.其次为爸爸节点添加属性 == Property Keys
rDad.addProperty("uuid", 1110L);
rDad.addProperty("name", "李大明");
rDad.addProperty("age", 45);
rDad.addProperty("height", "182.5cm");
// 3.添加关系
REdge edge = new REdge();
edge.addProperty("relationID", 521L);
edge.addProperty("time", "1997-05-12");
edge.setRelationID(521L);
edge.setName("父亲");
// 4.为关系节点添加指向节点 == 创建儿子节点
RNode rSon = new RNode();
rSon.setUuid(2220L);
rSon.setLabel("儿子");
// 5.为儿子节点添加属性 == Property Keys
rSon.addProperty("uuid", 2220L);
rSon.addProperty("name", "李小明");
rSon.addProperty("age", 20);
rSon.addProperty("height", "185cm");
// 6.给爸爸节点添加关系
rDad.setEdge(edge);
createNode(rDad);
createNode(rSon);
createRelation(rDad,rSon);
System.err.println("创建成功");
}
/**
* 创建节点
* @param rNode
* @throws Exception
*/
public void createNode(RNode rNode) throws Exception{
RNode srcNode = queryNode(rNode);
//查node是否已經存在了,不存在則創建
if(srcNode == null){
String propertiesString = mapper.writeValueAsString(rNode.getProperties());
String cypherSql = String.format("create (:%s%s)", rNode.getLabel(), propertiesString);
System.out.println(cypherSql);
session.run(cypherSql);
System.err.println("创建节点:"+rNode.getLabel()+"成功!");
}else{
System.err.println("节点已存在,跳过创建");
}
}
/**
* 创建关系
* @param srcNode
* @param tarNode
* @throws Exception
*/
public void createRelation(RNode srcNode,RNode tarNode) throws Exception{
REdge edge = queryRelation(srcNode,tarNode);
if(edge == null){
edge = srcNode.getEdge();
String propertiesString = mapper.writeValueAsString(edge.getProperties());
String cypherSql = String.format("match(a),(b) where a.uuid=%d and b.uuid=%d create (a)-[r:%s %s]->(b)",
srcNode.getUuid(),tarNode.getUuid(),
edge.getName(), propertiesString);
System.out.println(cypherSql);
session.run(cypherSql);
System.err.println("创建关系:"+edge.getName()+"成功!");
}else{
System.err.println("关系已存在,跳过创建");
}
}
/**
* 查询节点
*
* @param rNode
* @return
*/
public RNode queryNode(RNode rNode) {
RNode node = null;
String cypherSql = String.format("match(n:%s) where n.uuid = %d return n", rNode.getLabel(), rNode.getUuid());
StatementResult result = session.run(cypherSql);
if (result.hasNext()) {
Record record = result.next();
for (Value value : record.values()) {
/**
* 结果里面只要类型为节点的值
*/
if (value.type().name().equals("NODE")) {
Node noe4jNode = value.asNode();
node = new RNode();
node.setLabel(rNode.getLabel());
node.setProperties(noe4jNode.asMap());
}
}
}
return node;
}
/**
* 查询关系
* @param rNode
* @return
*/
public REdge queryRelation(RNode srcNode,RNode tarNode){
REdge edge = srcNode.getEdge();
String cypherSql =String.format("match(n)-[r:%s]-(b) where n.uuid = %d and b.uuid = %d return r",
edge.getName(),srcNode.getUuid(),tarNode.getUuid());
StatementResult result = session.run(cypherSql);
if(result.hasNext()){
return edge;
}
return null;
}
}
(2)执行单元测试方法createNodeAndRelation,查看效果如下
查看图库如下
为了防止节点和关系的重复创建,测试demo中加了条件判断,创建节点的时候先查询节点是否存在,如果不存在才新建节点
创建关系的时候也一样,如果两个节点之间已经存在关系了,则不再创建
为了验证查询语句是否起作用,我们让上面的demo再执行一遍,查看下控制台输出如下
因为爸爸节点和儿子节点已经存在neo4j图库中了,且二者之间已经有了关系等于父亲的边,因此,这里我们不会再neo4j里重复再创建数据了
为了再次验证条件语句的作用,我决定给爸爸节点【李大明】再加一个关系节点,假设李大明还有个17岁的儿子李二明,于是,我们稍作修改如下
理想状态下,是节点爸爸李大明不会创建了,因为已经存在了,而节点李二明属于新增节点,因为其uuid我们重新指定了,而李大明和李二明之间的关系是不存在的,因此,再次执行方法,会看到如下的效果
无误后,我们去图库中查询已下验证一把
清除语句很简单,牢记n,r,b表达式就行了,执行效果如下
关于关系的探究,上述只是一个简单的案列,当然利用上述的实体类,完全可以构造出复杂关系网,比如家庭关系等,有兴趣的可以下载下面提供的项目github地址:
https://github.com/kobeyk/Spring-Boot-Neo4jAPI.git