前言
图形数据库是专门用于存储图形数据的数据库,它使用图形模型来存储数据,并且支持复杂的图形查询。常见的图形数据库有neo4j、OrientDB等。
Neo4j是用Java实现的开源NoSQL图数据库,本篇博客介绍如何在SpringBoot中使用Neo4j图数据库,如何进行简单的增删改查,以及如何进行复杂的查询。
本篇博客相关代码的git网址如下:
https://gitee.***/pet365/spring-boot-neo4j
关于Neo4j的博客文章如下:
- 图数据库Neo4j——Neo4j简介、数据结构 & Docker版本的部署安装 & Cypher语句的入门
引出
1.Neo4j是用Java实现的开源NoSQL图数据库;
2.SpringBoot使用Neo4j,继承Neo4jRepository进行简单增删改查;
3.使用Neo4jClient进行复杂的查询;
springBoot整合
1、引入依赖
<!-- neo4j的包-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-neo4j</artifactId>
</dependency>
2、配置文件
server:
port: 9902
logging:
level:
org.springframework.data.neo4j: debug
spring:
application:
name: spring-neo4j
data:
neo4j:
database: neo4j
neo4j:
authentication:
username: neo4j
password: neo4j123
uri: neo4j://192.168.150.101:7687
3、实体类定义
提取抽象类
不同的节点类,网点、一级转运中心、二级转运中心
4、dao继承Neo4jRepository
进行自定义查询:
Keyword | Sample | Cypher snippet |
---|---|---|
After | findByLaunchDateAfter(Date date) | n.launchDate > date |
Before | findByLaunchDateBefore(Date date) | n.launchDate < date |
Containing (String) | findByNameContaining(String namePart) | n.name CONTAINS namePart |
Containing (Collection) | findByEmailAddressesContains(Collection addresses) findByEmailAddressesContains(String address) | ANY(collectionFields IN [addresses] WHERE collectionFields in n.emailAddresses) ANY(collectionFields IN address WHERE collectionFields in n.emailAddresses) |
In | findByNameIn(Iterable names) | n.name IN names |
Between | findByScoreBetween(double min, double max) findByScoreBetween(Range range) | n.score >= min AND n.score <= max Depending on the Range definition n.score >= min AND n.score <= max or n.score > min AND n.score < max |
StartingWith | findByNameStartingWith(String nameStart) | n.name STARTS WITH nameStart |
EndingWith | findByNameEndingWith(String nameEnd) | n.name ENDS WITH nameEnd |
Exists | findByNameExists() | EXISTS(n.name) |
True | findByActivatedIsTrue() | n.activated = true |
False | findByActivatedIsFalse() | NOT(n.activated = true) |
Is | findByNameIs(String name) | n.name = name |
NotNull | findByNameNotNull() | NOT(n.name IS NULL) |
Null | findByNameNull() | n.name IS NULL |
GreaterThan | findByScoreGreaterThan(double score) | n.score > score |
GreaterThanEqual | findByScoreGreaterThanEqual(double score) | n.score >= score |
LessThan | findByScoreLessThan(double score) | n.score < score |
LessThanEqual | findByScoreLessThanEqual(double score) | n.score <= score |
Like | findByNameLike(String name) | n.name =~ name |
NotLike | findByNameNotLike(String name) | NOT(n.name =~ name) |
Near | findByLocationNear(Distance distance, Point point) | distance( point(n),point({latitude:lat, longitude:lon}) ) < distance |
Regex | findByNameRegex(String regex) | n.name =~ regex |
And | findByNameAndDescription(String name, String description) | n.name = name AND n.description = description |
Or | findByNameOrDescription(String name, String description) | n.name = name OR n.description = description (Cannot be used to OR nested properties) |
package ***.tianju.mapper;
import ***.tianju.entity.AgencyEntity;
import org.mapstruct.Mapper;
import org.springframework.data.neo4j.repository.Neo4jRepository;
/**
* 网点的mapper,比如菜鸟驿站
*/
@Mapper
public interface AgencyMapper extends Neo4jRepository<AgencyEntity,Long> {
/**
* 根据bid 查询
* @param bid 业务id
* @return 网点数据
*/
AgencyEntity findByBid(Long bid);
/**
* 根据bid删除
*
* @param bid 业务id
* @return 删除的数据条数
*/
Long deleteByBid(Long bid);
}
复杂查询
最短路径查询
//查询两个网点之间最短路径,查询深度最大为10
MATCH path = shortestPath((n:AGENCY) -[*..10]->(m:AGENCY))WHERE n.name = "北京市昌平区定泗路" AND m.name = "上海市浦东新区南汇"RETURN path
package ***.tianju.mapper.impl;
import ***.hutool.core.bean.BeanUtil;
import ***.hutool.core.collection.CollUtil;
import ***.hutool.core.map.MapUtil;
import ***.hutool.core.util.StrUtil;
import ***.hutool.db.meta.Column;
import ***.tianju.dto.OrganDTO;
import ***.tianju.dto.TransportLineNodeDTO;
import ***.tianju.entity.AgencyEntity;
import ***.tianju.enums.OrganTypeEnum;
import ***.tianju.mapper.TransportLineRepository;
import org.neo4j.driver.internal.InternalPoint2D;
import org.neo4j.driver.types.Path;
import org.neo4j.driver.types.Relationship;
import org.springframework.data.neo4j.core.schema.Node;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.neo4j.core.Neo4jClient;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.***ponent;
import java.util.Map;
import java.util.Optional;
@***ponent
public class TransportLineRepositoryImpl implements TransportLineRepository {
@Autowired
private Neo4jClient neo4jClient;
/**
* 查询最短路线
* @param start 开始网点
* @param end 结束网点
* @return
*/
@Override
public TransportLineNodeDTO findShortestPath(AgencyEntity start, AgencyEntity end) {
// 获取网点数据在Neo4j中的类型 @Node("AGENCY") @Node("OLT")
String type = AgencyEntity.class.getAnnotation(Node.class).value()[0];
// 构造Sql语句 $startId
// String cql = "MATCH path = shortestPath((n:AGENCY) -[*..10]->(m:AGENCY))\n" +
// "WHERE n.bid = $startId AND m.bid = $endId\n" +
// "RETURN path";
String cql = StrUtil.format("MATCH path = shortestPath((n:{}) -[*..10]->(m:{})) " +
"WHERE n.bid = $startId AND m.bid = $endId " +
"RETURN path",type,type);
// 执行自定义查询
Neo4jClient.RecordFetchSpec<TransportLineNodeDTO> recordFetchSpec = neo4jClient.query(cql)
.bind(start.getBid()).to("startId") // 替换 $startId
.bind(end.getBid()).to("endId") // 替换 $endId
.fetchAs(TransportLineNodeDTO.class) // 设置响应类型,指定为 TransportLineNodeDTO 类型
.mappedBy((typeSystem, record) -> { // 设置结果集映射
Path path = record.get(0).asPath();// 得到第一条路线
TransportLineNodeDTO transportLineNodeDTO = new TransportLineNodeDTO();
path.nodes().forEach(node -> { // 将每个节点信息封装成一个 OrganDto
// 获得节点的 键值对 address: 上海市转运中心;bid:8002
Map<String, Object> map = node.asMap();
// {name=北京市昌平区定泗路,
// location=Point{srid=4326, x=116.37212849638287, y=40.11765281246394},
// address=北七家镇定泗路苍龙街交叉口, bid=100280, phone=010-86392987}
System.out.println("map: "+map);
// 把键值对转换成对象 OrganDTO
OrganDTO organDTO = BeanUtil.toBeanIgnoreError(map, OrganDTO.class);
// organDTO:
// OrganDTO(id=100280, name=北京市昌平区定泗路, type=null, phone=010-86392987,
// address=北七家镇定泗路苍龙街交叉口, latitude=null, longitude=null)
// type,latitude,longitude 没有映射成功
System.out.println("organDTO: "+organDTO);
// 获得标签的名称 OLT,TLT,
String first = CollUtil.getFirst(node.labels());
// 根据OLT获得枚举类型 OLT(1, "一级转运中心"),
OrganTypeEnum organTypeEnum = OrganTypeEnum.valueOf(first);
// 再获得枚举类型的 code :1、2、3
organDTO.setType(organTypeEnum.getCode()); // 设置类型的映射
// 经纬度 "location": point({srid:4326, x:121.59815370294322, y:31.132409729356993})
InternalPoint2D location = MapUtil.get(map, "location", InternalPoint2D.class); // 经纬度 BeanUtil.getProperty(map.get("location"),"x");
organDTO.setLatitude(location.x()); // 设置经纬度映射
organDTO.setLongitude(location.y()); // 经纬度映射
// OrganDTO(id=100280, name=北京市昌平区定泗路, type=3,
// phone=010-86392987, address=北七家镇定泗路苍龙街交叉口,
// latitude=116.37212849638287, longitude=40.11765281246394)
System.out.println("organDTO: "+organDTO);
transportLineNodeDTO.getNodeList().add(organDTO);
});
System.out.println("transportLineNodeDTO: "+transportLineNodeDTO);
path.relationships().forEach(relationship -> {
// 路径下面的关系
Map<String, Object> map = relationship.asMap();
Double cost = MapUtil.get(map, "cost", Double.class);
transportLineNodeDTO.setCost(cost + transportLineNodeDTO.getCost());
});
System.out.println("transportLineNodeDTO: "+transportLineNodeDTO);
return transportLineNodeDTO;
});
Optional<TransportLineNodeDTO> one = recordFetchSpec.one(); // Optional,1.8提供的,可以处理null的情况
return one.orElse(null); // 如果为null,就返回null,如果不是null,就返回结果
}
}
最小成本查询
package ***.tianju.mapper.impl;
import ***.hutool.core.bean.BeanUtil;
import ***.hutool.core.collection.CollUtil;
import ***.hutool.core.map.MapUtil;
import ***.hutool.core.util.StrUtil;
import ***.hutool.db.meta.Column;
import ***.tianju.dto.OrganDTO;
import ***.tianju.dto.TransportLineNodeDTO;
import ***.tianju.entity.AgencyEntity;
import ***.tianju.enums.OrganTypeEnum;
import ***.tianju.mapper.TransportLineRepository;
import org.neo4j.driver.internal.InternalPoint2D;
import org.neo4j.driver.types.Path;
import org.neo4j.driver.types.Relationship;
import org.springframework.data.neo4j.core.schema.Node;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.neo4j.core.Neo4jClient;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.***ponent;
import java.util.Map;
import java.util.Optional;
@***ponent
public class TransportLineRepositoryImpl implements TransportLineRepository {
@Autowired
private Neo4jClient neo4jClient;
@Override
public TransportLineNodeDTO findCostLeastPath(AgencyEntity start, AgencyEntity end) {
String type = AgencyEntity.class.getAnnotation(Node.class).value()[0];
String cqlB = "MATCH path = (n:{}) -[*..10]->(m:{}) " +
"WHERE n.bid = $startId AND m.bid = $endId " +
"UNWIND relationships(path) AS r " +
"WITH sum(r.cost) AS cost, path " +
"RETURN path ORDER BY cost ASC, LENGTH(path) ASC LIMIT 1";
String cql = StrUtil.format(cqlB, type,type);
Optional<TransportLineNodeDTO> one = neo4jClient.query(cql)
.bind(start.getBid()).to("startId")
.bind(end.getBid()).to("endId")
.fetchAs(TransportLineNodeDTO.class)
.mappedBy(((typeSystem, record) -> {
Path path = record.get(0).asPath();
TransportLineNodeDTO transportLineNodeDTO = new TransportLineNodeDTO();
path.nodes().forEach(node -> {
Map<String, Object> map = node.asMap();
OrganDTO organDTO = BeanUtil.toBeanIgnoreError(map, OrganDTO.class);
// 获得标签的名称 OLT,TLT,
String first = CollUtil.getFirst(node.labels());
// 根据OLT获得枚举类型 OLT(1, "一级转运中心"),
OrganTypeEnum organTypeEnum = OrganTypeEnum.valueOf(first);
// 再获得枚举类型的 code :1、2、3
organDTO.setType(organTypeEnum.getCode()); // 设置类型的映射
// 经纬度 "location": point({srid:4326, x:121.59815370294322, y:31.132409729356993})
InternalPoint2D location = MapUtil.get(map, "location", InternalPoint2D.class); // 经纬度 BeanUtil.getProperty(map.get("location"),"x");
organDTO.setLatitude(location.x()); // 设置经纬度映射
organDTO.setLongitude(location.y()); // 经纬度映射
transportLineNodeDTO.getNodeList().add(organDTO);
});
path.relationships().forEach(relationship -> {
// 路径下面的关系
Map<String, Object> map = relationship.asMap();
Double cost = MapUtil.get(map, "cost", Double.class);
transportLineNodeDTO.setCost(cost + transportLineNodeDTO.getCost());
});
return transportLineNodeDTO;
})).one();
return one.orElse(null);
}
private void findShortestPathMy(){
String cql = "MATCH path = shortestPath((n:AGENCY) -[*..10]->(m:AGENCY)) " +
"WHERE n.bid = 210127 AND m.bid = 100260 " +
"RETURN path";
// 执行自定义查询
Neo4jClient.UnboundRunnableSpec query = neo4jClient.query(cql);
ThreadPoolTaskExecutor threadPoolTaskExecutor = new ThreadPoolTaskExecutor();
}
}
总结
1.Neo4j是用Java实现的开源NoSQL图数据库;
2.SpringBoot使用Neo4j,继承Neo4jRepository进行简单增删改查;
3.使用Neo4jClient进行复杂的查询;