前言
项目框架
我的项目采用的是SpringBoot集成SpringDataNeo4j和MyBatisPlus的框架,接口测试使用Swagger,MyBatisPlus当时是搭建项目想尝试配两种数据库做的测试,目前还并没有用到.但是看了一些参考资料,大家都比较建议将不需要在Neo4j中查询时使用的的属性放到MySQL等其他数据库中,这个工作可能日后再优化吧.
版本说明:
- SpringBoot 2.1.6
- SpringDataNeo4j 5.1.9
- 本机安装的Neo4j 3.5.6
项目配置(依赖)
依赖添加和项目配置很多教程都有,我就不赘述了.值得一提的是,我当时配置项目用到了这个依赖,jdbc的驱动,如果不加这个,当时项目启动时候会报错,因为在配置类中有一个bean通过这种连接方式来启动并获取SessionFactory的.
<dependency>
<groupId>org.neo4j</groupId>
<artifactId>neo4j-jdbc-driver</artifactId>
<version>3.4.0</version>
</dependency>
另外,neo4j默认的依赖是使用http连接方式的,要是用bolt的连接方式的话,需要添加bolt驱动,也就是下面这个依赖
<dependency>
<groupId>org.neo4j</groupId>
<artifactId>neo4j-ogm-bolt-driver</artifactId>
<version>3.1.12</version>
</dependency>
配置类
这里再贴一下我自己的Neo4jConfig,下文中有地方会用到.
可以看到我创建了2个bean,一个是连接Neo4j数据库的,返回一个SessionFactory,一个是Neo4j事务的
package com.jytd.sdntest.common.config;
import org.neo4j.ogm.session.Session;
import org.neo4j.ogm.session.SessionFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.neo4j.repository.config.EnableNeo4jRepositories;
import org.springframework.data.neo4j.transaction.Neo4jTransactionManager;
import org.springframework.data.neo4j.transaction.SessionFactoryUtils;
import org.springframework.transaction.annotation.EnableTransactionManagement;
@Configuration
@EnableNeo4jRepositories(basePackages = "com.jytd.sdntest.sdn.repository")
@EnableTransactionManagement
public class Neo4jConfig{
@Value("${spring.data.neo4j.uri}")
private String databaseUrl;
@Value("${spring.data.neo4j.username}")
private String userName;
@Value("${spring.data.neo4j.password}")
private String password;
@Bean
public SessionFactory sessionFactory() {
org.neo4j.ogm.config.Configuration configuration = new org.neo4j.ogm.config.Configuration.Builder()
.uri(databaseUrl)
.credentials(userName, password)
.build();
SessionFactory sessionFactory=new SessionFactory(configuration, "com.jytd.sdntest.sdn");
return sessionFactory;
}
@Bean
public Neo4jTransactionManager transactionManager() {
return new Neo4jTransactionManager(sessionFactory());
}
}
分页查询
官方文档及实现方式
由于一开始尝试着写项目,对于分页都是直接skip然后limit的,但是返回结果只有数据的集合,不包含分页的属性,所以正好那天因为一些问题翻看官方说明文档,看到了分页查询的使用方法.官方文档地址在文章最后的参考资料里贴出.这里稍微写一下
下面代码复制自官方文档
// returns a Page of Actors that have a ACTS_IN relationship to the movie node
// with the title equal to movieTitle parameter with an accurate total count
@Query(value = "MATCH (movie:Movie {title={0}})<-[:ACTS_IN]-(actor) RETURN actor",
countQuery = "MATCH (movie:Movie {title={0}})<-[:ACTS_IN]-(actor) RETURN count(*)")
Page<Actor> getActorsThatActInMovieFromTitle(String movieTitle, Pageable page);
我按照这种写法自己也尝试写了一个,value
属性是要查询的cypher,无须写SKIP
和LIMIT
,countQuery
属性是查询总数的查询语句,语句是一样的,返回结果是count()
而已
//根据学生ID查询学生-实验,分页
/*@Query(value = "MATCH (s:Student)-[r:StudentExperiment]->(e:Experiment) WHERE id(s)=:#{#student.id} RETURN e AS experimentNode",
countQuery = "MATCH (s:Student)-[r:StudentExperiment]->(e:Experiment) WHERE id(s)=:#{#student.id} RETURN count(e)")*/
@Query(value = "MATCH (s:Student)-[r:StudentExperiment]->(e:Experiment) WHERE id(s)={id} RETURN r",
countQuery = "MATCH (s:Student)-[r:StudentExperiment]->(e:Experiment) WHERE id(s)={id} RETURN count(r)")
Page<StudentExperimentRel> queryExperimentByStudentId(@Param("id") Long id, Pageable pageable);
参数说明
从上面的例子可以看到,分页查询的参数是查询条件和一个Pageable,即分页参数,查询参数没什么好说的,需要几个传几个就是了.
Pageable的话就是分页参数,需要传的属性是page,size,sort
.之前没用过SpringData,所以也没用过Pageable这个接口.看了一下它的实现类常用的是PageRequest
所以,用如下方法创建一个PageRequest对象.
Pageable pageable=PageRequest.of(页码,每页条数,排序方式(可不要));
特别提醒:这个分页的页码是 从0开始的!从0开始的!从0开始的! 之前用MyBatisPlus都是从1开始的,计算起始也是(page-1)*limit这么算的,所以我之前page传的1说什么查不出数据!困扰好久!
service层的写法:
@Override
public Page<StudentExperimentRel> queryExperimentByStudentId(StudentQo studentQo) {
Pageable pageable=PageRequest.of(studentQo.getPage(),studentQo.getSize());
Page<StudentExperimentRel> relationPage=experimentRepository.queryExperimentByStudentId(studentQo.getId(),pageable);
return relationPage;
}
返回值说明
按如上方式查询之后,返回的是一个Page接口,这个接口的实现类是PageImpl,下面自定义复杂查询会用到.我们来看一下这个接口返回的对象属性,分析一下吧!因为是从controller的返回值复制过来的,所以以下内容是Json数据
{
"status": "OK",
"code": 0,
"message": "成功响应",
"data": {
"content": [//content这个key,对应的就是查询结果,是个数组
{
"id": 337,//这里每一个对象都是我的一个RelationshipEntity,并且包含开始节点和结束节点
"startId": null,
"endId": null,
"score": null,
"code": null,
"result": null,
"graph": null,
"submitTime": null,
"scoreTime": null,
"isFinished": null,
"studentNode": {//开始节点
"id": 159,
"name": "张三",
"gender": null,
"profession": null,
"username": null,
"password": null,
"createTime": null,
"state": null,
"courseRel": []
},
"experimentNode": {//结束节点
"id": 276,
"name": "实验04",
"purpose": null,
"content": null,
"procedure": null,
"courseId": null
}
},
{
"id": 335,
"startId": null,
"endId": null,
"score": null,
"code": "修改测试",
"result": null,
"graph": null,
"submitTime": null,
"scoreTime": null,
"isFinished": null,
"studentNode": {
"id": 159,
"name": "张三",
"gender": null,
"profession": null,
"username": null,
"password": null,
"createTime": null,
"state": null,
"courseRel": []
},
"experimentNode": {
"id": 309,
"name": "实验添加测试",
"purpose": null,
"content": null,
"procedure": null,
"courseId": null
}
}
],
"pageable": { //这个pageable就是我查询时候的那个参数
"sort": {//排序条件,如果参数传了分页相关的,应该会不一样
"sorted": false,
"unsorted": true,
"empty": true
},
"offset": 0,
"pageNumber": 0,//这个就是页码,从0开始
"pageSize": 5,//这是每页记录条数
"unpaged": false,
"paged": true
},
//这里往下就是Page这个接口所含的分页属性,前端取值取这个应该更方便
"totalElements": 2,//总记录条数
"totalPages": 1,//总页数
"last": true,
"number": 0,//这里跟上面pageable里面是一样的,页码
"size": 5,//每页记录条数
"sort": {
"sorted": false,
"unsorted": true,
"empty": true
},
"numberOfElements": 2,
"first": true,
"empty": false
},
"date": 1566197748209
}
存在的问题
存在的问题其实就是参数的问题,可以翻看我前面的博客,在@Query
里面可以使用:#{#对象名.属性名}
的方式来传一个对象类型的参数,用这种写法获取到对象中的属性作为参数,但是这种写法在@Query(value="xxxxx")
里面可以用,但是在countQuery
中,这种写法是不行的,之前因为页码问题尝试debug了一次源码,在第一句中,这种写法就被编译为了{str_exp1}
大概这样的,具体啥样记不清了.但第二句的时候不编译!!!仍然是#{#对象名.属性名}
!特别坑!所以当使用这种方式分页查询时,只能一个一个的传参数进去,用最简单的{参数名}
来匹配参数
拼接CQL实现复杂查询
在Repository中使用@Query
来进行Cyther查询虽然方便,但是不能像MyBatis一样作条件判断,SpringDataJPA中是可以的,它在@Query
中可以使用if()
,同时还有一个nativeQuery = true
的属性,SpringDataNeo4j没有…所以当复杂的需要根据条件拼接Cypher语句的情况下,我们只能自己拼接再使用其他方法来将语句进行执行.
Session方法实现
SpringDataNeo4j的session中有save,delete,query
等很多方法,具体可以自行查看Session接口中定义的方法!
所以首要要解决的问题,只要解决如何获取到这个Session就可以了.
之前参考了一个朋友的博客( 链接在这里)他用的是这种方式:
Session session = SessionFactoryUtils.getSession(sessionFactory);
这种方式没问题,官方API文档也写了这些方法.可是我用这种方式,获取到的session一直是null,看了源码也没看出是为啥,我觉得可能跟spring自带的事务管理器有关,不知如何解决就算了.
使用了SessionFactory来获取session,由于在前面的Neo4jConfig里添加了一个SessionFactory的bean,所以在service等其他地方直接@Autowired
即可,然后调用openSession()
方法即可获取Session.
@Autowired
private SessionFactory sessionFactory;
//在方法里直接获取即可
Session session=sessionFactory.openSession();
下面贴一下我Service的代码,根据controller的参数进行拼接Cypher并执行.(不相关的方法删掉了)
package com.jytd.sdntest.ssm.node.service.impl;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.jytd.sdntest.common.utils.Util;
import com.jytd.sdntest.sdn.node.domain.ExperimentNode;
import com.jytd.sdntest.sdn.node.domain.StudentNode;
import com.jytd.sdntest.sdn.node.model.ExperimentQo;
import com.jytd.sdntest.sdn.node.model.StudentQo;
import com.jytd.sdntest.sdn.node.repository.ExperimentRepository;
import com.jytd.sdntest.sdn.relation.domain.StudentExperimentRel;
import com.jytd.sdntest.ssm.node.service.ExperimentService;
import org.neo4j.ogm.driver.Driver;
import org.neo4j.ogm.model.Result;
import org.neo4j.ogm.session.Session;
import org.neo4j.ogm.session.SessionFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.neo4j.transaction.SessionFactoryUtils;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Map;
import java.util.Set;
@Service
public class ExperimentServiceImpl implements ExperimentService {
@Autowired
private SessionFactory sessionFactory;
@Override
public Page<ExperimentNode> queryExperimentByParam(ExperimentQo experimentQo) {
StringBuffer cql=new StringBuffer();
//拼接基本语句
cql.append("MATCH (n:Experiment) WHERE 1=1");
//按名称模糊查询
if(Util.isNotEmpty(experimentQo.getName())){
cql.append(" AND n.name=~'.*"+experimentQo.getName()+".*'");
}
//按学科查询
if(Util.isNotEmpty(experimentQo.getSubject())){
cql.append(" AND n.subject='"+experimentQo.getSubject()+"'");
}
//按分类查询
if(Util.isNotEmpty(experimentQo.getSort())){
cql.append(" AND n.sort='"+experimentQo.getSort()+"'");
}
//按难易程度查询
if(Util.isNotEmpty(experimentQo.getDifficulty())){
cql.append(" AND n.difficulty="+experimentQo.getDifficulty());
}
//按是否发布查询
if(Util.isNotEmpty(experimentQo.getIsPublished())){
cql.append(" AND n.isPublished="+experimentQo.getIsPublished());
}
//拼接返回值和Limit部分
int skip=experimentQo.getPage()*experimentQo.getSize();
String queryEnd=" RETURN n SKIP "+skip+" LIMIT "+experimentQo.getSize();
String countEnd=" RETURN count(n)";
//获取session
Session session=sessionFactory.openSession();
//分页数据查询结果
Iterable<ExperimentNode> experimentNodes=session.query(ExperimentNode.class,cql.toString()+queryEnd,Maps.newHashMap());
//查询总数
Integer total=session.queryForObject(Integer.class,cql.toString()+countEnd, Maps.newHashMap());
//封装为分页类
List<ExperimentNode> content= Lists.newArrayList(experimentNodes);
Pageable pageable=PageRequest.of(experimentQo.getPage(),experimentQo.getSize());
Page<ExperimentNode> page=new PageImpl<ExperimentNode>(content,pageable,total);
return page;
}
}
这里返回分页类是用的Page的实现类PageImpl的构造方法,将查询结果,分页,总数作为参数即可.
APOC实现
APOC安装
-
根据Neo4j版本下载对应的APOC的jar包,
地址在这里: https://github.com/neo4j-contrib/neo4j-apoc-procedures/releases
我的版本是3.5.6所以我下的是3.5.X -
将jar包放到Neo4j目录下的plugins目录下
-
修改conf/neo4j.conf的配置(可选)
dbms.security.procedures.unrestricted=apoc.*
#增加页缓存到至少4G,推荐20G:
dbms.memory.pagecache.size=4g
#JVM堆保存留内存从1G起,最大4G:
dbms.memory.heap.initial_size=1g
dbms.memory.heap.max_size=4G
- 重新启动neo4j服务,在7474的web页面执行
RETURN apoc.version();
如果返回版本号说明安装成功
调用存储过程
配置成功之后,我们就可以在项目中的Repository调用存储过程了.写法很简单
首先按上面的方法拼接一个CQL,作为参数传到Repository中
然后定义一个这样的接口方法,将cql作为从参数传入,调用apoc.cypher.run()
@Query("call apoc.cypher.run({cql}, null) yield value return value.n")
Set<TeamNode> teamTest(@Param("cql") String cql);
就可以查到返回结果了.
这里需要注意的是,调用之后后面的YIELD value RETURN value
这里,如果是这样写,返回的是这样的结果,不能映射到我定义的集合泛型上去.
可以看到返回的是一个value,value里面对应的是{“n”:节点}的对象,可是我们要的只是这个n的集合,所以返回这里,要写成YIELD value RETURN value.n
这样返回的就是我们想要的结果,也能映射到NodeEntity上了.
之后再像Session的方法一样,查询总数,封装分页类即可.
参考资料
SpringDataNeo4j官方文档及API:https://spring.io/projects/spring-data-neo4j#learn
APOC说明文档:https://neo4j-contrib.github.io/neo4j-apoc-procedures/#_virtual_nodes_rels