数据库之Neo4j 图数据库学习笔记

218 阅读2分钟
  • 什么是neo4j

    Neo4j是用Java实现的开源NoSQL图数据库。

    Neo4j实现了专业数据库级别的图数据模型的存储,提供了完整的数据库特性,包括ACID事务的支持、集群的支持、备份和故障转移等。

    Neo4j提供了申明式的查询语言Cypher,它类似于关系型数据库中的SQL语言,其具有表现力丰富、使用简单、查询效率高、高扩展性等特点。 image.png

    Neo4j有两个不同的版本,分别是:

    • 社区版(Community Edition)
      • 具备了基本功能的版本,功能较为完整,没有提供企业服务。
    • 企业版(Experience Edition)
      • 企业版相对于社区版而言,增加了一些功能,如:集群、高级监控、高级缓存、在线备份等功能。 :::info 建议:开发环境使用社区版,生产环境使用企业版。 说明:企业版从3.2版本开始支持集群,无地理位置限制并且可以做到事务的ACID特性。

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 规则表格

  • 接口方法命名可参照以下内容进行编写
KeywordSampleCypher snippet
AfterfindByLaunchDateAfter(Date date)n.launchDate > date
BeforefindByLaunchDateBefore(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)
InfindByNameIn(Iterable names)n.name IN names
BetweenfindByScoreBetween(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
StartingWithfindByNameStartingWith(String nameStart)n.name STARTS WITH nameStart
EndingWithfindByNameEndingWith(String nameEnd)n.name ENDS WITH nameEnd
ExistsfindByNameExists()EXISTS(n.name)
TruefindByActivatedIsTrue()n.activated = true
FalsefindByActivatedIsFalse()NOT(n.activated = true)
IsfindByNameIs(String name)n.name = name
NotNullfindByNameNotNull()NOT(n.name IS NULL)
NullfindByNameNull()n.name IS NULL
GreaterThanfindByScoreGreaterThan(double score)n.score > score
GreaterThanEqualfindByScoreGreaterThanEqual(double score)n.score >= score
LessThanfindByScoreLessThan(double score)n.score < score
LessThanEqualfindByScoreLessThanEqual(double score)n.score <= score
LikefindByNameLike(String name)n.name =~ name
NotLikefindByNameNotLike(String name)NOT(n.name =~ name)
NearfindByLocationNear(Distance distance, Point point)distance( point(n),point({latitude:lat, longitude:lon}) ) < distance
RegexfindByNameRegex(String regex)n.name =~ regex
AndfindByNameAndDescription(String name, String description)n.name = name AND n.description = description
OrfindByNameOrDescription(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);
    }