之前已经写过一篇文章简要介绍了图数据库Neo4j的概念,没看过的读者可以在此点链接《图数据库Neo4j简介》。本文主要讲解图数据库在真实项目中的实践应用,取自于我参与的真实项目代码。

后端用的是图数据库Neo4j来存节点和关系,前端用的是D3来画图。前后端交互是通过json数据来完成的,即Neo4j查出的结果组装成json后,传递给D3来画图。


1 Neo4j

首先看一下pom文件相关代码,如下:

<dependency><groupId>org.neo4j.driver</groupId><artifactId>neo4j-java-driver</artifactId><version>1.5.0</version>
</dependency>

如图所示,我用的是neo4j-java-driver连接的Neo4j,当然也可以选择其他方式。例如Spring-Data-Neo4j等连接方式(只不过我所在项目中的spring版本和Spring-Data-Neo4j版本冲突,所以不能使用)。

然后是加载驱动的代码如下:

private static void driverConnect() {InputStream in = GraphDriverManager.class.getClassLoader().getResourceAsStream(LOCAL);Properties p = new Properties();try {p.load(in);} catch (IOException e) {e.printStackTrace();}String url = p.getProperty("neo4j.url");String userName = p.getProperty("neo4j.username");String passWord = p.getProperty("neo4j.password");// 使用 basic authentication方式创建连接try {driver = GraphDatabase.driver(url, AuthTokens.basic(userName, passWord), Config.build().withMaxConnectionLifetime(30, TimeUnit.MINUTES).withMaxConnectionPoolSize(50).withConnectionAcquisitionTimeout(2, TimeUnit.MINUTES).toConfig());resetFlag();} catch (Exception e) {logger.error("图数据库连接异常",e);//当数据库连接异常时,把标志位写进文件,做提示用。ErrorConnectPut();}
}

        java连接Neo4j的方式有三种:bolt、http和https,我选用的是bolt协议。同时为了方便,将Neo4j的url、用户名和密码做成properties文件来读取。

最后就是具体的调用代码了。如下所示:

@Override
public Map<Set<String>, Set<String>> searchAllPathsOfTwoNodes(String firstLabel, String firstName, String secondLabel, String secondName) {Map<Set<String>, Set<String>> searchResults = null;Driver driver = driverManager.getDriver();try (Session session = driver.session()) {searchResults = session.readTransaction(tx -> {return standardGraphDao.searchAllPathsOfTwoNodes(tx, firstLabel, firstName, secondLabel, secondName);});} catch (Exception e) {throw new RuntimeException(e);}return searchResults;
}

用内部类和lambda表达式的方式调用DAO层的代码。正如在之前文章中所提,应避免写出循环开闭事务的代码,应将循环放进DAO层里。

        但是该种写法只是示例写法,用在实际的项目中会有很大的效率问题。假如在该方法中还需要调用test方法,那么不可避免的是test方法中仍然需要获取事务,这就会有嵌套事务的情况出现,在一些方法比较复杂和大数据量的执行下,效率会直线下降。解决办法是统一使用写事务(用写事务来代替读事务用以实现统一的事务获取,目前我还没有发现相关的bug),只有在最外面的方法才获取事务,里面如果有方法调用,则将事务tx一同作为参数传递过去,这样在整个方法中只有一次获取事务的情况出现。

但这样虽然解决了效率问题,在整个service层的开发却变得异常糟糕:充斥着大量的try...catch...业务无关语句。很自然的想到,可以用AOP进行改造,改造结果如下:

@Around("pointCut()")
public Object around(ProceedingJoinPoint jp) {MethodSignature msig = (MethodSignature) jp.getSignature();Method method = msig.getMethod();Object returnType = method.getReturnType();try {Field txField = StandardGraphDao.class.getDeclaredField("tx");txField.setAccessible(true);Driver driver = driverManager.getDriver();try (Session session = driver.session()) {returnType = session.writeTransaction(tx -> {try {txField.set(StandardGraphDao.class.newInstance(), tx);return jp.proceed();} catch (Throwable e) {throw new RuntimeException(e);}});} catch (Exception e) {if (LOGGER.isErrorEnabled()) {LOGGER.error(e.toString());}checkSynchronize();}txField.set(StandardGraphDao.class.newInstance(), null);} catch (NoSuchFieldException | IllegalAccessException | InstantiationException e) {throw new RuntimeException(e);}return returnType;
}

