博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
Neo4j Spring动态起始节点类型实现
阅读量:4197 次
发布时间:2019-05-26

本文共 8863 字,大约阅读时间需要 29 分钟。

Neo4j Spring Data动态起始节点类型实现


在spring-boot里我们可以基于neo4j-spring-data,编写类似关系数据库风格的JPA操作,实现对neo4j数据的CURD。研究neo4j-spring-data底层API的实现发现,本质上还是将JPA的操作翻译为cypher的语句,并基于neo4j的bolt的协议将操作语句发送至远程的数据库服务器来执行,anyway,这块我们不进一步展开了,我们来看看怎么利用neo4j-spring-data的API高效地实现工程化模型。

neo4j-spring-data创建节点和关系

我们利用Neo4j建模的时候,经常会发现,同样的Relationship里,可能连接的节点是不一样的,比如OWN(拥有)这样的关系,比如:

用户拥有手机:User-[OWN]->Phone
用户拥有银行卡:User-[OWN]->Card

在neo4j-spring-data中,主要有2种形式来创建节点和节点之间的关系。

基于NodeEntity

这里我们简单举一个简单例子:User-[OWN]->Phone

1. 定义好@NodeEntity,任何被@NodeEntity的POJO将会被作为neo4j的一个节点类型处理;
2. 定义好对应的Repository
模型定义:

//所有关系标签常量//这里使用了interface成员变量是常量的特性,比class使用static final String XX="xx"简洁 public interface RelsType {
String OWN = "OWN"; String HAVE = "HAVE";}@Data@NodeEntitypublic class User {
@Id @GeneratedValue private Long id; private String name; /** * 一对多的关系 */ @Relationship(type = RelsType.OWN, direction = Relationship.OUTGOING) private List
phones;}@Data@NodeEntitypublic class Phone {
@Id @GeneratedValue private Long id; private String phoneNo; /** * 一对多的关系 */ @Relationship(type = RelsType.OWN, direction = Relationship.INCOMING) private List
users;}

定义好对应的Repository:

public interface UserRepository extends Neo4jRepository:
{
}

注意:从4.2版本之后,API做了修改GraphRepository改为Neo4jRepository,参数也不一样,如果看到之前的博客的代码,请注意修改。

Base class for repositories GraphRepository has been renamed Neo4jRepository and parameter types change from <T> to<T, ID>

总结要点:

  • POJO中的属性将作为Node的properties
  • @Relationship注解指定与其他节点的关系,指定关系的direction方向,可以是一对一一对多(List<>)
  • @Id注解的id是对应Neo4j数据的<id>

测试代码:

@RunWith(SpringRunner.class)@SpringBootTestpublic class Neo4jSpringDataTest {
@Autowired private UserRepository userRepository; @Test public void userOwnPhoneTest() throws Exception { Phone phone = new Phone(); phone.setPhoneNo("13800123456"); User user = new User(); user.setName("Jack"); user.setPhones(Lists.newArrayList(phone)); userRepository.save(user); }}

运行一切正常的话,数据库将新建User-[OWN]->Phone:2个节点和1个关系。

思考:

走到这里我们可能觉得一切都正常,按照neo4j-spring-data的API来完成节点和关系的创建也比较简洁。等等,让我们思考一下可能遇到的问题:

如果User不止和PhoneOWN连接关系,还跟其他节点有其他关系,那么就需要在User的POJO里新加其他节点关系的代码,整个POJO就会越来越膨胀,这样代码将越来越难维护和理解;

比如,可能会出现下面类似臃肿的代码:

