Neo4j简单入门

1. Neo4j简单介绍

1.1 基本概念

图数据库的基本含义是以“图”这种数据结构存储和查询数据。传统关系数据库在查找范围超出浅遍历的情况下会变得缓慢,而图数据库因为使用了图遍历技术查找非常迅速。

neo4j图数据库中的基本概念:

Neo4j简单模型

  • Nodes:通常用来表使一个实体

    • Labels
    • Properties
  • Relationships:实体间关系,一个Node可以有0-N个Relationship

    • Labels
    • Properties
  • Traversal:

    搜索一个图的方法,例如“搜索上图中《阿甘正传》的导演是谁?”

  • Paths:

    Path是搜索的结果,Path最少可以只有一个Node,此时Path的长度为0

    path长度

1.2 优缺点

下图中A用户到C用户的路径长度为2和3。

路径长度

假设有一个问题:社交网站有100万用户,现在为某一用户推荐路径长度为3的用户

在传统关系数据库中:

1. 某一用户所有好友
   2. 好友的好友
   3. ......

在neo4j图数据库中:

用户Follow关系搜索

粗略性能对比:

​ 数据库中包含100万人,每人关注50个用户

关系数据库图数据库性能对比

优点 缺点
擅长处理复杂关系 图结构导致写入性能较差
高效搜索遍历 超级节点性能差
应用层友好,引入新的关系方便 社区版不支持分布式

1.3 应用场景

适用场景 不适用场景
社交网络 大规模数据处理
交通大数据(物流) 二进制数据存储
推荐系统 适合保存在关系型数据的结构化数据
欺诈分析
Web安全(垃圾邮件等等)

天眼查关系图

neo4j人脉关系

1.4安装

1.4.1 安装包安装

1.4.2 docker安装

1
2
3
4
docker run \
--publish=7474:7474 --publish=7687:7687 \
--volume=$HOME/neo4j/data:/data \
neo4j

2. Cypher

Cypher是用于查找和更新图数据库的语言。Cypher设计的初衷是让用户表达要获取的数据,而不是获取数据的方式,所以Cypher非常容易使用。

首先需要知道一些常用元素用Cypher的表示格式:

元素 表示格式
Node ()(a)(Jack:User:Teacher {name: "Jack", age: 33})
Relationship (a)-[r1]->(b)(a)-[r1:Teach {from: "2015-01-01"}]->(b)(a)--(b)

2.1 Cypher常用语句

2.1.1 数据写入

对于数据的写入可以使用以下语句:

  1. CREATE:创建nodes、relationships、paths

    创建多个node:

    1
    2
    CREATE (Jack:Actor:Father {name: "Jack", age: 18}),
    (Tom:Actor {name: "Tom", gender: "male"})

    创建relationship:

    1
    2
    3
    MATCH (a:Actor), (b:Actor)
    WHERE a.name = 'Jack' AND b.name = 'Tom'
    CREATE (b)-[r:Father {info: "information"}]->(a)

    直接创建path:

    1
    CREATE (Jack:Actor:Father {name: "Jack", age: 18})<-[r:Father {info: "information"}]-(Tom:Actor {name: "Tom", gender: "male"})
  2. DELETE:删除nodes、relationships和paths

    删除node:

    ​ 删除node时必须显式删除node的所有关系,可以使用DETACH DELETE快速完成这一操作

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    //先删除关系
    MATCH (Jack:Actor:Father {name: "Jack", age: 18})<-[r:Father {info: "information"}]-(Tom:Actor {name: "Tom", gender: "male"})
    DELETE r
    //再删除node
    MATCH (Jack:Actor:Father {name: "Jack", age: 18})
    DELETE Jack

    //使用DETEACH DELETE自动删除关系
    MATCH (Jack:Actor:Father {name: "Jack", age: 18})
    DETACH DELETE Jack
  3. SET:更新nodes的labels和properties,更新relationships的properties

    更新propertie:

    1
    2
    MATCH (Jack:Actor:Father {name: "Jack", age: 18})
    SET Jack.age = 22

    删除propertie:

    1
    2
    3
    MATCH (Jack:Actor:Father {name: "Jack"})
    SET Jack.age = NULL
    //也可以使用下面提到的remove删除propertie

    复制node/relationship的所有propertie:

    1
    2
    MATCH (Jack:Actor:Father {name: "Jack", age: 18}),(Tom:Actor {name: "Tom", gender: "male"})
    SET Tom = Jack

    追加label:

    1
    2
    MATCH (n {name: "Jack", age: 18})
    SET n:Actor:Father:Male
  4. REMOVE:删除nodes和relationships的properties

    删除propertie:

    1
    2
    MATCH (Jack:Actor:Father {name: "Jack"})
    REMOVE Jack.age

    删除label:

    1
    2
    MATCH (Jack:Actor:Father {name: "Jack"})
    REMOVE Jack:Actor
  5. FOREACH:遍历更新path的组成部分或聚合结果中list的数据

    初始化数据:

    1
    2
    3
    MATCH (n) DETACH DELETE n

    CREATE (Jack:Person {name:"Jack"})-[r1:Knows]->(Tom:Person {name:"Tom"})-[r2:Knows]->(Jerry:Person {name:"Jerry"})

    更新数据:

    1
    2
    MATCH p = (Jack:Person {name:"Jack"})-[*]->(Jerry:Person {name:"Jerry"})
    FOREACH (n IN nodes(p) | SET n.age = 18)