在切面类中加入上述的环绕通知,用注解来驱动,其中return jp.proceed();是真正调用的service层方法代码。同时利用反射来对方法返回值和事务tx赋值。改造完成后service层的代码如下:

@Override
@Neo4jTransactional
public void saveInfoSubject(List<StandardVO> standardVOList) {if (standardVOList == null) {throw new NullArgumentException("standardVOList为空");}for (StandardVO standardVO : standardVOList) {standardGraphDao.saveInfoSubjectNode(standardVO, StandardGraphConstant.NODE_LABEL_INFOSUBJECT);}
}

@Neo4jTransactional是我自定义的注解,有了它就可以启动之前设定好的AOP。由上可以看到,service层的代码去掉了try...catch...语句,变得很清爽,和普通方法调用无异。DAO层相关代码如下:

/*** * <p>Title: searchAllPathsOfTwoNodes </p>* <p>Description: 查找某两个节点是否有关系(全部路径) </p>* @param firstLabel* @param firstName* @param secondLabel* @param secondName* @return    参数说明* @author houyishuang* @date 2018年1月21日*/
public Map<Set<String>, Set<String>> searchAllPathsOfTwoNodes(String firstLabel, String firstName, String secondLabel, String secondName) {Map<Set<String>, Set<String>> returnMap = new HashMap<>(16);String cypher = "MATCH (a:" + firstLabel + "{name:$firstName}), (b:" + secondLabel + "{name:$secondName}), p = allShortestPaths((a)-[*]-(b)) RETURN p";List<Record> records = tx.run(cypher, parameters("firstName", firstName, "secondName", secondName)).list();if (!records.isEmpty()) {for (Record searchResult : records) {Map<Set<String>, Set<String>> map = StandardGraphUtils.getIdsFromPath(searchResult.values().get(0).toString());returnMap = StandardGraphUtils.pathMapMerge(returnMap, map);}}return returnMap;
}

该方法实现的功能是查询两个节点之间的全部路径。可以看到,直接用传过来的参数拼装成cypher语句,到Neo4j中查询出结果,然后组装成想要的格式返回即可。

其中StandardGraphUtils工具类的部分代码如下所示,仅供参考:

import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;/*** * <p>Classname: StandardGraphUtils </p>* <p>Description: 知识图谱工具类;</p>* @author houyishuang* @date 2018年1月12日*/
public class StandardGraphUtils {/*** 节点匹配*/private static final String NODE     = "node<(.*)>";/*** 节点匹配*/private static final String NODES    = "\\[node<(.*)>, node<(.*)>\\]";/*** 路径匹配*/private static final String PATH     = "path\\[(.*)\\]";/*** 路径匹配*/private static final String SUB_PATH = "\\((.*)\\).?-\\[(.*):.*\\]-.?\\((.*)\\)";//...private StandardGraphUtils() {}/*** * <p>Title: getIdFromNode </p>* <p>Description: 节点匹配 </p>* @param node* @return    参数说明* @author houyishuang* @date 2018年1月18日*/public static String getIdFromNode(String node) {String result = null;Pattern r = Pattern.compile(NODE);Matcher m = r.matcher(node);if (m.find()) {result = m.group(1);}return result;}/*** * <p>Title: getIdsFromNode </p>* <p>Description: 节点匹配 </p>* @param node* @return    参数说明* @author houyishuang* @date 2018年1月19日*/public static Map<String, String> getIdsFromNode(String node) {Map<String, String> results = new HashMap<>(16);String result1 = null;String result2 = null;Pattern r = Pattern.compile(NODES);Matcher m = r.matcher(node);if (m.find()) {result1 = m.group(1);result2 = m.group(2);}results.put(result1, result2);return results;}/*** * <p>Title: getIdsFromPath </p>* <p>Description: 路径匹配 </p>* @param path* @return    参数说明* @author houyishuang* @date 2018年1月21日*/public static Map<Set<String>, Set<String>> getIdsFromPath(String path) {Map<Set<String>, Set<String>> returnMap = new HashMap<>(16);Set<String> nodeIdList = new HashSet<>();Set<String> relIdList = new HashSet<>();String result = null;Pattern r = Pattern.compile(PATH);Matcher m = r.matcher(path);if (m.find()) {result = m.group(1);}String[] resultArray = result.split(", ");for (String rs : resultArray) {Pattern r1 = Pattern.compile(SUB_PATH);Matcher m1 = r1.matcher(rs);if (m1.find()) {String result1 = m1.group(1);String result2 = m1.group(2);String result3 = m1.group(3);nodeIdList.add(result1);relIdList.add(result2);nodeIdList.add(result3);}}returnMap.put(nodeIdList, relIdList);return returnMap;}/*** * <p>Title: pathMapMerge </p>* <p>Description: 路径map合并 </p>* @param returnMap* @param map* @return    参数说明* @author houyishuang* @date 2018年2月5日*/public static Map<Set<String>, Set<String>> pathMapMerge(Map<Set<String>, Set<String>> returnMap, Map<Set<String>, Set<String>> map) {Set<String> returnNodeIdList = null;Set<String> returnRelIdList = null;Set<String> nodeIdList = null;Set<String> relIdList = null;if (returnMap.isEmpty() && map.isEmpty()) {return null;}if (returnMap.isEmpty()) {return map;}if (map.isEmpty()) {return returnMap;}for (Map.Entry<Set<String>, Set<String>> entry : returnMap.entrySet()) {returnNodeIdList = entry.getKey();returnRelIdList = entry.getValue();}for (Map.Entry<Set<String>, Set<String>> entry : map.entrySet()) {nodeIdList = entry.getKey();relIdList = entry.getValue();}returnMap.clear();map.clear();returnNodeIdList.addAll(nodeIdList);returnRelIdList.addAll(relIdList);returnMap.put(returnNodeIdList, returnRelIdList);return returnMap;}//...
}

