Neo4j入门第二期(Spring Data Neo4j的使用)

强烈建议先看第一期:

Neo4j入门第一期(Cypher入门)-CSDN博客

SDN快速入门

Spring Data Neo4j简称SDN,是Spring对Neo4j数据库操作的封装,其底层基于neo4j-java-driver实现。

创建工程

javascript 复制代码
<?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>

基础代码

SDNApplication

编写启动类:

java 复制代码
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);
    }
}

Entity

编写实体,在物流中,会存在网点、二级转运中心、一级转运中心,我们分别用Agency、TLT、OLT表示。

由于以上三个机构的属性是相同的,但在Neo4j中的标签是不一样的,所以既要保证不同的类,也有相同的属性,这种场景比较适合将属性写到父类中,自己继承父类来实现,这里我们采用抽象类的来实现。

java 复制代码
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();

}

机构枚举:

java 复制代码
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);
    }
}
java 复制代码
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;
    }
}
java 复制代码
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;
    }
}
java 复制代码
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;
    }
}
java 复制代码
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; //成本

}

DTO

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

java 复制代码
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;

}
java 复制代码
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;

}

Repository

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

java 复制代码
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);

}

OLTRepository

java 复制代码
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);

}

复杂查询

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

定义Repository
java 复制代码
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);

}
编写实现
java 复制代码
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.stream.StreamUtil;
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.List;
import java.util.Map;
import java.util.Optional;

@Component // 将该类注册为 Spring Bean
public class TransportLineRepositoryImpl implements TransportLineRepository {

    @Resource // 注入 Neo4j 客户端用于执行 Cypher 查询
    private Neo4jClient neo4jClient;

    /**
     * 查找从一个网点到另一个网点的最短路径。
     *
     * @param start 起始网点实体
     * @param end   目标网点实体
     * @return 包含最短路径信息的 DTO 对象
     */
    @Override
    public TransportLineNodeDTO findShortestPath(AgencyEntity start, AgencyEntity end) {
        // 获取网点数据在 Neo4j 中的标签名称(类型)
        String type = AgencyEntity.class.getAnnotation(Node.class).value()[0];

        // 构造 Cypher 查询语句,寻找从起点到终点的最短路径
        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") // 绑定起始节点 ID 到查询参数 'startId'
                .bind(end.getBid()).to("endId") // 绑定目标节点 ID 到查询参数 'endId'
                .fetchAs(TransportLineNodeDTO.class) // 设置返回数据类型为 TransportLineNodeDTO
                .mappedBy((typeSystem, record) -> { // 自定义映射逻辑,将记录转换为 DTO 对象
                    PathValue pathValue = (PathValue) record.get(0); // 从记录中获取路径对象
                    Path path = pathValue.asPath(); // 将 PathValue 转换为 Path 对象

                    // 创建 TransportLineNodeDTO 实例
                    TransportLineNodeDTO dto = new TransportLineNodeDTO();

                    // 处理路径中的节点,将其转换为 OrganDTO 列表
                    List<OrganDTO> nodeList = StreamUtil.of(path.nodes()) // 遍历路径中的所有节点
                            .map(node -> {
                                Map<String, Object> map = node.asMap(); // 将节点转换为 Map
                                OrganDTO organDTO = BeanUtil.toBeanIgnoreError(map, OrganDTO.class); // 将 Map 转换为 OrganDTO
                                // 取第一个标签作为类型
                                OrganTypeEnum organTypeEnum = OrganTypeEnum.valueOf(CollUtil.getFirst(node.labels()));
                                organDTO.setType(organTypeEnum.getCode()); // 设置类型代码
                                // 查询出来的数据,x:经度,y:纬度
                                organDTO.setLatitude(BeanUtil.getProperty(map.get("location"), "y")); // 设置纬度
                                organDTO.setLongitude(BeanUtil.getProperty(map.get("location"), "x")); // 设置经度
                                return organDTO; // 返回 OrganDTO 实例
                            })
                            .collect(Collectors.toList()); // 收集所有 OrganDTO 实例到列表中

                    dto.setNodeList(nodeList); // 设置节点列表到 DTO 对象

                    // 提取关系中的 cost 数据,进行求和计算,算出该路线的总成本
                    double cost = StreamUtil.of(path.relationships()) // 遍历路径中的所有关系
                            .mapToDouble(relationship -> {
                                Map<String, Object> objectMap = relationship.asMap(); // 将关系转换为 Map
                                return Convert.toDouble(objectMap.get("cost"), 0d); // 获取 cost 值并转换为 double,如果不存在则默认为 0
                            }).sum(); // 计算所有关系的 cost 总和
                    dto.setCost(cost); // 设置总成本到 DTO 对象

                    // 取2位小数
                    dto.setCost(NumberUtil.round(dto.getCost(), 2).doubleValue());

                    return dto; // 返回最终的 DTO 对象
                }).one(); // 获取单个结果

        return optional.orElse(null); // 如果没有找到结果,则返回 null
    }
}
相关推荐
越来越无动于衷3 分钟前
java错题
java·算法
l_tian_tian_7 分钟前
SpringCloud——MybatisPlus
java·spring boot·mybatis
北漂老男孩25 分钟前
Flink基于Yarn多种启动方式详解
java·大数据·flink
王蛋11126 分钟前
后端环境配置
java·spring·maven
养-乐多28 分钟前
梳理Spring Boot中三种异常处理
java·spring boot·后端
找不到、了31 分钟前
字符串和常量池的进一步研究
java·开发语言
Code哈哈笑36 分钟前
【基于SpringBoot的图书购买系统】深度讲解 分页查询用户信息,分析前后端交互的原理
java·数据库·spring boot·后端·spring·交互
kingwebo'sZone42 分钟前
sqlite的拼接字段的方法(sqlite没有convert函数)
java·数据库·sqlite
星沁城44 分钟前
212. 单词搜索 II
java·数据结构·算法·leetcode
.生产的驴1 小时前
Vue3 数据可视化屏幕大屏适配 页面自适应 响应式 数据大屏 大屏适配
java·c++·vue.js·后端·信息可视化·前端框架·vue