2.1.2 数据导入

数据导入可以使用LOAD CSV语句实现

假设有如下用户表CSV文件:

1
2
3
4
5
"Id","Name","Year"
"1","ABBA","1992"
"2","Roxette","1986"
"3","Europe","1979"
"4","The Cardigans","1992"

导入CSV文件:

1
2
LOAD CSV WITH HEADERS FROM 'http://118.25.24.236/data.csv' AS line
CREATE (:User {name: line.Name, year:line.Year})

导入CSV行数较多:

使用USING PERIODIC COMMIT在行数达到1000行后自动提交(也可以在COMMIT后面跟一个指定的行数),避免行数过多导致事务占用内存

1
2
3
USING PERIODIC COMMIT
LOAD CSV WITH HEADERS FROM 'http://118.25.24.236/data.csv' AS line
CREATE (:User {name: line.Name, year:line.Year})

2.1.3 数据查询

数据查询可以使用MATCHOPTIONAL MATCH

导入示例数据:

MATCH (n) DETACH DELETE n

CREATE (Tom:User {name: “Tom”, age: 22}),
(Jerry:User {name: “Jerry”, age: 32}),
(Jim:User {name: “Jim”, age: 24}),
(Picture1:Picture {name: “Picture1”}),
(Picture2:Picture {name: “Picture2”})

CREATE (Tom)-[r1:Like]->(Picture1)
CREATE (Tom)-[r2:Like]->(Picture2)
CREATE (Jerry)-[r3:Like]->(Picture1)
CREATE (Jim)-[r4:Like]->(Picture1)
CREATE (Jim)-[r5:Dislike]->(Picture2)

  • Node查询

    • 查询所有Node

      1
      2
      MATCH (n)
      RETURN n
    • 查询指定label的所有Node

      1
      2
      MATCH (n:User)
      RETURN n
    • 查询所有相关Node

      1
      2
      MATCH (:User {name: "Tom"})--(p)	//--表示匹配任意方向的关系,也可以使用--><--进行方向匹配
      RETURN p
  • Relationship查询

    • 查询两个Node的关系

      1
      2
      3
      //查询Tom和Picture1的关系
      MATCH (:User {name: "Tom"})-[r]-(:Picture {name: "Picture1"})
      RETURN type(r)
    • 关系匹配

      1
      2
      3
      //查询Like Picture2的User
      MATCH (u)-[:Like]->(:Picture {name: "Picture2"})
      RETURN u
    • 多关系匹配

      1
      2
      3
      //查询Like/Dislike Picture2的User
      MATCH (u)-[:Like|:Dislike]->(:Picture {name: "Picture2"})
      RETURN u
    • 关系长度搜索

      1
      2
      3
      4
      5
      6
      //查询距离Picture1长度为2Node,查询距离Picture1长度为2-3且关系为Like/Dislike的Node
      MATCH (n)-[*2]-(:Picture {name: "Picture1"})
      RETURN n

      MATCH (n)-[:Like|:Dislike*2..3]-(:Picture {name: "Picture1"})
      RETURN n
  • 最短路径

    1
    2
    3
    //查询Jerry到Picture2的最短路径
    MATCH m=shortestPath((p1:User {name:"Jerry"})-->(p2:Picture {name:"Picture1"}))
    RETURN m
  • 根据id查询

    1
    2
    MATCH (n)
    return id(n)
  • OPTIONAL MATCH

    OPTIONAL MATCH 和 MATCH的作用是一致的,但是OPTIONAL MATCH在结果为空时会返回NULL

    1
    2
    3
    4
    5
    MATCH (:Pictrue { name: 'Pictrue1' })-->(a)
    RETURN a

    OPTIONAL MATCH (:Pictrue { name: 'Pictrue1' })-->(a)
    RETURN a

2.1.4 查询子句

  • WHERE

  • ORDER BY

    1
    MATCH (n) RETURN id(n),n ORDER BY id(n) DESC

    注:NULL永远排在最后

  • SKIP

    跳过头N条数据

    1
    MATCH (n) RETURN n SKIP 3
  • LIMIT

