近期由于工作需要,需要做一个人口关系大图的存储及检索方案,我们主要的数据对象有:人口(年龄,身份证号码,性别。。) ;学校信息(学校地址,学校名称,学校级别,学校下边的年级班级。。);就职信息(公司名称,公司地址,公司企业信用代码。。)以及论文发布信息(论文主题,参与人,发布时间)
问题思考:
- 针对这类人口关系大图,或者叫图谱的数据一般都是非关系型,而且有多钟关系的存在,比如张三跟李四两个人,可能是同学,同事,老乡,配偶,夫妻,等多种关系,那一般如果采取关系型数据库(oracle)为例,可能就需要建立人员信息表, 同学关系表,同事关系表,老乡关系表,配偶关系表,夫妻关系表,这样一旦后续增加一个关系,就要增加一张表,增加一张表,而且后续数据库中就基本上只剩关系表了,后续的维护,扩充,代码的修改就机器的困难,简直就是噩梦。
- 通过上述的第一点,我们不难看出,采用关系型数据库来存储并不是最好的解决方案,那么就很容易想到采用关系型数据库存,那么目前比较流行的有哪些非关系型数据库么,redis,mongoDB,neo4j都能存储单个菲关系型的数据,但他们三者还是有区别的,前两者一般在项目中作为一个数据缓存的容器,将一些不经常变化的数据放在容器中,需者自取。而neoj4j专门用于网络图的存储,网络图,这个跟我们的项目需求不是不谋而合?于是心底暗自拍板,就你了 ,neo4j。
数据库设计:
通过关系表中存储的ID来进行关联;注意设置的时候是将与一个人所有关联的信息进行抽象出来并划分为五个对象,即上图的五个表单。 通过上图我们可以和明确的看出来有如下几种关系: 人 -就读->学校;
人 -发布论文->论文; 人-就职->企业; 人-居住->家庭;这四类的外部关系(以单个人节点为参照),另外一方面,人根根之间也是存在关系的,比如刚开始提到的同事,同学类的,那么这类属于内部关系,内部关系我们一般需要通过外部关系区分,比如张三和李四只有在同一个班级上学,那么菜算得上同学吧,只有在同一个公司就职才算得上同事吧.。这样的话,我们数据库设计出来,逻辑也梳理出来,尤其是内部关系外部关系。
实现方案:
我项目采用的是spring+springMvc的环境。
组件代码:
注意我是将neo4j的配置及实体放在一块的,至于其他的逻辑操作在其他的包中。
部分代码:
设置Configuration属性到neo4j中
package com.audaque.module.graphData.neo4j.config; import org.neo4j.ogm.session.Session; import org.neo4j.ogm.session.SessionFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.*; import org.springframework.data.neo4j.config.Neo4jConfiguration; import org.springframework.data.neo4j.repository.config.EnableNeo4jRepositories; import org.springframework.transaction.annotation.EnableTransactionManagement; /*** *@ClassName AppConfiguration *@desc TODO *@Author xxxxxx *@Date 2019/5/17 0017 下午 10:57 *@version 1.0.1 **/ @Configuration @EnableNeo4jRepositories(basePackages = "com.audaque.module.graphData.neo4j.repos") @EnableTransactionManagement @ComponentScan("com.audaque.module.graphData.neo4j.config") public class AppConfiguration extends Neo4jConfiguration {
@Autowired private org.neo4j.ogm.config.Configuration config; @Bean public org.neo4j.ogm.config.Configuration getConfiguration() { return config; } @Override @Bean public SessionFactory getSessionFactory() { // with domain entity base package(s) return new SessionFactory(config,"com.audaque.module.graphData.neo4j.model"); } @Override @Bean @Scope(value = "session", proxyMode = ScopedProxyMode.TARGET_CLASS) public Session getSession() throws Exception { return super.getSession(); } }
动态设置属性到Configuration中属性到中
package com.audaque.module.graphData.neo4j.config; import org.neo4j.ogm.authentication.UsernamePasswordCredentials; import org.neo4j.ogm.config.Configuration; import static org.neo4j.ogm.config.DriverConfiguration.CREDENTIALS; import static org.neo4j.ogm.config.DriverConfiguration.DRIVER; import static org.neo4j.ogm.config.DriverConfiguration.URI; /*** *@ClassName MyConfiguration *@desc 自定义bean动态配置neo4j数据源。 *@Author xiaokang.ma@audaque.com *@Date 2019/5/24 9:14 *@version 1.0.1 **/ public class MyConfiguration extends Configuration { public MyConfiguration(String driverClass,String driverURL,String userName,String password){ super.set(DRIVER[0],driverClass); super.set(URI[0],driverURL); super.set(CREDENTIALS[0],new UsernamePasswordCredentials(userName, password)); } }
PaperInfoRepository配置(实体的保存):
package com.audaque.module.graphData.neo4j.repos; import com.audaque.module.graphData.neo4j.model.PaperInfo; import org.springframework.data.neo4j.repository.GraphRepository; import org.springframework.stereotype.Repository; /*** *@ClassName PaperInfoRepository *@desc 论文信息接口 *@Author xxxx *@Date 2019/5/17 0017 下午 5:43 *@version 1.0.1 **/ @Repository public interface PaperInfoRepository extends GraphRepository<PaperInfo> { }
内部关系的Repository我采用的自定义查询语句查询,如下:
package com.audaque.module.graphData.neo4j.repos; import com.audaque.module.graphData.neo4j.model.Neo4jRelationInfo; import org.neo4j.consistency.store.paging.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.neo4j.annotation.Query; import org.springframework.data.neo4j.repository.GraphRepository; import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; import java.util.Collection; import java.util.List; import java.util.Map; /*** *@ClassName Neo4jRelationInfoRepository *@desc 八大关系型数据。 *@Author xxxx *@Date 2019/5/17 0017 下午 5:43 *@version 1.0.1 **/ @Repository public interface Neo4jRelationInfoRepository extends GraphRepository<Neo4jRelationInfo> { /** * pe - pe 邻居关系 * @param fromId * @param toId * @return */ @Query(value = "match(a:pe),(b:pe) where a.ids={fromId} and b.ids={toId} merge (a) - [r:邻居{direction:{direction}}]->(b)") Neo4jRelationInfo generateGraphDataRelation_LJ(@Param("fromId") String fromId, @Param("toId") String toId, @Param("direction") String direction); /** * pe -> pe 同学关系 * @param fromId * @param toId * @return */ @Query(value = "match(a:pe),(b:pe) where a.ids={fromId} and b.ids={toId} merge (a) - [r:同学{direction:{direction}}]->(b)") Neo4jRelationInfo generateGraphDataRelation_TX(@Param("fromId") String fromId, @Param("toId") String toId, @Param("direction") String direction); /** * pe -> pe 校友关系 * @param fromId * @param toId * @return */ @Query(value = "match(a:pe),(b:pe) where a.ids={fromId} and b.ids={toId} merge (a) <- [r:校友{direction:{direction}}]->(b)") Neo4jRelationInfo generateGraphDataRelation_XY(@Param("fromId") String fromId, @Param("toId") String toId, @Param("direction") String direction); /** * pe -> li 居住关系 * @param fromId * @param toId * @return */ @Query(value = "match(a:pe),(b:li) where a.ids={fromId} and b.ids={toId} merge (a) - [r:居住{direction:{direction}}]->(b)") Neo4jRelationInfo generateGraphDataRelation_JUZ(@Param("fromId") String fromId, @Param("toId") String toId, @Param("direction") String direction); /** * pe -> pe 同事关系 * @param fromId * @param toId * @return */ @Query(value = "match(a:pe),(b:pe) where a.ids={fromId} and b.ids={toId} merge (a) - [r:同事{direction:{direction}}]-(b)") Neo4jRelationInfo generateGraphDataRelation_TS(@Param("fromId") String fromId, @Param("toId") String toId, @Param("direction") String direction); /** * pe -> wu 就职关系 * @param fromId * @param toId * @return */ @Query(value = "match(a:pe),(b:wu) where a.ids={fromId} and b.ids={toId} merge (a) - [r:就职{direction:{direction}}]->(b)") Neo4jRelationInfo generateGraphDataRelation_JZGX(@Param("fromId") String fromId, @Param("toId") String toId, @Param("direction") String direction); /** * pe -> sc 毕业院校关系 * @param fromId * @param toId * @return */ @Query(value = "match(a:pe),(b:sc) where a.ids={fromId} and b.ids={toId} merge (a) - [r:毕业{direction:{direction}}]->(b)") Neo4jRelationInfo generateGraphDataRelation_BYYX(@Param("fromId") String fromId, @Param("toId") String toId, @Param("direction") String direction); /** * pe -> pa 论文参与关系 * @param fromId * @param toId * @return */ @Query(value = "match(a:pe),(b:pa) where a.ids={fromId} and b.ids={toId} merge (a) - [r:参与者{direction:{direction}}]->(b)") Neo4jRelationInfo generateGraphDataRelation_PA(@Param("fromId") String fromId, @Param("toId") String toId, @Param("direction") String direction); /** * pe -> pa 论文合作关系 * @param fromId * @param toId * @return */ @Query(value = "match(a:pe),(b:pe) where a.ids={fromId} and b.ids={toId} merge (a) - [r:论文合作{direction:{direction}}]->(b)") Neo4jRelationInfo generateGraphDataRelation_LWHZ(@Param("fromId") String fromId, @Param("toId") String toId, @Param("direction") String direction); /** * web查询接口,查询与当前节点为1的数据---根据节点编号,适用于登录第二次节点点击之后的查询 */ @Query(value = "match(a)-[r]-(b) where a.ids={qid} return a,r,b") List<Map<String,String>> queryRelationDataByIds(@Param("qid") String qid); /** * web查询接口,查询与当前节点为1的数据 */ @Query(value = "match(a:pe)-[r]-(b) where a.cardNo={cardNo} return a,r,b") List<Map<String,String>> queryRelationDataByCardNo(@Param("cardNo") String cardNo); }
bean配置:
<!--初始化配置的Config--> <bean id="myConfiguration" class="com.audaque.module.graphData.neo4j.config.MyConfiguration"> <constructor-arg name="driverClass" value="org.neo4j.ogm.drivers.http.driver.HttpDriver" index="0" type="java.lang.String"/> <constructor-arg name="driverURL" value="http://10.229.183.142:7474" index="1" type="java.lang.String"/> <constructor-arg name="userName" value="neo4j" index="2" type="java.lang.String"/> <constructor-arg name="password" value="123456" index="3" type="java.lang.String"/> </bean>
通过上述的简单配置就在spring环境中搭建到了neo4j。
业务逻辑代码:
- 存储所有的节点,直接读取数据中的人员信息,学校信息,居住信息,教育信息,工作信息,5张表的数据直接save到neo4j,部分代码如下.
/** * 初始化图数据需要的人员节点数据 * * @param personBaseInfoRepository * @return */ @Override public void resetNeo4jPersonBaseInfoNode(PersonBaseInfoRepository personBaseInfoRepository) { //查询人员节点列表 List<PersonBaseInfo> personBaseInfos = graphDataDao.queryPersonBaseInfo(); for (PersonBaseInfo personBaseInfo :personBaseInfos){ try { personBaseInfoRepository.save(personBaseInfo); } catch (Throwable t){ //生成节点异常时,继续跳过。 continue; } } }
初始化内部外部关心数据,代码如下:
/********************************************开始初始化节点间关系的数据**********************************************/ /** * 初始化五大节点间的关系数据。 * @param neo4jRelationInfoRepository * @return */ @Override public void resetAllRelationInfo(Neo4jRelationInfoRepository neo4jRelationInfoRepository) { long start = System.currentTimeMillis(); //初始化人员-->工作关系,人员->学校;人员->论文;人员->住房信息4个主关系的数据。 List<Map<String,Object>> lists = graphDataDao.queryEntityRelationInfo(); //计算内部关系信息 ,同事,校友,邻居,合作者关系。 Map<String,List<String>> schoolFriendMap = new ConcurrentHashMap<>(); //校友 Map<String,List<String>> workTogetherMap = new ConcurrentHashMap<>(); //同事 Map<String,List<String>> neighborMap = new ConcurrentHashMap<>(); //邻居 Map<String,List<String>> collaboratorMap = new ConcurrentHashMap<>();//论文合作者 for(Map<String,Object> map :lists){ String userId = ""; //获取人员信息 if(StringUtils.isNoneEmpty((String)map.get("USERID")) && StringUtils.isNoneEmpty((String)map.get("USERNAME"))){ //取工作单位字段 UNIT_ID , UNIT_NAME if(StringUtils.isNoneEmpty((String)map.get("UNIT_ID")) && StringUtils.isNoneEmpty((String)map.get("UNIT_NAME"))){ //插入就职关系 neo4jRelationInfoRepository.generateGraphDataRelation_JZGX((String)map.get("USERID"),(String)map.get("UNIT_ID"),(String)map.get("USERID")+"->"+(String)map.get("UNIT_ID")); //处理同事关系 if(workTogetherMap.containsKey((String)map.get("UNIT_ID"))){ List<String> list = workTogetherMap.get((String)map.get("UNIT_ID")); if(!list.contains((String)map.get("USERID"))){ list.add((String)map.get("USERID")); workTogetherMap.put((String)map.get("UNIT_ID"),list); } }else{ List<String> lis = new ArrayList<String>(); lis.add((String)map.get("USERID")); workTogetherMap.put((String)map.get("UNIT_ID"),lis); } } //取学校字段 SCHOOL_ID , SCHOOL_NAME if(StringUtils.isNoneEmpty((String)map.get("SCHOOL_ID")) && StringUtils.isNoneEmpty((String)map.get("SCHOOL_NAME"))){ //插入毕业院校 neo4jRelationInfoRepository.generateGraphDataRelation_BYYX((String)map.get("USERID"),(String)map.get("SCHOOL_ID"),(String)map.get("USERID")+"->"+(String)map.get("SCHOOL_ID")); //处理校友关系 if(schoolFriendMap.containsKey((String)map.get("SCHOOL_ID"))){ List<String> list = schoolFriendMap.get((String)map.get("SCHOOL_ID")); if(!list.contains((String)map.get("USERID"))){ list.add((String)map.get("USERID")); schoolFriendMap.put(this.schoolFriend,list); } }else{ List<String> lis = new ArrayList<String>(); lis.add((String)map.get("USERID")); schoolFriendMap.put((String)map.get("SCHOOL_ID"),lis); } } //取住房信息字段 HOUSE_ID , HOUSE_ADDR if(StringUtils.isNoneEmpty((String)map.get("HOUSE_ID")) && StringUtils.isNoneEmpty((String)map.get("HOUSE_ADDR"))){ //插入毕业院校 neo4jRelationInfoRepository.generateGraphDataRelation_JUZ((String)map.get("USERID"),(String)map.get("HOUSE_ID"),(String)map.get("USERID")+"->"+(String)map.get("HOUSE_ID")); //处理邻居关系 if(neighborMap.containsKey((String)map.get("HOUSE_ID"))){ List<String> list = neighborMap.get((String)map.get("HOUSE_ID")); if(!list.contains((String)map.get("USERID"))){ list.add((String)map.get("USERID")); neighborMap.put((String)map.get("HOUSE_ID"),list); } }else{ List<String> lis = new ArrayList<String>(); lis.add((String)map.get("USERID")); neighborMap.put((String)map.get("HOUSE_ID"),lis); } } //取论文信息字段 LW_ID , LW_TITLE if(StringUtils.isNoneEmpty((String)map.get("LW_ID")) && StringUtils.isNoneEmpty((String)map.get("LW_TITLE"))){ //插入毕业院校 neo4jRelationInfoRepository.generateGraphDataRelation_PA((String)map.get("USERID"),(String)map.get("LW_ID"),(String)map.get("USERID")+"->"+(String)map.get("LW_ID")); //处理合作者关系 if(collaboratorMap.containsKey((String)map.get("LW_ID"))){ List<String> list = collaboratorMap.get((String)map.get("LW_ID")); if(!list.contains((String)map.get("USERID"))){ list.add((String)map.get("USERID")); collaboratorMap.put((String)map.get("LW_ID"),list); } }else{ List<String> lis = new ArrayList<String>(); lis.add((String)map.get("USERID")); collaboratorMap.put((String)map.get("LW_ID"),lis); } } } } //开始剔除长度为1的数据,没有对应的关系。 schoolFriendMap.forEach((key,value) -> { //独立个体无法形成关系,剔除 if(((List<String>)value).size()<=1){ schoolFriendMap.remove(key); } }); workTogetherMap.forEach((key,value) -> { //独立个体无法形成关系,剔除 if(((List<String>)value).size()<=1){ workTogetherMap.remove(key); } }); neighborMap.forEach((key,value) -> { //独立个体无法形成关系,剔除 if(((List<String>)value).size()<=1){ neighborMap.remove(key); } }); collaboratorMap.forEach((key,value) -> { //独立个体无法形成关系,剔除 if(((List<String>)value).size()<=1){ collaboratorMap.remove(key); } }); //开始遍历校友关系数据,生成节点。 schoolFriendMap.forEach((key,value) -> { List<String> li = (List<String>)value; for(int st = 0;st<li.size() ; st++){ for(int ss = li.size()-1;ss>st;ss--){ System.out.println("生成关系"+li.get(st)+"->"+li.get(ss)); neo4jRelationInfoRepository.generateGraphDataRelation_XY(li.get(st),li.get(ss),"<->"); } } }); //同事关系 workTogetherMap.forEach((key,value) -> { List<String> li = (List<String>)value; for(int st = 0;st<li.size() ; st++){ for(int ss = li.size()-1;ss>st;ss--){ System.out.println("生成关系"+li.get(st)+"->"+li.get(ss)); neo4jRelationInfoRepository.generateGraphDataRelation_TS(li.get(st),li.get(ss),"<->"); } } }); //邻居关系 neighborMap.forEach((key,value) -> { List<String> li = (List<String>)value; for(int st = 0;st<li.size() ; st++){ for(int ss = li.size()-1;ss>st;ss--){ System.out.println("生成关系"+li.get(st)+"->"+li.get(ss)); neo4jRelationInfoRepository.generateGraphDataRelation_LJ(li.get(st),li.get(ss),"<->"); } } }); //合作者关系 collaboratorMap.forEach((key,value) -> { List<String> li = (List<String>)value; for(int st = 0;st<li.size() ; st++){ for(int ss = li.size()-1;ss>st;ss--){ System.out.println("生成关系"+li.get(st)+"->"+li.get(ss)); neo4jRelationInfoRepository.generateGraphDataRelation_LWHZ(li.get(st),li.get(ss),"<->"); } } }); System.out.println("总共花费时间:" + (System.currentTimeMillis() - start) + "ms"); }
然后Control中对外暴露三个接口, 1.初始化节点, 2.初始化关系。 就可以了。
然后通过浏览器直接可以看到图数据的结构,分享下我做出来的效果。
至此一个从数据库存入到neo4j数据库中,然后可以对外开放一个查询的接口,返回与之有关系并且深度为1的数据,方便其他系统展示,接口如下:
/** * web查询接口,查询与当前节点为1的数据---根据节点编号,适用于登录第二次节点点击之后的查询。 */ @Query(value = "match(a)-[r]-(b) where a.ids={qid} return a,r,b") List<Map<String,String>> queryRelationDataByIds(@Param("qid") String qid); /** * web查询接口,用户登录之后第一次查询,需根据身份证号码查询。第二次查询就直接会调用上一个查询。 */ @Query(value = "match(a:pe)-[r]-(b) where a.cardNo={cardNo} return a,r,b") List<Map<String,String>> queryRelationDataByCardNo(@Param("cardNo") String cardNo);