跳至主要內容

初学Neo4j

程序员李某某原创数据库图数据库大约 7 分钟

初学Neo4j

基础

下载

Neo4j官网open in new window下载open in new window版本open in new window桌面版open in new window

桌面工具介绍

Browser

img
img

概念

  1. 节点:实体,可以是人、公司、组织、地点、事物等。可以理解为sql中的表
  2. 边:关系,可以是朋友、投资、投资组合、投资组合成员、投资组合成员投资等。
  3. 属性:节点和边的属性,可以是节点的姓名、年龄、性别、地址、电话等,也可以是边的类型、时间等。
  4. 标签:标签是对节点的分类,这样使得构建Neo4j数据模型更加简单
  5. 图:由节点和边组成的数据结构,可以表示复杂的关系和结构。
  6. 图数据库:一种存储结构为图数据的数据库,可以存储大量复杂的关系和结构,并支持复杂的查询和操作。
  7. Cypher :图数据库的查询语言,可以查询和操作图数据。
  8. Gremlin :图数据库的查询语言,可以查询和操作图数据。
  9. Neo4j:一个开源的图数据库,支持Cypher和Gremlin两种查询语言。

安装

  1. 下载Neo4j安装包,并解压到任意目录。
  2. 启动Neo4j服务,在命令行窗口中执行以下命令:
neo4j console
  1. 访问Neo4j管理界面,默认地址为http://localhost:7474,用户名密码为neo4j/neo4j。

Cypher 语法

Cypher是一种用于查询和操作图数据的查询语言,它支持多种数据类型和操作符,可以满足各种复杂的查询需求。

官网open in new window

  1. 创建节点:使用CREATE命令创建节点,节点可以包含属性。

    CREATE (a:Person {name:"Alice",age:30,isAlive:true})
    
    • Persion 是节点的类型(类似sql的表),name、age、isAlive 是节点的属性。
    • a 是节点的标识符(类似别名)。
  2. 创建边:使用CREATE命令创建边,边可以包含属性。

    MATCH (a:Person {name:"Alice"}), (b:Person {name:"Bob"})
    CREATE (a)-[:KNOWS]->(b)
    
  3. 创建节点:在Cypher查询窗口中输入以下命令,创建一个名为"Alice"的节点:

    CREATE (a:Person {name:"Alice"})
    
  4. 查询节点:在Cypher查询窗口中输入以下命令,查询所有节点:

    MATCH (n)
    RETURN n
    
  5. 查询边:在Cypher查询窗口中输入以下命令,查询所有边:

    MATCH ()-[r]-()
    RETURN r
    
  6. 查询关系:在Cypher查询窗口中输入以下命令,查询"Alice"节点和"Bob"节点之间的关系:

    MATCH (a:Person {name:"Alice"}), (b:Person {name:"Bob"})
    MATCH (a)-[r]->(b)
    RETURN r
    
  7. 删除节点:在Cypher查询窗口中输入以下命令,删除"Alice"节点:

    MATCH (a:Person {name:"Alice"})
    DELETE a
    
  8. 删除边:在Cypher查询窗口中输入以下命令,删除"Alice"节点和"Bob"节点之间的"KNOWS"类型的边:

    MATCH (a:Person {name:"Alice"}), (b:Person {name:"Bob"})
    MATCH (a)-[r:KNOWS]->(b)
    DELETE r
    
    MATCH (a)-[r]->(b) WHERE id(a)=1 AND id(b)=2 DELETE r;
    

    这些操作将删除图中的节点和边,并返回删除的记录数。 注意:删除操作可能会影响其他节点和边的关系,因此需要谨慎操作。

  9. 创建索引:在Cypher查询窗口中输入以下命令,为"name"属性创建索引:

    //创建索引语法:
    //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
    

SDN

Spring Data Neo4j简称SDN

依赖

<!--SDN依赖-->
<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

实体

以物流转运为例

@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;
    // 在 org.springframework.data.geo.Point 包中
    @ApiModelProperty(value = "位置坐标, x: 纬度,y: 经度", required = true)
    private Point location;

    //机构类型
    public abstract OrganTypeEnum getAgencyType();

}

机构枚举

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);
    }
}

网点实体

@Node("AGENCY")		// sdn中的注解,对于节点名称
@Data
@ToString(callSuper = true)
@SuperBuilder(toBuilder = true)
@NoArgsConstructor
public class AgencyEntity extends BaseEntity {

    @Override
    public OrganTypeEnum getAgencyType() {
        return OrganTypeEnum.AGENCY;
    }
}

一级转运实体

@Node("OLT")
@Data
@ToString(callSuper = true)
@SuperBuilder(toBuilder = true)
@NoArgsConstructor
public class OLTEntity extends BaseEntity {

    @Override
    public OrganTypeEnum getAgencyType() {
        return OrganTypeEnum.OLT;
    }
}

二级转运实体

@Node("TLT")
@Data
@ToString(callSuper = true)
@SuperBuilder(toBuilder = true)
@NoArgsConstructor
public class TLTEntity extends BaseEntity {

    @Override
    public OrganTypeEnum getAgencyType() {
        return OrganTypeEnum.TLT;
    }
}

转运路线实体

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class TransportLine {

    private Long id;
    private Double cost; //成本

}

DTO

/**
 * 机构数据对象,网点、一级转运、二级转运都是看作是机构
 * BaseEntity中的location无法序列化,需要将经纬度拆开封装对象
 */
@Data
public class OrganDTO {

    @Alias("bid") //业务id作为id进行封装		该注解是hutool包中的
    @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

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

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.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);

}
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);
}
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);

}

测试

@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);
        });
    }

}

常见问题

物流项目中,运输路线错综复杂,你们如何进行计算路线的?

  • 距离优先:有些物流运输场景,如快递、紧急送货等,可能更关注路程的短暂,因此距离优先可以减少整体的运输时间和成本。
  • 成本优先:其他场景可能更关注成本效益,因此会优先选择经济效益最高的路线,即使路程稍长也可以接受,以降低整体的运输成本。

选择距离优先还是成本优先,需要根据具体的运输需求、客户要求、市场竞争等因素综合考虑,并可能根据不同的情况进行调整。

是距离优先还是成本优先?

为什么选择使用Neo4j图数据库存储路线?

路线运输模型在Neo4j中是如何设计的?

在Neo4j中如何设置关系的查询深度?

在SDN中如何自定义Cypher查询?

可以直接定义JPA方法,为什么还要自定义查询?

上次编辑于:
贡献者: 李元昊