2.1.5 读写语句

  • MERGE

    MERGE类似于MATCHCREATE的复合体,MERGE对模式进行完全匹配或创建。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    //MERGE普通节点
    MERGE (u:User {name:"Tonny"}) RETURN u

    //MERGE创建派生节点
    MATCH (n) DETACH DELETE n

    CREATE (:User {name:"Tonny", city:"ShenZhen"}),
    (:User {name:"Jimmy", city:"GuangZhou"}),
    (:User {name:"Jack", city:"ZhuHai"})

    MATCH (u:User)
    MERGE (u)-[:LiveIn]->(c:City {name: u.city})
    RETURN u, c

    还可以使用ON CREATEON MATCH语句来进行不同操作

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    //MERGE创建派生节点和关系,删除User中的city属性,创建City时新增创建时间
    MATCH (n) DETACH DELETE n

    CREATE (:User {name:"Tonny", city:"ShenZhen"}),
    (:User {name:"Jimmy", city:"GuangZhou"}),
    (:User {name:"Jack", city:"ZhuHai"})

    MATCH (u:User)
    MERGE (u)-[:LiveIn]->(c:City {name: u.city})
    ON CREATE SET u.city = NULL, c.createTime = timestamp()
    ON MATCH SET u.city = NULL
    RETURN u, c
  • CALL

    CALL用来调用存储过程

    1
    2
    //返回库中所有Label
    CALL db.labels

2.1.6 Schema语句

  • CONSTRAINTS

    • 节点键:Node Keys(商业版)

      用来保证指定标签下所有节点的指定属性都是唯一且存在的,类似关系数据库的主键和联合主键。创建一个节点键会对应创建一个复合索引。

      1
      2
      3
      4
      5
      //创建节点键
      CREATE CONSTRAINT ON (n:User) ASSERT (n.id, n.username) IS NODE KEY

      //删除节点键
      DROP CONSTRAINT ON (n:User) ASSERT (n.id, n.username) IS NODE KEY
    • 节点属性唯一约束:Unique node propertie constraints

      用来保证指定标签下所有节点的一个指定属性都是唯一的。创建一个节点属性唯一约束或对应创建一个单属性索引。

      1
      2
      3
      4
      5
      //创建唯一约束
      CREATE CONSTRAINT ON (n:User) ASSERT n.username IS UNIQUE

      //删除唯一约束
      DROP CONSTRAINT ON (n:User) ASSERT n.username IS UNIQUE
    • 关系属性存在约束:Relationship property existence constraints(商业版)

      用来保证指定关系一定存在某个属性。

      1
      2
      3
      4
      5
      //创建关系属性存在约束
      CREATE CONSTRAINT ON ()-[f:Follow]-() ASSERT exists(f.day)

      //删除关系属性存在约束
      DROP CONSTRAINT ON ()-[f:Follow]-() ASSERT exists(f.day)

获取库中约束列表:

1
CALL db.constraints
  • INDEX

    索引的创建不是同步完成的,索引会在后台创建,创建完成后自动上线生效

    • 单属性索引:single-property index

      用来索引指定标签节点的某一属性

      1
      2
      3
      4
      5
      //创建单属性索引
      CREATE INDEX ON :Person(firstname)

      //删除单属性索引
      DROP INDEX ON :Person(firstname)
    • 复合属性索引:composite index

      用来索引指定标签节点的某几个属性

      1
      2
      3
      4
      5
      //创建复合属性索引
      CREATE INDEX ON :Person(firstname, surname)

      //删除复合属性索引
      DROP INDEX ON :Person(firstname, surname)

获取库中索引列表:

1
CALL db.indexes

索引使用:

​ 索引使用通常不需要刻意去关注,Cypher会自动使用索引。

2.1.7 UNION

使用UNION可以将多个查询的结果进行合并,使用UNION会对结果进行去重,UNION ALL不会。注意:UNION关联的查询结果字段名必须相同

1
2
3
4
5
MATCH (u:User)
RETURN u.name AS name
UNION
MATCH (p:Picture)
RETURN p.name AS name

2.2 Cypher常用函数

2.2.1 标量函数

标量函数返回一个值

函数 简介
id() 返回一个node或relationship的id MATCH (u:User) RETURN id(u)
length() 返回path的长度 MATCH (:User {name:”Jack”})–(:User {name:”Tom”})
properties() 返回一个Map包含指定node的所有propertie
startNode/endNode() 返回一个relationship的起始/结束node

2.2.2 聚合函数

函数 简介
avg() 返回平均值 MATCH (u:User) RETURN avg(u.age)
collect() 将多个值添加到一个list并返回
count() 返回数量
max/min() 返回最大最小值
sum() 求和

