近期由於工作需要,需要做一個人口關系大圖的存儲及檢索方案,我們主要的數據對象有:人口(年齡,身份證號碼,性別。。) ;學校信息(學校地址,學校名稱,學校級別,學校下邊的年級班級。。);就職信息(公司名稱,公司地址,公司企業信用代碼。。)以及論文發布信息(論文主題,參與人,發布時間)
問題思考:
- 針對這類人口關系大圖,或者叫圖譜的數據一般都是非關系型,而且有多鍾關系的存在,比如張三跟李四兩個人,可能是同學,同事,老鄉,配偶,夫妻,等多種關系,那一般如果采取關系型數據庫(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);