同时之前在切面类中对事务tx赋值就是对下面DAO层的tx属性赋值,这样就可以像上述searchAllPathsOfTwoNodes方法中那样,用事务去执行cypher了。

@Component
public class StandardGraphDao {/*** Neo4j事务 */private static Transaction tx;//do something...}

        在之后的学习工作中知道,上述的tx静态变量写法还是存在着线程安全的问题的。解决办法是将tx用ThreadLocal包装起来。但使用ThreadLocal时必须确保get和set方法在同一次请求中才行。


2 D3

之所以决定要用D3而不是ECharts,主要是觉得D3的灵活性更大一些,可以做一些定制化的需求。而ECharts的功能都已经给你提供了,想要定制化比较困难一些。当然,D3相对于来说更难上手,所以这里先普及一些D3的基本概念。

D3是一个JavaScript的函数库,是用来做数据可视化的。D3的全称是Data-Driven Document,数据驱动的文档。D3的核心是数据和元素之间的绑定,这点需要读者多进行理解消化。下面讲解一个核心概念:update、enter和exit

既然D3做的是数据和元素之间的绑定,那如果数组长度和元素数量不一致,就会带来三个选择集:update、enter和exit,如下图所示:

由图可知,没有被元素绑定的多余数据叫做enter;没有数据对应、多余的元素叫做exit;元素和数据一一对应的部分叫做update。

enter代表没有足够的元素,因此处理方法是添加元素;如果存在多余的元素,没有数据与之对应,那么就需要删除元素。所以可以看到,在D3中,数据是最为重要的。可以删元素,但是不能删除数据。

但是如果不知道数组的长度,如何为update、enter、exit提供处理方案呢?其实,数组长度和元素数量的大小并不重要。在多数可视化中,无论哪一边大,

  1. update所代表的元素都该“更新”。
  2. enter所代表的元素都该“添加”。
  3. exit所代表的元素都该“删除”。

因此,这种数据绑定(Data-Join)允许开发者在不知道新数据长度的情况下更新图形。将这种类似的处理方案总结为一个模板,代码如下:

var dataset = [ 10, 20, 30 ];
var p = d3.select("body").selectAll("p");
// 绑定数据后,分别返回update、enter、exit部分
var update = p.data(dataset);
var enter = update.enter();
var exit = update.exit();// 1.update部分的处理方法
update.text(function(d) {return d;
});// 2.enter部分的处理方法
enter.append("p").text(function(d) {return d;
});// 3.exit部分的处理方法
exit.remove();