2.2.3 字符串函数

函数 简介
lTrim/rTrim()/trim() 去掉开头/尾随空白
replace 替换 RETURN replcae(“aaab”, “a”, “x”)
reverse() 反转
subString() 截取字符串 RETURN subString(“aaab”, 0, 1)
split() 拆分成list RETURN split(“aa,ab”, “,”)
toUpper()/toLower() 大小写转换

2.2.4 日期函数

函数 简介
date() 返回当前日期
datetime() 返回当前日期时间
time() 返回当前时间
timestamp() 返回时间戳

3. Neo4j整合开发

3.1 整合Mybatis

  1. 添加jdbc驱动依赖

    1
    2
    3
    4
    5
    <dependency>
    <groupId>org.neo4j</groupId>
    <artifactId>neo4j-jdbc-driver</artifactId>
    <version>3.1.0</version>
    </dependency>
  2. 添加自定义数据源

    1
    2
    3
    4
    5
    6
    <bean id="dataSource2" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
    <property name="driverClassName" value="org.neo4j.jdbc.Driver" />
    <property name="url" value="jdbc:neo4j:bolt://localhost/" />
    <property name="username" value="neo4j" />
    <property name="password" value="neo4j" />
    </bean>
  3. 添加neo4j方言,修改分页插件,(以下内容都是基于jeesite3)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    public class Neo4jDialect implements Dialect{
    @Override
    public boolean supportsLimit() {
    return true;
    }
    @Override
    public String getLimitString(String sql, int offset, int limit) {
    return sql + " SKIP " + offset + " LIMIT " + limit;
    }
    }

    SQLHelpher.java

    1
    2
    3
    4
    5
    else if ("neo4j".equals(dbName)){
    String tmp = sql.replace("RETURN", "WITH");
    tmp = tmp.replace("return", "WITH");
    countSql = tmp + " RETURN count(*)";
    }
  4. 简单查询示例

    分页查找标签为User的用户

    mapper.xml

    1
    2
    3
    4
    <select id="findList" resultType="NeoUser">
    MATCH (u:User)
    RETURN u.name AS name
    </select>

    Service

    1
    2
    3
    4
    5
    6
    7
    @Service
    @Transactional(readOnly = true)
    public class TestService extends CrudService<TestDao, NeoUser> {
    public Page<NeoUser> findPage(Page<NeoUser> page, NeoUser user){
    return super.findPage(page, user);
    }
    }

    Controller

    1
    2
    3
    4
    5
    6
    7
    8
    9
    @RequestMapping("/neo4j")
    @ResponseBody
    public Page<NeoUser> neo4j(){
    DynamicDataSource.setCurrentLookupKey("dataSource2");
    Page<NeoUser> userPage = new Page<NeoUser>();
    userPage.setPageNo(1);
    userPage.setPageSize(2);
    return testService.findPage(userPage, new NeoUser());
    }
  5. 效果

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    {
    "pageNo": 1,
    "pageSize": 2,
    "count": 15,
    "list": [{
    "isNewRecord": true,
    "name": "Tom"
    }, {
    "isNewRecord": true,
    "name": "Jerry"
    }],
    "html": "<ul>\n<li class=\"disabled\"><a href=\"javascript:\">« 上一页</a></li>\n<li class=\"active\"><a href=\"javascript:\">1</a></li>\n<li><a href=\"javascript:\" onclick=\"page(2,2,'');\">2</a></li>\n<li><a href=\"javascript:\" onclick=\"page(3,2,'');\">3</a></li>\n<li><a href=\"javascript:\" onclick=\"page(4,2,'');\">4</a></li>\n<li><a href=\"javascript:\" onclick=\"page(5,2,'');\">5</a></li>\n<li><a href=\"javascript:\" onclick=\"page(6,2,'');\">6</a></li>\n<li><a href=\"javascript:\" onclick=\"page(7,2,'');\">7</a></li>\n<li><a href=\"javascript:\" onclick=\"page(8,2,'');\">8</a></li>\n<li><a href=\"javascript:\" onclick=\"page(2,2,'');\">下一页 »</a></li>\n<li class=\"disabled controls\"><a href=\"javascript:\">当前 <input type=\"text\" value=\"1\" onkeypress=\"var e=window.event||event;var c=e.keyCode||e.which;if(c==13)page(this.value,2,'');\" onclick=\"this.select();\"/> / <input type=\"text\" value=\"2\" onkeypress=\"var e=window.event||event;var c=e.keyCode||e.which;if(c==13)page(1,this.value,'');\" onclick=\"this.select();\"/> 条,共 15 条</a></li>\n</ul>\n<div style=\"clear:both;\"></div>",
    "firstResult": 0,
    "maxResults": 2
    }