图数据库Neo4j——SpringBoot使用Neo4j & 简单增删改查 & 复杂查询初步
作者:mmseoamin日期:2023-12-25

在这里插入图片描述

前言


图形数据库是专门用于存储图形数据的数据库,它使用图形模型来存储数据,并且支持复杂的图形查询。常见的图形数据库有Neo4j、OrientDB等。

Neo4j是用Java实现的开源NoSQL图数据库,本篇博客介绍如何在SpringBoot中使用Neo4j图数据库,如何进行简单的增删改查,以及如何进行复杂的查询。

本篇博客相关代码的git网址如下:

https://gitee.com/pet365/spring-boot-neo4j

关于Neo4j的博客文章如下:

  • 图数据库Neo4j——Neo4j简介、数据结构 & Docker版本的部署安装 & Cypher语句的入门

    目录

    • 前言
    • 引出
    • springBoot整合
      • 1、引入依赖
      • 2、配置文件
      • 3、实体类定义
      • 4、dao继承Neo4jRepository
      • 复杂查询
        • 最短路径查询
        • 最小成本查询
        • 总结

          引出


          1.Neo4j是用Java实现的开源NoSQL图数据库;

          2.SpringBoot使用Neo4j,继承Neo4jRepository进行简单增删改查;

          3.使用Neo4jClient进行复杂的查询;

          springBoot整合

          1、引入依赖

          
                  
                      org.springframework.boot
                      spring-boot-starter-data-neo4j
                  
          

          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

          进行自定义查询:

          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)

          在这里插入图片描述

          package com.tianju.mapper;
          import com.tianju.entity.AgencyEntity;
          import org.mapstruct.Mapper;
          import org.springframework.data.neo4j.repository.Neo4jRepository;
          /**
           * 网点的mapper,比如菜鸟驿站
           */
          @Mapper
          public interface AgencyMapper extends Neo4jRepository {
              /**
               * 根据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 com.tianju.mapper.impl;
          import cn.hutool.core.bean.BeanUtil;
          import cn.hutool.core.collection.CollUtil;
          import cn.hutool.core.map.MapUtil;
          import cn.hutool.core.util.StrUtil;
          import cn.hutool.db.meta.Column;
          import com.tianju.dto.OrganDTO;
          import com.tianju.dto.TransportLineNodeDTO;
          import com.tianju.entity.AgencyEntity;
          import com.tianju.enums.OrganTypeEnum;
          import com.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.Component;
          import java.util.Map;
          import java.util.Optional;
          @Component
          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 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 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 map = relationship.asMap();
                                  Double cost = MapUtil.get(map, "cost", Double.class);
                                  transportLineNodeDTO.setCost(cost + transportLineNodeDTO.getCost());
                              });
                              System.out.println("transportLineNodeDTO: "+transportLineNodeDTO);
                              return transportLineNodeDTO;
                          });
                  Optional one = recordFetchSpec.one(); // Optional,1.8提供的,可以处理null的情况
                  return one.orElse(null); // 如果为null,就返回null,如果不是null,就返回结果
              }
          }
          

          最小成本查询

          在这里插入图片描述

          package com.tianju.mapper.impl;
          import cn.hutool.core.bean.BeanUtil;
          import cn.hutool.core.collection.CollUtil;
          import cn.hutool.core.map.MapUtil;
          import cn.hutool.core.util.StrUtil;
          import cn.hutool.db.meta.Column;
          import com.tianju.dto.OrganDTO;
          import com.tianju.dto.TransportLineNodeDTO;
          import com.tianju.entity.AgencyEntity;
          import com.tianju.enums.OrganTypeEnum;
          import com.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.Component;
          import java.util.Map;
          import java.util.Optional;
          @Component
          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 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 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 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进行复杂的查询;