@Data@NodeEntitypublic class User {
@Id @GeneratedValue private Long id; private String name; @Relationship(type = RelsType.OWN, direction = Relationship.OUTGOING) private List
phones; @Relationship(type = RelsType.HAVE, direction = Relationship.OUTGOING) private List
cars; @Relationship(type = RelsType.BUY, direction = Relationship.OUTGOING) private List
houses; //其他关系 ...}

也就是说,基于@NodeEntity及内置@Relationship的实现方式,在遇到可能存在多关系的场景,工程实现可能会不那么优雅和易维护。

基于RelationshipEntity

为了测试代码,我们新举Customer-[HAVE]->Car这个例子,neo4j-spring-data为了支持多个角度构建关系的灵活性出发,即从关系的角度出发,也能完成节点和关系的构建,这个机制就是RelationshipEntity

模型定义:

@Datapublic class Customer {
@Id @GeneratedValue private Long id; private String name;}@Datapublic class Car {
@Id @GeneratedValue private Long id; private String brand;}@Data@RelationshipEntity(type = RelsType.HAVE)public class Have {
@Id @GeneratedValue private Long id; @Property private String createTime; @StartNode private Customer customer; @EndNode private Car car;}

定义好对应的Repository:

public interface HaveRepository extends Neo4jRepository
{
}

测试代码:

@RunWith(SpringRunner.class)@SpringBootTestpublic class HaveTest {
@Autowired private HaveRepository haveRepository; @Test public void customerHaveCarTest() throws Exception { Customer customer = new Customer(); customer.setName("Tom"); Car car = new Car(); car.setBrand("BMW"); Have have = new Have(); have.setCustomer(customer); have.setCar(car); have.setCreateTime(LocalDateTime.now().toString()); haveRepository.save(have); }}

总结要点:

  • @RelationshipEntity注解的POJO里有2个关键的内部field的注解:@StartNode和表示关系的起始节点,@EndNode表示关系的结束节点,即起始节点、结束节点和关系类型构建完整构建一个关系,这里只支持一对一的情况;
  • @Property注解的成员变量会被作为关系的属性处理;
  • @StartNode@EndNode注解的成员变量可以是没有被@NodeEntity注解的POJO,但是一定要有指定@Id,否则会报错
  • 很明显,如果要完成一对多关系的构建,需要多次执行上面关系构建的操作(不过这个不是大问题);

思考:

基于@RelationshipEntity节点关系构建需要显示指明对应的起始节点的类型,如果需要构建的模型中存在大量不同的关系,那么将要实现大量类似HaveHaveRepository的代码。这在工程上也是不现实的实现,那么我们是否可以使用动态节点类型来作为起始节点和结束节点呢?答案是可以的!需要我们动手封装一下实现。

基于RelationshipEntity的动态节点类型

从上面2种实现节点关系构建的DEMO中我们可以发现,当我们的关系类型和节点类型越来越多时,需要编写和维护的代码也越来越多,我们需要控制这种复杂性。

一切只有Relationship

我们将图构建的时候从关系类型的角度去抽象,因为关系类型是可以枚举的,不管开始节点或者结束节点是什么类型的节点,他们之间只通过关系类型建立关系,比如:

  • 用户拥有手机:User-[OWN]->Phone
  • 用户拥有银行卡:User-[OWN]->Card
  • 用户拥有车子:User-[OWN]->Car

那么这里只需要对OWN建模即可,开始节点和结束节点的类型可动态变化,那么我们的建模系统数量就可以维持在关系类型的水平上,节点类型可以灵活扩展。

*思考

建模的思路有了,那怎么实现动态节点类型呢?用Java泛型啊!思路是对的,让我们试试吧!

测试代码:

//起始节点和结束节点都为动态类型的泛型实现@Data@RelationshipEntity(type = RelsType.HAVE)public class HaveDynamic
{
@Id @GeneratedValue private Long id; @Property private String createTime; @StartNode private S startNode; @EndNode private E endNode;}//实现对应的Repositorypublic interface HaveDynamicRepository extends Neo4jRepository
{
}//测试代码:@RunWith(SpringRunner.class)@SpringBootTestpublic class HaveDynamicTest {
@Autowired private HaveDynamicRepository haveDynamicRepository; @Test public void dynamicTest() throws Exception { Customer customer = new Customer(); customer.setName("John"); Car car = new Car(); car.setBrand("Benz"); HaveDynamic
haveDynamic = new HaveDynamic<>(); haveDynamic.setCreateTime(LocalDateTime.now().toString()); haveDynamic.setStartNode(customer); haveDynamic.setEndNode(car); haveDynamicRepository.save(haveDynamic); }}

很不幸的,Spring-Boot启动注入bean的阶段就失败了,报错信息如下:

...省略Caused by: org.springframework.beans.BeanInstantiationException: Failed to instantiate [org.neo4j.ogm.session.SessionFactory]: Factory method 'sessionFactory' threw exception; nested exception is java.lang.NullPointerException    at org.springframework.beans.factory.support.SimpleInstantiationStrategy.instantiate(SimpleInstantiationStrategy.java:185)    at org.springframework.beans.factory.support.ConstructorResolver.instantiateUsingFactoryMethod(ConstructorResolver.java:579)    ... 55 moreCaused by: java.lang.NullPointerException    at org.neo4j.ogm.metadata.schema.DomainInfoSchemaBuilder.getNodeByTypeDescriptor(DomainInfoSchemaBuilder.java:145)    at org.neo4j.ogm.metadata.schema.DomainInfoSchemaBuilder.getNodeByFieldAndContainingClass(DomainInfoSchemaBuilder.java:139)...省略

