当前位置:   article > 正文

Neo4j+D3展现的应用实例_neo4jd3

neo4jd3

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

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


1 Neo4j

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

  1. <dependency>
  2. <groupId>org.neo4j.driver</groupId>
  3. <artifactId>neo4j-java-driver</artifactId>
  4. <version>1.5.0</version>
  5. </dependency>

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

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

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

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

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

  1. @Override
  2. public Map<Set<String>, Set<String>> searchAllPathsOfTwoNodes(String firstLabel, String firstName, String secondLabel, String secondName) {
  3. Map<Set<String>, Set<String>> searchResults = null;
  4. Driver driver = driverManager.getDriver();
  5. try (Session session = driver.session()) {
  6. searchResults = session.readTransaction(tx -> {
  7. return standardGraphDao.searchAllPathsOfTwoNodes(tx, firstLabel, firstName, secondLabel, secondName);
  8. });
  9. } catch (Exception e) {
  10. throw new RuntimeException(e);
  11. }
  12. return searchResults;
  13. }

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

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

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

  1. @Around("pointCut()")
  2. public Object around(ProceedingJoinPoint jp) {
  3. MethodSignature msig = (MethodSignature) jp.getSignature();
  4. Method method = msig.getMethod();
  5. Object returnType = method.getReturnType();
  6. try {
  7. Field txField = StandardGraphDao.class.getDeclaredField("tx");
  8. txField.setAccessible(true);
  9. Driver driver = driverManager.getDriver();
  10. try (Session session = driver.session()) {
  11. returnType = session.writeTransaction(tx -> {
  12. try {
  13. txField.set(StandardGraphDao.class.newInstance(), tx);
  14. return jp.proceed();
  15. } catch (Throwable e) {
  16. throw new RuntimeException(e);
  17. }
  18. });
  19. } catch (Exception e) {
  20. if (LOGGER.isErrorEnabled()) {
  21. LOGGER.error(e.toString());
  22. }
  23. checkSynchronize();
  24. }
  25. txField.set(StandardGraphDao.class.newInstance(), null);
  26. } catch (NoSuchFieldException | IllegalAccessException | InstantiationException e) {
  27. throw new RuntimeException(e);
  28. }
  29. return returnType;
  30. }

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

  1. @Override
  2. @Neo4jTransactional
  3. public void saveInfoSubject(List<StandardVO> standardVOList) {
  4. if (standardVOList == null) {
  5. throw new NullArgumentException("standardVOList为空");
  6. }
  7. for (StandardVO standardVO : standardVOList) {
  8. standardGraphDao.saveInfoSubjectNode(standardVO, StandardGraphConstant.NODE_LABEL_INFOSUBJECT);
  9. }
  10. }

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

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

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

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

  1. import java.util.HashMap;
  2. import java.util.HashSet;
  3. import java.util.Map;
  4. import java.util.Set;
  5. import java.util.regex.Matcher;
  6. import java.util.regex.Pattern;
  7. /**
  8. *
  9. * <p>Classname: StandardGraphUtils </p>
  10. * <p>Description: 知识图谱工具类;</p>
  11. * @author houyishuang
  12. * @date 2018年1月12日
  13. */
  14. public class StandardGraphUtils {
  15. /**
  16. * 节点匹配
  17. */
  18. private static final String NODE = "node<(.*)>";
  19. /**
  20. * 节点匹配
  21. */
  22. private static final String NODES = "\\[node<(.*)>, node<(.*)>\\]";
  23. /**
  24. * 路径匹配
  25. */
  26. private static final String PATH = "path\\[(.*)\\]";
  27. /**
  28. * 路径匹配
  29. */
  30. private static final String SUB_PATH = "\\((.*)\\).?-\\[(.*):.*\\]-.?\\((.*)\\)";
  31. //...
  32. private StandardGraphUtils() {}
  33. /**
  34. *
  35. * <p>Title: getIdFromNode </p>
  36. * <p>Description: 节点匹配 </p>
  37. * @param node
  38. * @return 参数说明
  39. * @author houyishuang
  40. * @date 2018年1月18日
  41. */
  42. public static String getIdFromNode(String node) {
  43. String result = null;
  44. Pattern r = Pattern.compile(NODE);
  45. Matcher m = r.matcher(node);
  46. if (m.find()) {
  47. result = m.group(1);
  48. }
  49. return result;
  50. }
  51. /**
  52. *
  53. * <p>Title: getIdsFromNode </p>
  54. * <p>Description: 节点匹配 </p>
  55. * @param node
  56. * @return 参数说明
  57. * @author houyishuang
  58. * @date 2018年1月19日
  59. */
  60. public static Map<String, String> getIdsFromNode(String node) {
  61. Map<String, String> results = new HashMap<>(16);
  62. String result1 = null;
  63. String result2 = null;
  64. Pattern r = Pattern.compile(NODES);
  65. Matcher m = r.matcher(node);
  66. if (m.find()) {
  67. result1 = m.group(1);
  68. result2 = m.group(2);
  69. }
  70. results.put(result1, result2);
  71. return results;
  72. }
  73. /**
  74. *
  75. * <p>Title: getIdsFromPath </p>
  76. * <p>Description: 路径匹配 </p>
  77. * @param path
  78. * @return 参数说明
  79. * @author houyishuang
  80. * @date 2018年1月21日
  81. */
  82. public static Map<Set<String>, Set<String>> getIdsFromPath(String path) {
  83. Map<Set<String>, Set<String>> returnMap = new HashMap<>(16);
  84. Set<String> nodeIdList = new HashSet<>();
  85. Set<String> relIdList = new HashSet<>();
  86. String result = null;
  87. Pattern r = Pattern.compile(PATH);
  88. Matcher m = r.matcher(path);
  89. if (m.find()) {
  90. result = m.group(1);
  91. }
  92. String[] resultArray = result.split(", ");
  93. for (String rs : resultArray) {
  94. Pattern r1 = Pattern.compile(SUB_PATH);
  95. Matcher m1 = r1.matcher(rs);
  96. if (m1.find()) {
  97. String result1 = m1.group(1);
  98. String result2 = m1.group(2);
  99. String result3 = m1.group(3);
  100. nodeIdList.add(result1);
  101. relIdList.add(result2);
  102. nodeIdList.add(result3);
  103. }
  104. }
  105. returnMap.put(nodeIdList, relIdList);
  106. return returnMap;
  107. }
  108. /**
  109. *
  110. * <p>Title: pathMapMerge </p>
  111. * <p>Description: 路径map合并 </p>
  112. * @param returnMap
  113. * @param map
  114. * @return 参数说明
  115. * @author houyishuang
  116. * @date 2018年2月5日
  117. */
  118. public static Map<Set<String>, Set<String>> pathMapMerge(Map<Set<String>, Set<String>> returnMap, Map<Set<String>, Set<String>> map) {
  119. Set<String> returnNodeIdList = null;
  120. Set<String> returnRelIdList = null;
  121. Set<String> nodeIdList = null;
  122. Set<String> relIdList = null;
  123. if (returnMap.isEmpty() && map.isEmpty()) {
  124. return null;
  125. }
  126. if (returnMap.isEmpty()) {
  127. return map;
  128. }
  129. if (map.isEmpty()) {
  130. return returnMap;
  131. }
  132. for (Map.Entry<Set<String>, Set<String>> entry : returnMap.entrySet()) {
  133. returnNodeIdList = entry.getKey();
  134. returnRelIdList = entry.getValue();
  135. }
  136. for (Map.Entry<Set<String>, Set<String>> entry : map.entrySet()) {
  137. nodeIdList = entry.getKey();
  138. relIdList = entry.getValue();
  139. }
  140. returnMap.clear();
  141. map.clear();
  142. returnNodeIdList.addAll(nodeIdList);
  143. returnRelIdList.addAll(relIdList);
  144. returnMap.put(returnNodeIdList, returnRelIdList);
  145. return returnMap;
  146. }
  147. //...
  148. }

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

  1. @Component
  2. public class StandardGraphDao {
  3.    /**
  4.     * Neo4j事务
  5.     */
  6.    private static Transaction tx;
  7.    //do something...
  8. }

        在之后的学习工作中知道,上述的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)允许开发者在不知道新数据长度的情况下更新图形。将这种类似的处理方案总结为一个模板,代码如下:

  1. var dataset = [ 10, 20, 30 ];
  2. var p = d3.select("body").selectAll("p");
  3. // 绑定数据后,分别返回update、enter、exit部分
  4. var update = p.data(dataset);
  5. var enter = update.enter();
  6. var exit = update.exit();
  7. // 1.update部分的处理方法
  8. update.text(function(d) {
  9. return d;
  10. });
  11. // 2.enter部分的处理方法
  12. enter.append("p").text(function(d) {
  13. return d;
  14. });
  15. // 3.exit部分的处理方法
  16. 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,导致了错误。解决办法就是不采用默认的绑定规则,改用按照某种规律进行绑定,如下:

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

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

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

  1. function showInfo(metaID,analyseType){
  2.    var height = 1500;
  3.    var width = 1500;
  4.    nodes_data =[];
  5.    edges_data =[];
  6.    edgeWidth = 2;
  7.    r1 = 40;
  8.    r2 = 20;
  9.    color = d3.scale.category20();
  10.    $.ajax({
  11. type:"post",
  12. url:__contextPath+"/standard/standardGraph/initGraph.d",
  13. async:true,
  14. cache:false,
  15. success:function(result){
  16. if(result==null || result==""){
  17. return "";
  18. }else{
  19. arr=eval('('+result+')');
  20. }
  21. nodes_data=arr.nodes;
  22. edges_data=arr.links;
  23. edges_data.forEach(function (link) {
  24. nodes_data.forEach(function(node){
  25. if(link.source==node.inst_cd){
  26. link.source=node;
  27. }
  28. if(link.target==node.inst_cd){
  29. link.target=node;
  30. }
  31. })
  32. });
  33.        svg = d3.select("#standardgraph").append("svg")
  34.               .attr("width", width)
  35.               .attr("height", height);
  36.        force = d3.layout.force()
  37.           .nodes(nodes_data)
  38.           .links(edges_data)
  39.           .size([width, height])
  40.           .linkDistance(function(d){
  41.           if(d.target.model =="CommonCodeRoot" || d.target.model =="InfoSubject"){
  42. return 300;
  43. }else{
  44. return 100;
  45. }
  46.           })
  47.            // .friction(0.8)
  48.           .charge(-1000)
  49.           .on("start",forceStart)
  50.           .on("tick", tick)
  51.           .start();
  52.        // 箭头
  53.        drawMarker();
  54.        drawLinks();
  55.        drawNodes();
  56.        // 标签
  57.        drawNodes_lables();
  58.        drawLinks_text();
  59.        
  60.        
  61.   function zoomed(){  
  62.   // svg.attr("transform","translate("+d3.event.translate+")scale("+d3.event.scale+")")
  63.   svg.attr("transform","translate("+d3.event.translate+")")  
  64.   } // d3.event.translate 是平移的坐标值,d3.event.scale 是缩放的值
  65.  
  66.        var zoom = d3.behavior.zoom()
  67.           .scaleExtent([-10,10])// 用于设置最小和最大的缩放比例
  68.           .on("zoom",zoomed);
  69.        
  70.        // svg.call(zoom);
  71. }
  72.   })
  73. }

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


3 展示

最后的成果如下:

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

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/不正经/article/detail/727627
推荐阅读
相关标签
  

闽ICP备14008679号