-
什么是neo4j
Neo4j是用Java实现的开源NoSQL图数据库。
Neo4j实现了专业数据库级别的图数据模型的存储,提供了完整的数据库特性,包括ACID事务的支持、集群的支持、备份和故障转移等。
Neo4j提供了申明式的查询语言Cypher,它类似于关系型数据库中的SQL语言,其具有表现力丰富、使用简单、查询效率高、高扩展性等特点。
Neo4j有两个不同的版本,分别是:
- 社区版(Community Edition)
- 具备了基本功能的版本,功能较为完整,没有提供企业服务。
- 企业版(Experience Edition)
- 企业版相对于社区版而言,增加了一些功能,如:集群、高级监控、高级缓存、在线备份等功能。 :::info 建议:开发环境使用社区版,生产环境使用企业版。 说明:企业版从3.2版本开始支持集群,无地理位置限制并且可以做到事务的ACID特性。
- 社区版(Community Edition)
1 官网安装部署
Neo4j支持众多平台的部署安装,如:Windows、Mac、Linux等系统。Neo4j是基于Java平台的,所以部署安装前先保证已经安装了Java虚拟机。 安装命令如下:
docker run \
-d \
--restart=always \
--name neo4j \
-p 7474:7474 \
-p 7687:7687 \
-v neo4j:/data \
neo4j:4.4.5
# 7474是web管理工具的端口,7687是neo4j协议端口进行数据通信
2 登陆后台系统
打开浏览器,输入地址:neo4j.yourproject.com/browser/
neo4j123
3 Cypher 语句
3.1 创建数据
//查询所有数据
MATCH (n) RETURN n
//删除所有节点和关系,慎用!
MATCH (n) DETACH DELETE n
CREATE (n {name: $value}) RETURN n //创建节点,该节点具备name属性,n为该节点的变量,创建完成后返回该节点
CREATE (n:$Tag {name: $value}) //创建节点,指定标签
CREATE (n)-[r:KNOWS]->(m) //创建n指向m的关系,并且指定关系类型为:KNOWS
//示例
CREATE (n {name:'迪士尼营业部'})
CREATE (n:AGENCY {name:'航头营业部'})
//创建浦东新区转运中心、上海转运中心节点,并且创建关系为:IN_LINE,创建完成后返回节点和关系
//TLT -> Two Level Transport(二级转运中心)
//OLT -> One Level Transport(一级转运中心)
CREATE (n:TLT {name:'浦东新区转运中心'}) -[r:IN_LINE]-> (m:OLT {name:'上海转运中心'}) RETURN n,r,m
//关系也是可以反向,并且可以为关系中指定属性
CREATE (n:TLT {name:'浦东新区转运中心'}) <-[r:OUT_LINE]- (m:OLT {name:'上海转运中心'}) RETURN n,r,m
3.2 查询数据
MATCH (n) RETURN n //查询所有的数据,数据量大是勿用
MATCH (n:AGENCY) RETURN n //查询所有的网点(AGENCY)
MATCH (n:OLT {name: "北京市转运中心"}) -- (m) RETURN n,m //查询所有与“北京市转运中心”有关系的节点
MATCH (n:OLT {name:"北京市转运中心"}) --> (m:OLT) RETURN n,m //查询所有"北京市转运中心"关联的一级转运中心
MATCH (n:OLT {name:"北京市转运中心"}) -[r:IN_LINE]- (m) RETURN n,r,m //可以指定关系标签查询
MATCH p = (n:OLT {name:"北京市转运中心"}) --> (m:OLT) RETURN p //将查询赋值与变量
//通过 type()函数查询关系类型
MATCH (n:OLT {name:"北京市转运中心"}) -[r]-> (m:OLT {name:"南京市转运中心"}) RETURN type(r)
3.3 关系深度查询
//查询【北京市转运中心】关系中深度为1~2层关系的节点
MATCH (n:OLT {name:"北京市转运中心"}) -[*1..2]->(m) RETURN *
//也可以这样
MATCH (n:OLT {name:"北京市转运中心"}) -[*..2]->(m) RETURN *
//也可以通过变量的方式查询
MATCH path = (n:OLT {name:"北京市转运中心"}) -[*..2]->(m)
RETURN path
//查询关系,relationships()获取结果中的关系,WITH向后传递数据
MATCH path = (n:OLT {name:"北京市转运中心"}) -[*..2]->(m)
WITH n,m, relationships(path) AS r
RETURN r
//查询两个网点之间所有的路线,最大深度为6,可以查询到2条路线
MATCH path = (n:AGENCY) -[*..6]->(m:AGENCY)
WHERE n.name = "北京市昌平区定泗路" AND m.name = "上海市浦东新区南汇"
RETURN path
//查询两个网点之间最短路径,查询深度最大为10
MATCH path = shortestPath((n:AGENCY) -[*..10]->(m:AGENCY))
WHERE n.name = "北京市昌平区定泗路" AND m.name = "上海市浦东新区南汇"
RETURN path
//查询两个网点之间所有的路线中成本最低的路线,最大深度为10(如果成本相同,转运节点最少)
MATCH path = (n:AGENCY) -[*..10]->(m:AGENCY)
WHERE n.name = "北京市昌平区定泗路" AND m.name = "上海市浦东新区南汇"
UNWIND relationships(path) AS r
WITH sum(r.cost) AS cost, path
RETURN path ORDER BY cost ASC, LENGTH(path) ASC LIMIT 1
//UNWIND是将列表数据展开操作
//sum()是聚合统计函数,类似还有:avg()、max()、min()等
3.4 分页查询
//分页查询网点,按照bid正序排序,每页查询2条数据,第一页
MATCH (n:AGENCY)
RETURN n ORDER BY n.bid ASC SKIP 0 LIMIT 2
//第二页
MATCH (n:AGENCY)
RETURN n ORDER BY n.bid ASC SKIP 2 LIMIT 2
3.5 更新数据
// 更新/设置 属性
MATCH (n:AGENCY {name:"北京市昌平区新龙城"})
SET n.address = "龙跃苑四区3号楼底商101号"
RETURN n
//通过remove移除属性
MATCH (n:AGENCY {name:"北京市昌平区新龙城"}) REMOVE n.address RETURN n
//没有address属性的增加属性
MATCH (n:AGENCY) WHERE n.address IS NULL SET n.address = "暂无地址" RETURN n
3.6 删除数据
//删除节点
MATCH (n:AGENCY {name:"航头营业部"}) DELETE n
//有关系的节点是不能直接删除的
MATCH (n:AGENCY {name:"北京市昌平区新龙城"}) DELETE n
//删除节点和关系
MATCH (n:AGENCY {name:"北京市昌平区新龙城"}) DETACH DELETE n
//删除所有节点和关系,慎用!
MATCH (n) DETACH DELETE n
3.7 索引
//创建索引语法:
//OPTIONS子句指定索引提供程序和配置。
CREATE [TEXT] INDEX [index_name] [IF NOT EXISTS]
FOR (n:LabelName)
ON (n.propertyName)
[OPTIONS "{" option: value[, ...] "}"]
//示例:
CREATE TEXT INDEX agency_index_bid IF NOT EXISTS FOR (n:AGENCY) ON (n.bid)
//删除索引语法:
DROP INDEX index_name
//示例:
DROP INDEX agency_index_bid
4 SDN 对接 spring
Spring Data Neo4j简称SDN,是Spring对Neo4j数据库操作的封装,其底层基于neo4j-java-driver实现。
4.1 添加依赖及配置
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-neo4j</artifactId>
</dependency>
server:
port: 9902
logging:
level:
org.springframework.data.neo4j: debug
spring:
application:
name: sl-express-sdn
mvc:
pathmatch:
#解决异常:swagger Failed to start bean 'documentationPluginsBootstrapper'; nested exception is java.lang.NullPointerException
#因为Springfox使用的路径匹配是基于AntPathMatcher的,而Spring Boot 2.6.X使用的是PathPatternMatcher
matching-strategy: ant_path_matcher
data:
neo4j:
database: neo4j
neo4j:
authentication:
username: neo4j
password: neo4j123
uri: neo4j://192.168.150.101:7687
4.2 编写相关代码
-
可以把 neo4j 单独封装一个服务,作为基础对接服务来用
4.2.1 编写启动类、entity、dto、相关 enums、Repository(相当于mysql 的mapper类)
- 编写基础 base 类,并继承该类,注意关注以下注解
@Id:标识实体类的唯一标识符字段,标记该字段作为节点的主键,对应 Neo4j 数据库中的节点ID @GeneratedValue:与 @Id 注解一起使用,表示该ID值由 Neo4j 自动生成,而不是由应用程序提供 @Node("OLT"):将 Java 类标记为 Neo4j 中的节点实体@Data @SuperBuilder(toBuilder = true) @NoArgsConstructor @AllArgsConstructor public abstract class BaseEntity { @Id @GeneratedValue @ApiModelProperty(value = "Neo4j ID", hidden = true) private Long id; @ApiModelProperty(value = "业务id", required = true) private Long bid; @ApiModelProperty(value = "名称", required = true) private String name; @ApiModelProperty(value = "电话", required = true) private String phone; @ApiModelProperty(value = "地址", required = true) private String address; @ApiModelProperty(value = "位置坐标, x: 纬度,y: 经度", required = true) private Point location; //机构类型 public abstract OrganTypeEnum getAgencyType(); }/** * 一级转运中心实体 (OneLevelTransportEntity) */ @Node("OLT") @Data @ToString(callSuper = true) @SuperBuilder(toBuilder = true) @NoArgsConstructor public class OLTEntity extends BaseEntity { @Override public OrganTypeEnum getAgencyType() { return OrganTypeEnum.OLT; } }public enum OrganTypeEnum implements BaseEnum { OLT(1, "一级转运中心"), TLT(2, "二级转运中心"), AGENCY(3, "网点"); /** * 类型编码 */ private final Integer code; /** * 类型值 */ private final String value; OrganTypeEnum(Integer code, String value) { this.code = code; this.value = value; } public Integer getCode() { return code; } public String getValue() { return value; } public static OrganTypeEnum codeOf(Integer code) { return EnumUtil.getBy(OrganTypeEnum::getCode, code); } }- 实现相关 dto
@Data public class OrganDTO { @Alias("bid") //业务id作为id进行封装 @ApiModelProperty(value = "机构id", required = true) private Long id; @ApiModelProperty(value = "名称", required = true) private String name; @ApiModelProperty(value = "类型,1:一级转运,2:二级转运,3:网点", required = true) private Integer type; @ApiModelProperty(value = "电话", required = true) private String phone; @ApiModelProperty(value = "地址", required = true) private String address; @ApiModelProperty(value = "纬度", required = true) private Double latitude; @ApiModelProperty(value = "经度", required = true) private Double longitude; }@Data public class TransportLineNodeDTO { @ApiModelProperty(value = "节点列表", required = true) private List<OrganDTO> nodeList = new ArrayList<>(); @ApiModelProperty(value = "路线成本", required = true) private Double cost = 0d; }- 实现 Repository,仅仅需要继承 Neo4jRepository 即可,直接调用
public interface AgencyRepository extends Neo4jRepository<AgencyEntity, Long> { /** * 根据bid查询 * * @param bid 业务id * @return 网点数据 */ AgencyEntity findByBid(Long bid); /** * 根据bid删除 * * @param bid 业务id * @return 删除的数据条数 */ Long deleteByBid(Long bid); }
4.3 JPA 规则表格
- 接口方法命名可参照以下内容进行编写
| 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) |
4.4 定制化语句
public class TransportLineRepositoryImpl implements TransportLineRepository {
@Resource
private Neo4jClient neo4jClient;
@Override
public TransportLineNodeDTO findShortestPath(AgencyEntity start, AgencyEntity end) {
// 1.定义执行的 cypher 语句
String type = AgencyEntity.class.getAnnotation(Node.class).value()[0];
String cypherQuery = StrUtil.format("MATCH path = shortestPath((start:{}) -[*..10]-> (end:{}))\n" +
"WHERE start.bid = $startId AND end.bid = $endId \n" +
"RETURN path", type, type);
// 2.执行语句
Optional<TransportLineNodeDTO> optional = this.neo4jClient.query(cypherQuery)
.bind(start.getBid()).to("startBid") // 绑定参数
.bind(end.getBid()).to("endBid") // 绑定参数
.fetchAs(TransportLineNodeDTO.class) // 返回值映射的对象
.mappedBy(((typeSystem, record) -> { //手动映射
System.out.println(record);
PathValue pathValue = (PathValue) record.get(0);
Path path = pathValue.asPath();
TransportLineNodeDTO dto = new TransportLineNodeDTO();
List<OrganDTO> nodeList = StreamUtil.of(path.nodes()).map(node -> {
Map<String, Object> map = node.asMap();
OrganDTO organDTO = BeanUtil.toBeanIgnoreError(map, OrganDTO.class);
Object location = map.get("location");
// 设置经纬度
organDTO.setLongitude(BeanUtil.getProperty(location, "x"));
organDTO.setLatitude(BeanUtil.getProperty(location, "y"));
OrganTypeEnum organTypeEnum = OrganTypeEnum.valueOf(CollUtil.getFirst(
node.labels()
));
organDTO.setType(organTypeEnum.getCode());
return organDTO;
}).collect(Collectors.toList());
double cost = StreamUtil.of(path.relationships()).mapToDouble(relation ->
Convert.toDouble(relation.asMap().get("cost"))).sum();
dto.setNodeList(nodeList);
dto.setCost(cost);
return dto;
})).one();
return optional.orElse(null);
}