SpringDataNeo4j使用详解

server/2024/11/23 17:00:03/

SDN快速入门

Spring Data Neo4j简称SDN,是Spring对Neo4j数据库操作的封装,其底层基于neo4j-java-driver实现。
我们使用的版本为:6.2.3,官方文档:
下面我们将基于项目中的运输路线业务进行学习,例如:
:::info
【迪士尼营业部】-> 【浦东区转运中心】 -> 【上海转运中心】 -> 【北京转运中心】-> 【昌平区转运中心】-> 【金燕龙营业部】
:::

1、创建工程

创建工程,sl-express-sdn,导入依赖:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><parent><groupId>com.sl-express</groupId><artifactId>sl-express-parent</artifactId><version>1.4</version></parent><groupId>com.sl-express.sdn</groupId><artifactId>sl-express-sdn</artifactId><version>1.0-SNAPSHOT</version><properties><maven.compiler.source>11</maven.compiler.source><maven.compiler.target>11</maven.compiler.target><sl-express-common.version>1.2-SNAPSHOT</sl-express-common.version></properties><dependencies><dependency><groupId>com.sl-express.common</groupId><artifactId>sl-express-common</artifactId><version>${sl-express-common.version}</version></dependency><!--SDN依赖--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-neo4j</artifactId></dependency></dependencies></project>

2、编写配置文件

server:port: 9902
logging:level:org.springframework.data.neo4j: debug
spring:application:name: sl-express-sdnmvc:pathmatch:#解决异常:swagger Failed to start bean 'documentationPluginsBootstrapper'; nested exception is java.lang.NullPointerException#因为Springfox使用的路径匹配是基于AntPathMatcher的,而Spring Boot 2.6.X使用的是PathPatternMatchermatching-strategy: ant_path_matcherdata:neo4j:database: neo4jneo4j:authentication:username: neo4jpassword: neo4j123uri: neo4j://192.168.150.101:7687

3、基础代码

3.1、SDNApplication

编写启动类:

package com.sl.sdn;import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;@SpringBootApplication
public class SDNApplication {public static void main(String[] args) {SpringApplication.run(SDNApplication.class, args);}
}

3.2、Entity

编写实体,在物流中,会存在网点、二级转运中心、一级转运中心,我们分别用Agency、TLT、OLT表示。
由于以上三个机构的属性是相同的,但在Neo4j中的标签是不一样的,所以既要保证不同的类,也有相同的属性,这种场景比较适合将属性写到父类中,自己继承父类来实现,这里我们采用抽象类的来实现。

package com.sl.sdn.entity.node;import com.sl.sdn.enums.OrganTypeEnum;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.SuperBuilder;
import org.springframework.data.geo.Point;
import org.springframework.data.neo4j.core.schema.GeneratedValue;
import org.springframework.data.neo4j.core.schema.Id;@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();}

机构枚举:

package com.sl.sdn.enums;import cn.hutool.core.util.EnumUtil;
import com.sl.transport.common.enums.BaseEnum;/*** 机构类型枚举*/
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);}
}
package com.sl.sdn.entity.node;import com.sl.sdn.enums.OrganTypeEnum;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;
import lombok.experimental.SuperBuilder;
import org.springframework.data.neo4j.core.schema.Node;/*** 网点实体*/
@Node("AGENCY")
@Data
@ToString(callSuper = true)
@SuperBuilder(toBuilder = true)
@NoArgsConstructor
public class AgencyEntity extends BaseEntity {@Overridepublic OrganTypeEnum getAgencyType() {return OrganTypeEnum.AGENCY;}
}
package com.sl.sdn.entity.node;import com.sl.sdn.enums.OrganTypeEnum;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;
import lombok.experimental.SuperBuilder;
import org.springframework.data.neo4j.core.schema.Node;/*** 一级转运中心实体 (OneLevelTransportEntity)*/
@Node("OLT")
@Data
@ToString(callSuper = true)
@SuperBuilder(toBuilder = true)
@NoArgsConstructor
public class OLTEntity extends BaseEntity {@Overridepublic OrganTypeEnum getAgencyType() {return OrganTypeEnum.OLT;}
}
package com.sl.sdn.entity.node;import com.sl.sdn.enums.OrganTypeEnum;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;
import lombok.experimental.SuperBuilder;
import org.springframework.data.neo4j.core.schema.Node;/*** 二级转运中心实体(TwoLevelTransportEntity)*/
@Node("TLT")
@Data
@ToString(callSuper = true)
@SuperBuilder(toBuilder = true)
@NoArgsConstructor
public class TLTEntity extends BaseEntity {@Overridepublic OrganTypeEnum getAgencyType() {return OrganTypeEnum.TLT;}
}
package com.sl.sdn.entity.line;import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;/*** 运输路线实体*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class TransportLine {private Long id;private Double cost; //成本}

3.3、DTO

DTO用于服务间的数据传输,会用到OrganDTOTransportLineNodeDTO

package com.sl.sdn.dto;import cn.hutool.core.annotation.Alias;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;import javax.validation.constraints.NotNull;/*** 机构数据对象,网点、一级转运、二级转运都是看作是机构* BaseEntity中的location无法序列化,需要将经纬度拆开封装对象*/
@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;}
package com.sl.sdn.dto;import io.swagger.annotations.ApiModelProperty;
import lombok.Data;import java.util.ArrayList;
import java.util.List;/*** 运输路线对象*/
@Data
public class TransportLineNodeDTO {@ApiModelProperty(value = "节点列表", required = true)private List<OrganDTO> nodeList = new ArrayList<>();@ApiModelProperty(value = "路线成本", required = true)private Double cost = 0d;}