很明显的是,因为DomainInfoSchemaBuilder.getNodeByTypeDescriptor找不到HaveDynamic的节点类型,因此在初始化org.neo4j.ogm.session.SessionFactory失败了,也就是说@RelationshipEntity里指定@StartNode@EndNode的节点类型需要是明确的类型,否则无法解析。

深入分析:

(1)在neo4j-spring-data里,java class定义的类型会被解析为对应Neo4j里节点的label,而Neo4j的Node是支持多层label的;

(2)如果java class定义的节点类型存在多重继承,通过neo4j-spring-data保存在数据库时会被解析为存在多个label的node;
(3)泛型的实现在Java编译的时候会发生类型的擦除(默认会擦除为Object),我们可以通过< Y extends X>通配符限制的方式来控制类型擦除后为X类型的泛型保证;

最终解决方案:

  • 设计可被DomainInfoSchemaBuilder.getNodeByTypeDescriptor解析的基础类型;
  • 基础类型作为所有动态节点类型的基类;

最后代码改写如下:

/** * 基础节点类型 */@Datapublic class BaseNode {
@Id @GeneratedValue private Long id;}/** * 基础关系类型 */@Datapublic class BaseRel {
@Id @GeneratedValue private Long id;}@Data@EqualsAndHashCode(callSuper = false)public class Customer extends BaseNode {
private String name;}@Data@EqualsAndHashCode(callSuper = false)public class Car extends BaseNode {
private String brand;}@Data@EqualsAndHashCode(callSuper = false)@RelationshipEntity(type = RelsType.HAVE)public class HaveDynamic extends BaseRel {
@Property private String createTime; @StartNode private S startNode; @EndNode private E endNode;}@RunWith(SpringRunner.class)@SpringBootTestpublic class HaveDynamicTest {
@Autowired private HaveDynamicRepository haveDynamicRepository; @Test public void dynamicTest() throws Exception { Customer customer = new Customer(); customer.setName("John"); Car car = new Car(); car.setBrand("Benz"); HaveDynamic
haveDynamic = new HaveDynamic<>(); haveDynamic.setCreateTime(LocalDateTime.now().toString()); haveDynamic.setStartNode(customer); haveDynamic.setEndNode(car); haveDynamicRepository.save(haveDynamic); }}

HaveDynamic实现支持任何基于BaseNode的开始和结束节点类型,实现动态的构建节点和关系模型,对于同一关系类型,只需要编写一类关系构建的代码即可。

提示: 以上动态起始节点类型的实现比较适合OLTP的系统上存在复杂的节点和关系需要做CURD的时候,能够改善整个模型工程的复杂度。但是,在做类似ETL大批量数据插入的场景是不合适的,如果存在大批量数据插入来完成建模的情况,建议优先使用cypher schema来处理,并通过cypher-shell来完成导入,性能会更佳。

以上实现的项目源码地址:

提示:

  • 代码使用了lombok插件来完成代码的getter和setter,需要在IDE安装对应的lombok插件和开启Annotation处理才能跑测试;
  • 测试时,请记得修改application.properties里的neo4j远程服务器上的ip,用户名和密码;

转载地址:http://kgwli.baihongyu.com/

你可能感兴趣的文章
Observer模式
查看>>
高性能服务器设计
查看>>
图文介绍openLDAP在windows上的安装配置
查看>>
Pentaho BI开源报表系统
查看>>
Pentaho 开发: 在eclipse中构建Pentaho BI Server工程
查看>>
android中使用TextView来显示某个网址的内容,使用<ScrollView>来生成下拉列表框
查看>>
andorid里关于wifi的分析
查看>>
Spring MVC 教程,快速入门,深入分析
查看>>
Ubuntu Navicat for MySQL安装以及破解方案
查看>>
在C++中如何实现模板函数的外部调用
查看>>
HTML5学习之——HTML 5 应用程序缓存
查看>>
HTML5学习之——HTML 5 服务器发送事件
查看>>
hbase shell出现ERROR: org.apache.hadoop.hbase.ipc.ServerNotRunningYetException
查看>>
解决Rhythmbox乱码
查看>>
豆瓣爱问共享资料插件发布啦
查看>>
kermit的安装和配置
查看>>
linux中cat命令使用详解
查看>>
java中的异常机制
查看>>
商务智能-基本方法-数据钻取
查看>>
openstack-instance-high-availability-Evacuate
查看>>