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-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
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 {
@Override
public 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 {
@Override
public 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 {
@Override
public 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用于服务间的数据传输,会用到OrganDTO
、TransportLineNodeDTO
。
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 {
@Resource
private AgencyRepository agencyRepository;
@Test
public void testFindByBid() {
AgencyEntity agencyEntity = this.agencyRepository.findByBid(9001L);
System.out.println(agencyEntity);
}
@Test
public void testSave() {
AgencyEntity agencyEntity = new AgencyEntity();
agencyEntity.setAddress("测试数据地址");
agencyEntity.setBid(9001L);
agencyEntity.setName("测试节点");
agencyEntity.setPhone("1388888888888");
this.agencyRepository.save(agencyEntity);
System.out.println(agencyEntity);
}
@Test
public void testUpdate() {
AgencyEntity agencyEntity = this.agencyRepository.findByBid(9001L);
agencyEntity.setName("测试节点1");
this.agencyRepository.save(agencyEntity);
System.out.println(agencyEntity);
}
@Test
public void testDeleteByBid() {
Long count = this.agencyRepository.deleteByBid(9001L);
System.out.println(count);
}
/**
* 查询全部
*/
@Test
public void testFindAll() {
List<AgencyEntity> list = this.agencyRepository.findAll();
for (AgencyEntity agencyEntity : list) {
System.out.println(agencyEntity);
}
}
/**
* 分页查询
*/
@Test
public 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中的规则,进行自定义查询:
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.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 {
@Resource
private Neo4jClient neo4jClient;
@Override
public 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 {
@Resource
private TransportLineRepository transportLineRepository;
@Test
void 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)
:::