        需要注意的是exit.remove方法。D3中将数据和元素进行绑定,默认采用的是从上到下的顺序。也就是说来一个数据,就和一个元素绑定。这在没有exit.remove出现的场景中是没有问题的。但当有删除元素的情况出现的话,默认的绑定规则可能会出错。比如说原来是a对1、b对2、c对3,我删除了数据2,理论上应该变成a对1、c对3、b没有数据对应,所以是exit。但是,如前所说,默认绑定规则是从上到下,所以实际上变成了a对1、b对3、c没有数据与之对应。结果c变成了exit,导致了错误。解决办法就是不采用默认的绑定规则,改用按照某种规律进行绑定,如下:

nodes = nodes.data(force.nodes(),function(d) {return d.inst_cd;
});

如上所示nodes是按照inst_cd进行绑定,这样再删除数据的时候,就不会出现删除元素错误的情况出现了。

明白了update、enter和exit这三个概念,再来理解D3就容易多了。本例用的是D3的力导向图(Force-Directed Graph)。力导向图是绘图的一种算法,在二维或三维空间里配置节点,节点之间用线连接,称为连线。各连线的长度几乎相等,且尽可能不相交。节点和连线都被施加了力的作用,力是根据节点和连线的相对位置计算的。根据力的作用,来计算节点和连线的运动轨迹,并不断降低它们能量,最终达到一种能量很低的安定状态。

function showInfo(metaID,analyseType){  var height = 1500;var width = 1500;nodes_data =[];edges_data =[];edgeWidth = 2;r1 = 40;r2 = 20;color = d3.scale.category20();$.ajax({type:"post",url:__contextPath+"/standard/standardGraph/initGraph.d",async:true,cache:false,success:function(result){if(result==null || result==""){return "";}else{arr=eval('('+result+')');}nodes_data=arr.nodes;edges_data=arr.links;edges_data.forEach(function (link) {nodes_data.forEach(function(node){if(link.source==node.inst_cd){link.source=node;}if(link.target==node.inst_cd){link.target=node;}})});svg = d3.select("#standardgraph").append("svg").attr("width", width).attr("height", height);force = d3.layout.force().nodes(nodes_data).links(edges_data).size([width, height]).linkDistance(function(d){if(d.target.model =="CommonCodeRoot" || d.target.model =="InfoSubject"){return 300;}else{return 100;}})// .friction(0.8).charge(-1000).on("start",forceStart).on("tick", tick).start();// 箭头drawMarker();drawLinks();drawNodes();// 标签drawNodes_lables();drawLinks_text();function zoomed(){  // svg.attr("transform","translate("+d3.event.translate+")scale("+d3.event.scale+")")svg.attr("transform","translate("+d3.event.translate+")")  } // d3.event.translate 是平移的坐标值,d3.event.scale 是缩放的值var zoom = d3.behavior.zoom().scaleExtent([-10,10])// 用于设置最小和最大的缩放比例.on("zoom",zoomed);// svg.call(zoom);}})
}   

上图所示是我在实际项目中参与完成的、用D3力导向图画出图形的部分代码。后台向前台传进json数据,前台拿到json数据进行处理画图。


3 展示

最后的成果如下:

注:即使经过了上述的优化,最终的执行速度依然算不上快。以上方案仅适用于没有使用Spring Boot的项目,在Spring Boot的项目中会有Neo4j的相关依赖,可以考虑使用相关API来进行开发。

Neo4j+D3展现的应用实例相关推荐

  1. d3 svg path添加文本_数据可视化——D3展现数据最炫丽的一面

    热情的或--有温度的"1" 大家好,大家肯定很好奇,数据能是什么样子嘛,不就是干巴巴的1.2.3-!哟,这个火热的"1"好像是挺绚丽的啊,但对不起,这只是数字, ...

  2. Neo4j教程 Neo4j视频教程 Neo4j 图数据库视频教程

    课程发布地址 地址: 腾讯课堂<Neo4j 图数据库视频教程> https://ke.qq.com/course/327374?tuin=442d3e14 作者 庞国明,<Neo4j ...

  3. Neo4j资料 Neo4j教程 Neo4j视频教程 Neo4j 图数据库视频教程

    课程发布地址 地址: 腾讯课堂<Neo4j 图数据库视频教程> https://ke.qq.com/course/327374?tuin=442d3e14 作者 庞国明,<Neo4j ...

  4. D3.js系列——初步使用、选择元素与绑定数据

    D3 的全称是(Data-Driven Documents),顾名思义可以知道是一个被数据驱动的文档.听名字有点抽象,说简单一点,其实就是一个 JavaScript 的函数库,使用它主要是用来做数据可 ...

  5. 图数据库Neo4j全栈Web技术解密

    注:作者的代表作品[Neo4j + D3.js 项目视频教程]http://edu.51cto.com/course/11315.html?reci Neo4j是什么? 图1. Neo4j Web控制 ...

  6. Neo4j因果集群路由策略详解及驱动访问

    Neo4j的使用和大多数数据库一样,如果您只想查询,这很简单.使用驱动程序,创建连接,提交查询并返回结果,如此简单! 如果您正在使用数据库集群,那么要做的事情不止这些.首先,数据库实例不是单个节点,而 ...

  7. NEO4J分析《权力的游戏》人物领土等关系

    写在前面 最近研究了下使用NOSQL维护风控衍生变量.知识图谱.接触到NEO4J的时候发现可以利用图数据库描述复杂人物.领土.资源等的关系.之前在CSDN看到很多厉害的年轻人描述过美剧<权利的游 ...

  8. Neo4J Cypher neo4j-driver py2neo介绍与使用

    Neo4J Cypher neo4j-driver介绍与使用 neo4j介绍 关系型数据库和图数据库 图数据库的基本概念 Nodes Labels Relationship RelationshipT ...

  9. Python neo4j建立知识图谱,药品知识图谱,neo4j知识图谱,知识图谱的建立过程,智能用药知识图谱,智能问诊必备知识图谱

    一.知识图谱概念 知识图谱的概念是由谷歌公司在2012年5月17日提出的,谷歌公司将以此为基础构建下一代智能化搜索引擎,知识图谱技术创造出一种全新的信息检索模式,为解决信息检索问题提供了新的思路.本质 ...

最新文章

  1. UVa10970 - Big Chocolate(计数问题)
  2. oracle查询orapw文件,oracle学习笔记《一》
  3. Oracle数据库adg数据没同步,Oracle 11g备库无法开启ADG的原因分析
  4. Andorid之打包出现Proguard returned with erro code 1.See console解决办法
  5. Qt下简单的文件读取
  6. 字典树从第i个构造HDU2846
  7. Tomcat端口占用的处理方式
  8. linux删除第二列为空_Linux系列第二谈(开机关机、Linux中的文件、目录管理、基本属性)...
  9. 曼昆经济学原理_第五版[1].txt.doc
  10. python定时任务apschedule_Python定时任务-APScheduler
  11. 《缠中说禅108课》58:图解分析示范三
  12. java 视频转mp4_java视频转码mov转MP4
  13. 集成百度做敏感词鉴定
  14. 最基本财务基础知识,财务知识基础来源
  15. pytorch个人学习笔记(2)—Normalize()参数详解及用法
  16. 基于线性回归的员工离职率预测
  17. c语言编写数码管的现实函数,C语言实现一位共阳极数码管
  18. 首款Unreal Engine 4引擎制作手机游戏曝光
  19. 远程计算机的凭据无法工作,Windows远程桌面时提示凭证不工作问题的解决办法...
  20. 中科大计算机考研录取分数线_计算机专业学校考研难度排行榜 计算机考研难度排名...

热门文章

  1. 【收藏向】电磁场思维导图 4小时不挂科(冯慈璋教材)
  2. 后端实现上传文件接口,并使用阿里云的oss对象存储
  3. 三星手机安装linux系统下载,三星R18笔记本电脑安装Ubuntu8.04正式版
  4. CCriticalSection 在哪个.h头文件
  5. svn导致桌面图标带蓝色问号,加号等svn标记问题解决
  6. [渝粤教育] 厦门大学 电磁场与电磁波 参考 资料
  7. 【English学习】6个老外最常用的口语表达
  8. 4 Python 先学会基本语法
  9. vee-validate 验证
  10. 企立方:拼多多差评的处理应该怎么办