使用
Spring Boot 集成 neo4j 主要使用 Spring Data Neo4j
添加依赖项和建立实体的映射请自己阅读文档相应部分,非常简单。
我这里只介绍 Spring Data Repositories 部分,也就是 jpa 和数据库交互的代码。
Spring Data Repositories
返回实体相关
这里日常的 crud 和 jpa 的使用差不多,包括分页等,但是注意如果直接使用 jpa 而不是 @Query 自定义查询语句的话,返回的实体中会包含关系映射对应的值,如果关系映射对应的值还有关系的话,都会返回,可能得到一堆不需要的值。例如:
@Node("Mathematician")
public class Mathematician {
@Id
private final Long mid;
@Property("name")
private final String name;
@Property("country")
private final String country;
@Property("title")
private final String title;
@Property("year")
private final Integer year;
@Property("institution")
private final String institution;
@Property("dissertation")
private final String dissertation;
@Property("classification")
private final Integer classification;
@Relationship(type = "advisorOf", direction = Relationship.Direction.OUTGOING)
private List<Mathematician> students = new ArrayList<>();
@Relationship(type = "studentOf", direction = Relationship.Direction.INCOMING)
private List<Mathematician> advisors = new ArrayList<>();
}
//第一种方法
Mathematician findMathematicianByMid(Long mid);
//第二种方法
@Query("match (m) where m.mid=$mid return m")
Mathematician findMathematicianByMid(Long mid);
第一个方法返回的值中students
和advisors
都会有符合关系的值,而 list 其中的Mathematician
又有这两个属性,会一层层都查询进去,可想而知如果这条路径够长会返回多少数据,所以非必要时不使用这种方法。
第二个方法返回的值中students
和advisors
都为空。
注意查询结果只能和数据库中节点映射的实体有关。查看支持的返回类型,也就是里面的 T 类型只能是节点映射的实体。
返回实体的部分属性
如果查询只想要实体的部分属性,也可以使用投影(上面实现第二种方法的效果应该也能用这种方法)。
我没用过,自己看文档吧。(~o ̄3 ̄)~
返回单列数据
还有一种情况如果返回单列数据如下图:
此时就算返回的数据不能映射到实体(使用一些聚合函数、转换成树之类的很常见),用List<Map<String, Object>>
也可以接受数据,但是后面对数据操作只能使用Map
的方法得到值。
一个要注意的地方
使用 @Query 自定义查询语句时,如果匹配节点的属性,不是在 where 中匹配而是在(m:Mathematician{mid:123})
中这样匹配的话,平常我们是使用$mid
匹配方法形参的,但是这里要用:#{literal(#mid)}
。
这个文档中@Query("MATCH (n:`:#{literal(#label)}`) RETURN n")
里面还用 `
包裹起来,但我自己用的时候去掉才能正常使用。
例子如下:(这个 depth 我不知道有没有必要也用这种格式,我是直接统一成一种格式了)
@Query("""
match (m:Mathematician{mid::#{literal(#mid)}})
call apoc.path.spanningTree(m, {
relationshipFilter: "studentOf>",
minLevel: 0,
maxLevel: :#{literal(#depth)}
})
yield path
with collect(path) as pathList
call apoc.convert.toTree(pathList)
yield value
return value
""")
List<Map<String, Object>> findAdvisorTreeByMid(Long mid, Long depth);
Neo4jClient
如果返回的是多列数据,又和实体对应不上去,这时候就要用 Neo4jClient,如下图:
拿自己的代码做例子:
@Repository
public class Neo4jClientRepository {
@Resource
Neo4jClient client;
@Resource
Neo4jMappingContext mappingContext;
@Resource
MathematicianMapper mathematicianMapper;
public Optional<MathematicianVO> findByMid(Long mid) {
BiFunction<TypeSystem, MapAccessor, Mathematician> mappingFunction = mappingContext.getRequiredMappingFunctionFor(Mathematician.class);
return client
.query("""
match (n{mid:$mid})
with n
optional match (n)-[:advisorOf]->(s)
optional match (n)-[:studentOf]->(a)
with n as person, collect(distinct {mid:s.mid,name:s.name}) as students, collect(distinct {mid:a.mid,name:a.name}) as advisors
return person,
case
when apoc.coll.contains(students,{name:NULL,mid:NULL}) then []
else students
end as students,
case
when apoc.coll.contains(advisors,{name:NULL,mid:NULL}) then []
else advisors
end as advisors
""")
.bind(mid).to("mid")
.fetchAs(MathematicianVO.class)
.mappedBy((TypeSystem t, Record record) -> {
MathematicianVO person = mathematicianMapper.toMathematicianVO(mappingFunction.apply(t, record.get("person").asNode()));
person.setAdvisors(record.get("advisors").asList());
person.setStudents(record.get("students").asList());
return person;
}).first();
}
public Collection<Map<String, Object>> getCountryCount() {
return client
.query("""
match (r)
return r.country as country, count(*) as num
order by num desc
limit 25
""")
.fetch()
.all();
}
}
第一个方法findByMid
query
中为查询语句。bind
和to
即在查询中用bind
中的值替换掉to
中的值,to
中的值对应在query
中是$mid
的格式。fetchAs
表示返回结果要映射成的类型,与后面的mappedBy
配套使用,如果这里是fetch()
就表示返回数据为默认的Map<String, Object>
类型,也就不需要mappedBy
了。mappedBy
中是将返回数据映射为fetchAs
中类型的过程。Record
相当于返回数据的所有行,可以通过record.get(列名)
得到对应列的值,然后通过as...
转换成对应的格式,as...
方法中可以选择添加一个参数作为值为 null 时的默认值。我这里advisors
和students
因为是collect
聚合的,所以转换成 list。asList
方法中也能依次选取然后映射到一个实体上,如下图的代码最后返回的是List<Rinking>
:record.get("students").asList(v -> new Ranking( v.get("mid").asLong(-1), v.get("name").asString(""), v.get("classificationId").asInt(-1), v.get("descendants").asInt(0)))
TypeSystem
说是此驱动程序可以处理的所有数据库类型的列表,具体我也不知道是怎么用的,平常用可能就是个固定的模板。- 如果在这个过程中有节点到实体的映射的话,提供了一个简便的方法能够直接转换。先用实体类型定义一个
BiFunction
,然后使用BiFunction.apply
就能够直接从数据中映射为实体。这里节点到实体的映射,显然record.get("person")
是一个节点,所以用asNode
。 - 最后
return
返回一个fetchAs
中类型的对象。
最后有
one
、first
和all
三个方法,决定返回数据的条数。- 确定只有一条就用
one
,如果多于一条会抛出异常,返回值用Optional<T>
包裹。 first
顾名思义,取第一条,没有的话就返回空的Optional<T>
。all
就是所有行,存放在Collection<T>
中。
- 确定只有一条就用
第二个方法getCountryCount
感觉第二个方法也不用讲了吧,上面一个已经讲的够清楚了。o( ̄▽ ̄)ブ
实际还有别的用法,可以再自己学习官方文档。Neo4jClient 官方文档