Java如何去解析Neo4j返回的Record

前言

Neo4j是一个可以构建节点关系的非关系型的图数据库,通过使用Neo4j存储数据,我们可以很好地实现一些节点关系数据,比如物流中的运输路线等。

对于第一次使用Neo4j的朋友来说,解析查询记录是很痛苦的,本文记录了几种常用的处理查询结果的方法,以便不时之需。

操作

如果查询返回的是m,r,n

正如下述Neo4j查询语句,返回的记录是两个节点的信息及其关系:

1
MATCH (n) -[r]- (m) RETURN n,r,m

我们可以这样去解析:

1
2
3
4
5
6
// 获取节点n的数据
Node n = record.get("n").asNode();
// 获取节点m的数据
Node m = record.get("m").asNode();
// 获取关系r的数据
Relationship relationship = record.get("r").asRelationship();

注意:上述的r,n,m要与cypher语句中的对应

如果返回的是一条路径 path = (xxx)

在我们物流的路线规划业务中,往往会有最短距离路线和最低成本路线的选取,而这是返回的路线数据是比较复杂的,正如下面 cypher 查询:

1
2
3
MATCH path = shortestPath((n:n_Label) -[*..{}]- (m:m_Label))
WHERE n.bid = 10001 AND m.bid = 10002
RETURN path

此时我们获取到的 record 节点n节点m 的整条路径的值,包括一个开始节点(n)、结束节点(m)。

以及从开始节点到结束节点之间经过的每两个节点间的片段segements:

segments:[{startNode, relationship, endNode},{xxx},{xxx}]

此时,如果我们想要获取record中的某一条线路的值,我们可以通过 record.get(x) 来获取(注意:x为第x条线路,因为我们这里的最短线路只有一条线路,所以 record.get(0) 即可以获取到),这里获取到的就是我们的path值,是Value接口类型的。

record.get(0) 的结果:

image-20230822200019573

Value接口类型是一个类似于Map的接口,这里我们不好处理,所以我们要将其转换为PathValue(PathValue是Value类型的一个实现类可以获取到Path,Path类型处理结果方便)。

1
2
3
4
5
6
7
8
9
// 将第一条记录格式化成path-> path
PathValue pathValue = (PathValue) record.get(0);
/**
* path[nodes, relationship, segment]
* nodes: 所有节点
* relationship: 节点间的关系
* segment: 路线的轨迹片段
*/
Path path = pathValue.asPath();

转换成path之后,我们可以看到path里面的值

image-20230822200125781

在这里,我们可以查看一下path源码,看一下nodes、relationships、segments的值分别是什么

image-20230822200148765

通过上述代码以及注释我们可以发现:

  • nodes是Path类帮我们实现的一个迭代器,里面包含了整条路径涉及到的所有的node

  • relationships同上,里面封装的是整条路线涉及到的节点之间的关系

  • segment是一个接口,segment其实就是我们某段关系的记录,包括开始节点、关系、接收节点,也就是对应的 n-r-m

    注意:segment是某一段关系,而一个节点到另外一个节点的中间,可能会有多段关系

上述cypher语句的查询路径如下图所示

注意:因为 shortestPath() 是有方向性的,所以这里只能查询到从a到b的结点

如果要查询a到e之间的所有路线,我们把shortestPath去掉即可,但是一定要加排序并加limit加以限制,不然会造成OOM。

image-20230822200229541

好的,言归正传,如果我们需要获取record中的node进行数据处理,只需要调用 path.nodes() 遍历即可

1
2
3
4
5
6
7
8
9
10
11
12
13
// 这里我用的是Stream来处理数据
List<OrganDTO> organDTOList = StreamUtil.of(path.nodes()).map(
node -> {
Map<String, Object> map = node.asMap();
OrganDTO organDTO = BeanUtil.toBean(map, OrganDTO.class);
OrganTypeEnum organTypeEnum = OrganTypeEnum.valueOf(CollUtil.getFirst(node.labels()));
organDTO.setType(organTypeEnum.getCode());
Object location = map.get("location");
organDTO.setLatitude(BeanUtil.getProperty(location, "y"));
organDTO.setLongitude(BeanUtil.getProperty(location, "x"));

return organDTO;
}

上面代码的执行流程:

  1. nodes 转换为Stream流,用于遍历处理node

  2. 因为node是一个接口并且其结构与Map类似,也是K-V键值对,所以我们在这里可以直接调用它的asMap 方法将其转成map集合,用于后续数据处理

    Node接口的类图:

    image-20230822203138378

    我们可以查看MapAcessor接口,看里面的方法

    image-20230822203236912

  3. 将Node转成Map之后,我们就可以用工具类对我们需要的实体类进行封装。

同理,relationship 的获取也是差不多。

1
2
3
4
5
6
// 这里获取到的是一个迭代器,我们可以直接使用Stream遍历
path.relationships();
double cost = StreamUtil.of(path.relationships()).mapToDouble(relationship -> {
Map<String, Object> map = relationship.asMap();
return Convert.toDouble(map.get("cost"), 0D);
}).sum();

上面获取cost的代码,执行流程是这样的

  1. 通过 StreamUtil.of(path.relationships()) 将relationships转换为流。

  2. 使用 mapToDouble 对Stream流做一个映射,映射的结果返回为double类型

  3. 然后再映射过程中,将 relationship 转换成map,因为relationship是一个k-v键值对。

    relationship.asMap(); 将relationship转为map

    image-20230822200316496

  4. 然后获取 cost 属性,并且进行一个求和处理

    image-20230822200333102

如果返回的是一个统计结果

当然,如果我们对结果进行统计,这时想要获取record中的统计结果的话,我们直接获取即可。

1
2
3
4
// 统计从n到m有多少条线路
MATCH path = (n:n_LabelType)-[r]-(m:m_LabelType)
WHERE n.bid =$startId AND m.bid = $endId
RETURN count(r) AS c

比如上述cypher查询中,我们要获取c的值,直接get即可。

1
Long result = Convert.toLong(record.get(0))