4、Repository

SDN也是遵循了Spring Data JPA规范,同时也提供了Neo4jRepository,该接口中提供了基本的CRUD操作,我们定义Repository需要继承该接口。

4.1、AgencyRepository

package com.sl.sdn.repository;import com.sl.sdn.entity.node.AgencyEntity;
import org.springframework.data.neo4j.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);}

测试:

package com.sl.sdn.repository;import com.sl.sdn.entity.node.AgencyEntity;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.domain.*;import javax.annotation.Resource;
import java.util.List;@SpringBootTest
class AgencyRepositoryTest {@Resourceprivate AgencyRepository agencyRepository;@Testpublic void testFindByBid() {AgencyEntity agencyEntity = this.agencyRepository.findByBid(9001L);System.out.println(agencyEntity);}@Testpublic void testSave() {AgencyEntity agencyEntity = new AgencyEntity();agencyEntity.setAddress("测试数据地址");agencyEntity.setBid(9001L);agencyEntity.setName("测试节点");agencyEntity.setPhone("1388888888888");this.agencyRepository.save(agencyEntity);System.out.println(agencyEntity);}@Testpublic void testUpdate() {AgencyEntity agencyEntity = this.agencyRepository.findByBid(9001L);agencyEntity.setName("测试节点1");this.agencyRepository.save(agencyEntity);System.out.println(agencyEntity);}@Testpublic void testDeleteByBid() {Long count = this.agencyRepository.deleteByBid(9001L);System.out.println(count);}/*** 查询全部*/@Testpublic void testFindAll() {List<AgencyEntity> list = this.agencyRepository.findAll();for (AgencyEntity agencyEntity : list) {System.out.println(agencyEntity);}}/*** 分页查询*/@Testpublic void testPage() {//设置分页、排序条件,page从0开始PageRequest pageRequest = PageRequest.of(1, 2, Sort.by(Sort.Order.desc("bid")));Page<AgencyEntity> page = this.agencyRepository.findAll(pageRequest);page.getContent().forEach(agencyEntity -> {System.out.println(agencyEntity);});}}

4.2、JPA自定义方法规则

使用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.3、OLTRepository

package com.sl.sdn.repository;import com.sl.sdn.entity.node.OLTEntity;
import org.springframework.data.neo4j.repository.Neo4jRepository;/*** 一级转运中心数据操作*/
public interface OLTRepository extends Neo4jRepository<OLTEntity, Long> {/*** 根据bid查询** @param bid 业务id* @return 一级转运中心数据*/OLTEntity findByBid(Long bid);/*** 根据bid删除** @param bid 业务id* @return 删除的数据条数*/Long deleteByBid(Long bid);}

4.4、OrganRepository

package com.sl.sdn.repository;import com.sl.sdn.dto.OrganDTO;import java.util.List;/*** 通用机构查询*/
public interface OrganRepository {/*** 无需指定type,根据id查询** @param bid 业务id* @return 机构数据*/OrganDTO findByBid(Long bid);/*** 查询所有的机构,如果name不为空的按照name模糊查询** @param name 机构名称* @return 机构列表*/List<OrganDTO> findAll(String name);
}

4.5、TLTRepository

package com.sl.sdn.repository;import com.sl.sdn.entity.node.TLTEntity;
import org.springframework.data.neo4j.repository.Neo4jRepository;/*** 二级转运中心数据操作*/
public interface TLTRepository extends Neo4jRepository<TLTEntity, Long> {/*** 根据bid查询** @param bid 业务id* @return 二级转运中心数据*/TLTEntity findByBid(Long bid);/*** 根据bid删除** @param bid 业务id* @return 删除的数据条数*/Long deleteByBid(Long bid);}

5、复杂查询

通过继承Neo4jRepository实现简单的查询是非常方便的,如果要实现复杂的查询就需要定义Cypher查询实现了,需要通过Neo4jClient进行查询操作,下面我们以查询两个网点间最短运输路线为例进行查询。

5.1、定义Repository

package com.sl.sdn.repository;import com.sl.sdn.dto.TransportLineNodeDTO;
import com.sl.sdn.entity.node.AgencyEntity;/*** 运输路线相关操作*/
public interface TransportLineRepository {/*** 查询两个网点之间最短的路线,查询深度为:10** @param start 开始网点* @param end   结束网点* @return 路线*/TransportLineNodeDTO findShortestPath(AgencyEntity start, AgencyEntity end);}

5.2、编写实现

package com.sl.sdn.repository.impl;import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.util.NumberUtil;
import cn.hutool.core.util.StrUtil;
import com.sl.sdn.dto.OrganDTO;
import com.sl.sdn.dto.TransportLineNodeDTO;
import com.sl.sdn.entity.node.AgencyEntity;
import com.sl.sdn.enums.OrganTypeEnum;
import com.sl.sdn.repository.TransportLineRepository;
import org.neo4j.driver.internal.value.PathValue;
import org.neo4j.driver.types.Path;
import org.springframework.data.neo4j.core.Neo4jClient;
import org.springframework.data.neo4j.core.schema.Node;
import org.springframework.stereotype.Component;import javax.annotation.Resource;
import java.util.Map;
import java.util.Optional;@Component
public class TransportLineRepositoryImpl implements TransportLineRepository {@Resourceprivate Neo4jClient neo4jClient;@Overridepublic TransportLineNodeDTO findShortestPath(AgencyEntity start, AgencyEntity end) {//获取网点数据在Neo4j中的类型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);//执行查询Optional<TransportLineNodeDTO> optional = this.neo4jClient.query(cypherQuery).bind(start.getBid()).to("startId") //设置参数.bind(end.getBid()).to("endId")//设置参数.fetchAs(TransportLineNodeDTO.class) //设置响应数据类型.mappedBy((typeSystem, record) -> { //对结果进行封装处理PathValue pathValue = (PathValue) record.get(0);Path path = pathValue.asPath();TransportLineNodeDTO dto = new TransportLineNodeDTO();// 读取节点数据path.nodes().forEach(node -> {Map<String, Object> map = node.asMap();OrganDTO organDTO = BeanUtil.toBeanIgnoreError(map, OrganDTO.class);//取第一个标签作为类型organDTO.setType(OrganTypeEnum.valueOf(CollUtil.getFirst(node.labels())).getCode());//查询出来的数据,x:经度,y:纬度organDTO.setLatitude(BeanUtil.getProperty(map.get("location"), "y"));organDTO.setLongitude(BeanUtil.getProperty(map.get("location"), "x"));dto.getNodeList().add(organDTO);});//提取关系中的 cost 数据,进行求和计算,算出该路线的总成本path.relationships().forEach(relationship -> {Map<String, Object> objectMap = relationship.asMap();double cost = Convert.toDouble(objectMap.get("cost"), 0d);dto.setCost(NumberUtil.add(cost, dto.getCost().doubleValue()));});//取2位小数dto.setCost(NumberUtil.round(dto.getCost(), 2).doubleValue());return dto;}).one();return optional.orElse(null);}
}

5.3、测试

编写测试用例:

package com.sl.sdn.repository;import com.sl.sdn.dto.TransportLineNodeDTO;
import com.sl.sdn.entity.node.AgencyEntity;
import com.sl.sdn.repository.TransportLineRepository;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;import javax.annotation.Resource;import static org.junit.jupiter.api.Assertions.*;@SpringBootTest
class TransportLineRepositoryTest {@Resourceprivate TransportLineRepository transportLineRepository;@Testvoid findShortestPath() {AgencyEntity start = AgencyEntity.builder().bid(100280L).build();AgencyEntity end = AgencyEntity.builder().bid(210057L).build();TransportLineNodeDTO transportLineNodeDTO = this.transportLineRepository.findShortestPath(start, end);System.out.println(transportLineNodeDTO);}
}

测试结果:
:::tips
TransportLineNodeDTO(nodeList=[OrganDTO(id=100280, name=北京市昌平区定泗路, type=3, phone=010-86392987, address=北七家镇定泗路苍龙街交叉口, latitude=40.11765281246394, longitude=116.37212849638287), OrganDTO(id=90001, name=昌平区转运中心, type=2, phone=null, address=昌平区转运中心, latitude=40.220952, longitude=116.231034), OrganDTO(id=8001, name=北京市转运中心, type=1, phone=null, address=北京市转运中心, latitude=39.904179, longitude=116.407387), OrganDTO(id=8002, name=上海市转运中心, type=1, phone=null, address=上海市转运中心, latitude=31.230525, longitude=121.473667), OrganDTO(id=90003, name=浦东新区转运中心, type=2, phone=null, address=浦东新区转运中心, latitude=31.221461, longitude=121.544346), OrganDTO(id=210057, name=上海市浦东新区南汇, type=3, phone=18821179169, address=园春路8号, latitude=31.035240152911637, longitude=121.73459966751048)], cost=11577.8)
:::


http://www.ppmy.cn/server/144310.html

相关文章

Springboot之登录模块探索(含Token,验证码,网络安全等知识)

简介 登录模块很简单&#xff0c;前端发送账号密码的表单&#xff0c;后端接收验证后即可~ 淦&#xff01;可是我想多了&#xff0c;于是有了以下几个问题&#xff08;里面还包含网络安全问题&#xff09;&#xff1a; 1.登录时的验证码 2.自动登录的实现 3.怎么维护前后端…

Shell脚本基础(4):条件判断

内容预览 ≧∀≦ゞ Shell脚本基础&#xff08;4&#xff09;&#xff1a;条件判断声明导语基本的if语句结构数值比较运算符文件测试运算符扩展&#xff1a;使用elif和else使用&&和||结合条件判断小结 Shell脚本基础&#xff08;4&#xff09;&#xff1a;条件判断 声明…

ElasticSearch学习篇17_《检索技术核心20讲》最邻近检索-局部敏感哈希、乘积量化PQ思路

目录 场景在搜索引擎和推荐引擎中&#xff0c;对相似文章去重是一个非常重要的环节&#xff0c;另外是拍照识花、摇一摇搜歌等场景都可以使用它快速检索。 基于敏感性哈希的检索更擅长处理字面上的相似而不是语义上的相似。 向量空间模型ANN检索加速思路 局部敏感哈希编码 随…

linux之调度管理(11)-cpu动态调频总体架构

一、cpufreq的背景 随着技术的发展&#xff0c;当前soc中的cpu主频一般都超过了1Ghz&#xff0c;而cpu的主频越高&#xff0c;其消耗的功耗也越大&#xff0c;这主要体现在以下两个方面&#xff1a; &#xff08;1&#xff09;cpu的运行频率越高&#xff0c;则晶体管在单位时间…

macOS 无法安装第三方app,启用任何来源的方法

升级新版本 MacOS 后&#xff0c;安装下载的软件时&#xff0c;不能在 ”安全性与隐私” 中找不到 ”任何来源” 选项。 1. 允许展示任何来源 点击 启动器 (Launchpad) – 其他 (Other) – 终端 (Terminal)&#xff1a; 打开终端后&#xff0c;输入以下代码回车&#xff1a; …

Qt 实现网络数据报文大小端数据的收发

1.大小端数据简介 大小端&#xff08;Endianness&#xff09;是计算机体系结构的一个术语&#xff0c;它描述了多字节数据在内存中的存储顺序。以下是大小端的定义和它们的特点&#xff1a; 大端&#xff08;Big-Endian&#xff09; 在大端模式中&#xff0c;一个字的最高有效…

一文详解kafka知识点

目录 1、kafka定义 2、消息队列 2.1、产品选择 2.2、应用场景 2.3、消息队列的两种模式 3、kafka架构 4、kafka生产者 4.1、kafka生产者原理 4.2、kafka生产者异步发送 4.3、同步发送 4.4、分区 4.4.1、kafka分区好处 4.4.2、分区策略 4.4.3、自定义分区 4.5、生成吞…

算法学习笔记(七):常用数据结构、堆、栈、队列

一&#xff1a;常用技巧&#xff1a;枚举右&#xff0c;维护左 1.双变量问题 对于双变量问题&#xff0c;例如两数之和 ai aj t&#xff0c;可以枚举右边的aj&#xff0c;转换成单变量问题&#xff0c;也就是 在aj左边查找是否有 ai t - aj&#xff0c;这就可以用哈希表来维…