Neo4j系列博客-程序开发-驱动式

驱动包开发模式

Neo4j 驱动程序也为其他的语言开发了访问途径,编写使用驱动程序的应用程序实例就可以与数据库基于事务的会话。在会话中事务可以运行创建和查询数据的相关命令,也可以定义数据库模式以及监视和管理数据库。当 Neo4j 部署在因果集群中时,驱动程序可以处理读取和写入操作的路由。使用因果集群驱动程序还提供书签,用于保证因果一致性,也就是说应用程序可以在集群的不同成员上运行多个查询,同时保持数据的一致。

Neo4j 驱动程序使用 Bolt 协议进行通信,因果集群提供了使用驱动程序获取集群拓扑的功能,驱动程序必须具有集群感知能力才能提供路由和负载均衡。(Bolt 在多个版本中提供兼容)

入门

  1. 使用依赖管理获得驱动

    1
    2
    3
    4
    5
    6
    7
    <dependencies>
    <dependency>
    <groupId>org.neo4j.driver</groupId>
    <artifactId>neo4j-java-driver</artifactId>
    <version>1.1.0</version>
    </dependency>
    </dependencies>
  2. 使用官方驱动包
    每个 Neo4j 驱动程序都有一个用于创建驱动程序的数据库对象,步骤如下:

    • 向数据库对象请求一个新的驱动程序。
    • 向驱动程序对象请求一个新的会话。
    • 请求会话对象创建事务。
    • 使用事务对象运行语句,他返回一个表示结果的对象。
    • 处理结果。
    • 关闭会话。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    import org.neo4j.driver.v1.AuthTokens;
    import org.neo4j.driver.v1.Driver;
    import org.neo4j.driver.v1.GraphDatabase;
    import org.neo4j.driver.v1.Record;
    import org.neo4j.driver.v1.Session;
    import org.neo4j.driver.v1.StatementResult;
    import org.neo4j.driver.v1.Transaction;
    import static org.neo4j.driver.v1.Values.parameters;

    Driver driver = GraphDatabase.driver("bolt://localhost:7687", AuthTokens.basic("neo4j", "neo4j"));
    try (Session session = driver.session()) {
    try (Transaction tx = session.beginTransaction()) {
    tx.run("CREATE (a:Person {name: {name}, title: {title}})", parameters("name", "Arthur", "title", "King"));
    tx.success();
    }
    try (Transaction tx = session.beginTransaction()) {
    StatementResult result = tx.run("MATCH (a:Person) WHERE a.name = {name}" + "RETURN a.name AS name, a.title as title", parameters("name", "Arthur"));
    whiel (result.hasNext()) {
    Record record = result.next();
    System.out.println(String.format("%s %s", record.gete("title").asString(), record.get("name").asString()));
    }
    }
    }
    driver.close();

配置与连接

驱动程序对象实例可以在整个应用程序中跨多个线程进行全局共享,它包含一个到集群的各个成员的连接池,可以借由它执行任何操作。同时驱动程序提供 Session 封装了针对数据库的一个或多个工作事务的上下文。

部署

驱动程序包含一个图数据库对象,此对象提供驱动程序实例。当从数据库对象请求驱动程序实例时,需要提供 URI 声明 Neo4j 服务器的协议、主机和端口。
其中 URI 分为两种分为直接 URI 和路由 URI。其中直接 URI 用于连接单个实例,如果应用程序依赖路由和负载均衡,则需要单独提供。路由 URI 则是用于创建支持集群的驱动程序,路由驱动程序连接到集群并处理路由和负载均衡,可以根据连接获得集群拓扑信息。

Bolt URI 格式遵循 schema://host:port 的标准 URI 模式。

1
Driver driver = GraphDatabase.driver("bolt://localhost:7687", AuthTokens.basic("neo4j", "neo4j"));

创建驱动程序应用时需要传递一个 URI 当作初始化参数来连接服务器,如果没有服务器可用在该 URI 响应,则抛出 ServiceUnavailableException,因此可以在应用实例中组装一个 List 循环创建连接,这样会比较可靠。

1
2
3
4
5
6
7
8
9
10
public Driver acquireDriver(List<String> uris, AuthToken authToken, Config config) {
for (String uri : uris) {
try {
return GraphDatabase.driver(uri, authToken, config);
} catch (ServiceUnavailableException e) {
// This URI failed, so loop around again if we have another
}
}
throw new ServiceUnavailableException("No valid database URI found");
}
配置
  1. 加密和授权
    所有 Neo4j 驱动程序支持使用 SSL/TLSNeo4j 驱动程序和 Neo4j 实例之间的连接进行加密。但是在使用本地地址 localhost 时不会启用加密。强烈建议使用加密功能以保护 Neo4j 中存储的身份验证凭据和数据。

  2. 加密
    驱动程序可以通过配置来打开或关闭 TLS 加密。建议只应在可信的内部网络中关闭加密。虽然大部分驱动程序默认启用加密,但最好时明确配置为使用加密。

    1
    2
    3
    Driver driver = GraphDatabase.driver("bolt://localhost:7687", 
    AuthTokens.basic("neo4j", "neo4j"),
    Config.build().withEncryptionLevel(Config.EncryptionLevel.REQUIRED).toConfig());
  3. 解密
    当建立加密连接时,需要验证远程对等端是期望连接到的对等端。没有验证这一点则容易受到中间人攻击,欺骗获取加密连接。
    Neo4j 共有两种信任策略:

    • 信任第一次使用策略。指的是驱动程序将信任对主机的第一个连接是安全和有效的,在后续的连接上,驱动程序将验证他与第一个连接上的主机通信。因此请确保在可信的网络环境中建立与服务器的第一个连接。
      1
      2
      3
      Driver driver = GraphDatabase.driver("bolt://localhost:7687", 
      AuthTokens.basic("neo4j", "neo4j"),
      Config.build().withEncryptionLevel(Config.EncryptionLevel.REQUIRED).withTrustStrategy(Config.TrustStrategy.trustOnFirstUse(new File("/path/to/neo4j_known_hosts"))).toConfig());
    • 信任签名的证书策略。指的是为驱动程序提供信任的证书。
      1
      2
      3
      Driver driver = GraphDatabase.driver("bolt://localhost:7687", 
      AuthTokens.basic("neo4j", "neo4j"),
      Config.build().withEncryptionLevel(Config.EncryptionLevel.REQUIRED).withTrustStrategy(Config.TrustStrategy.trustOnFirstUse(new File("/path/to/certificate.pem"))).toConfig());
  4. 客户端授权
    服务器将要求驱动程序为用户提供身份验证凭证,以便连接到数据库,除非已禁用身份验证。

    1
    2
    3
    Driver driver = GraphDatabase.driver("bolt://localhost:7687", 
    AuthTokens.basic("neo4j", "neo4j"),
    Config.build().withEncryptionLevel(Config.EncryptionLevel.REQUIRED).toConfig());

执行 Cypher 语句

Session 会话用于事务处理的轻量级容器,与 Neo4j 数据库的所有交互都在 session 中进行,每个 session 会话是针对数据库的一个或多个事务,同时 Session 可以从驱动程序对象中获取。
使用路由驱动程序时,可以将 session 会话访问模式声明为 READWRITE。驱动程序将针对所选工作模式对指定的集群成员创建会话。在通常情况下,事务可以保证从用 session 创建运行的语句到对于每个语句的结果处理以及任何异常都能在可控的范围内,当事务全部执行完毕后,会话将被关闭。

1
2
3
4
try (Transaction tx = session.beginTransaction()) {
tx.run("CREATE (:Person {name: 'Guinevere'}");
tx.Success();
}
Session

Session 会话如同 Web 开发中的 Session,可以区分开不同的操作实例。使用 driver.session 方法来创建一个新的 SessionSession 会话创建是一个低资源消耗的过程,其底层原理是连接将从构建的驱动程序连接池创建 Session,并在关闭时返还到连接池。

  1. 访问模式
    当驱动程序启用路由时,需要先制定访问模式 READ 或者 WRITE。如果省略则默认使用 WRITE 模式。路由驱动程序将分别针对适合写入或读取的实例创建会话,如果是针对因果集群进行部署意味者驱动程序可以有效地路由读取和写入。
    如果创建会话的实例对于声明的访问模式不可用,则会抛出 SessionExpiredException

  2. 连接池
    驱动程序具有连接池,Session 会话将借用连接来执行工作,当会话关闭时,Session 将返回到连接池。最重要的时确保会话是正确关闭的,以便结果和错误完全由客户端接收,并且 Session 连接可以重新使用。另外当抛出异常时 Session 会话将被关闭,这样可以确保执行所有操作都是稳定可靠的。

回滚事务
1
2
3
4
try (Transaction tx = session.beginTransaction()) {
tx.run("CREATE (:Person {name: {name}}", Values.parameters("name", "Merlin"));
tx.failure();
}
自动提交事务

通常情况下事务在程序中是被明确声明的,但有些情况下 Sessionrun 方法可以隐含地自动提交需要运行语句的事务,这只出现在只读查询的情况下,因为读操作不会改变数据库内容。并且自动提交事务不支持书签功能,在调试时难以诊断故障。

1
2
StatementResult result = transaction.run("CREATE (person:Person {name: {name}})", Values.parameters("name", "Arthur"));
transaction.success();
查询语句中的参数

建议在查询语句始终使用参数。使用参数具有显著的性能和安全优势:

  • 参数允许查询中重复使用,使查询更加高效。
  • 保护数据库免受 Cypher 注入攻击。
1
2
StatementResult result = transaction.run("CREATE (person:Person {name: "Arthur"})");
transaction.success();
书签

可以在 Session 执行操作之后创建一个书签,书签是指向某些操作的指针。当为其他相关工作创建新会话时,可以使用已经创建的书签,同时书签确保之后的操作依赖于先前的操作。无论数据是否已复制到集群的每个实例,书签都保证下一个操作会被应用于先前设置了标签的实例。

1
2
3
4
5
6
7
8
9
10
11
12
Driver driver = GraphDatabase.driver("bolt://localhost:7687", AuthTokens.basic("neo4j", "neo4j"));
String bookmark;
try (Session session = driver.session(AccessMode.WRITE)) {
try (Transaction tx = session.beginTransaction()) {
tx.run("CREATE (a:Person {name: {name}, title: {title}})", Values.parameters("name", "Arthur", "title", "King"));
tx.success();
tx.close();
} finally {
bookmark = session.lastBookmark();
}
}
return bookmark;

创建书签之后,在集群随后的操作中就可以使用它。Session 会话将会为加过书签的实例提供服务。数据的视图对于应用程序是一致的。

1
2
3
4
5
6
7
8
9
10
11
12
13
Driver driver = GraphDatabase.driver("bolt://localhost:7687", AuthTokens.basic("neo4j", "neo4j"));
try (Session session = driver.session()) {
StatementResult result;
try (Transaction tx = session.beginTransaction()) {
result = tx.run("MATCH (a:Person) WHERE a.name = {name}" + "RETURN a.name AS name, a.title as title", Values.parameters("name", "Arthur"));
tx.success();
tx.close();
whiel (result.hasNext()) {
Record record = result.next();
System.out.println(String.format("%s %s", record.get("title").asString(), record.get("name").asString()));
}
}
}

返回结果

结果游标

结果游标提供了对结果记录流的访问。游标可以不断依次指向结果集合中的每个记录。在将游标移动到第一条记录之前,他会先指向记录的起始位置。一旦游标已经遍历到结果流的末尾,就可以使用摘要信息和元数据。
随着游标在结果集中前进,结果记录将被惰性加载。这意味着必须将游标移动到第一个结果,然后才能使用此结果。通常最好的做法就是明确使用结果和关闭会话,特别是在运行 update 语句时。

1
2
3
4
5
6
7
8
String searchTerm = "Sword";
try (Transaction tx = session.beginTransaction()) {
StatementResult result = tx.run("MATCH (weapon:Weapon) WHERE weapon.name CONTAINS {term} RETURN weapon.name", Values.parameters("name", searchTerm));
while (result.hasNext()) {
Record record = result.next();
System.out.println(record.get("weapon.name").asString());
}
}

结果记录集合由记录组成。记录提供结果的一部分固定不变的内容,他是键和值的有序映射。这些键值对称为字段,由于字段都是有键和有序的,因此可以通过位置或键访问字段的值。

1
2
3
4
5
6
7
8
9
10
11
12
String searchTerm = "Sword";
try (Transaction tx = session.beginTransaction()) {
StatementResult result = tx.run("MATCH (weapon:Weapon) WHERE weapon.name CONTAINS {term} RETURN weapon.name, weapon.material, weapon.size", Values.parameters("name", searchTerm));
while (result.hasNext()) {
Record record = result.next();
List<String> sword = new ArrayList<>();
for (String key : record.keys()) {
sword.add(key + ": " + record.get(key));
}
System.out.println(sword);
}
}
保留结果

语句执行获得的结果直到在 Session 会话中运行另一个语句之前或直到当前事务关闭结果集都可用。如果需要保留特定的结果集,每个驱动程序提供了收集结果集合并将其转换为每种语言的标准数据结构的方法,另外保存的结果也可以直接用于其他待运行的新语句中。

1
2
3
4
5
6
7
8
9
10
List<Record> records;
try (Session session = driver.session()) {
try (Transaction tx = session.beginTransaction()) {
StatementResult result = tx.run("MATCH (knight:Person:Knight) WHERE knight.castle = {castle} RETURN knight.name AS name", Values.parameters("castle", "Camelot"));
records = result.list();
}
}
for (Record record : records) {
System.out.println(record.get("name").asString() + " is a knight of Camelot");
}

在查询中使用结果。

1
2
3
4
5
6
7
8
9
10
11
12
StatementResult result;
try (Transaction tx = session.beginTransaction()) {
result = tx.run("MATCH (knight:Person:Knight) WHERE knight.castle = {castle} RETURN id(knight) AS knight_id", Values.parameters("castle", "Camelot"));
}
for (Record record : result.list()) {
try (Transaction tx = session.beginTransaction()) {
result = tx.run("MATCH (knight) WHERE id(knight) = {id} " +
"MATCH (king:Person) WHERE king.name = {king} " +
"CREATE (knight)-[:DEFENDS]->(king)", Values.parameters("id", record.get("knight_id"), "king", "Arthur"));
tx.success();
}
}
结果摘要

Cypher 语句的元数据可以与结果一起展现出来,方法是通过在 Cypher 语句前面添加 PROFILEEXPLAIN 关键字来获得查询操作的运行计划。在 Cypher 指令前面添加 PROFILE 关键字会导致本次查询操作将生成一个运行计划,通过 IresultSummary 对象的 Profile 属性可以获得此运行计划。

1
2
3
4
5
6
try (Transaction tx = session.beginTransaction()) {
StatementResult result = tx.run("PROFILE MATCH (p:Person {name: {name}}) RETURN id(p)", Values.parameters("name", "Camelot"));
ResultSummary summary = result.consume();
System.out.println(summary.statementType());
System.out.println(summary.profile());
}

还可以为查询提供通知,同时也可以查询执行时间。

1
2
3
4
5
6
try (Transaction tx = session.beginTransaction()) {
ResultSummary summary = tx.run("EXPLAIN MATCH (king), (queen) RETURN king, queen").consume();
for (Notification notification : summary.notifications()) {
System.out.println(notification);
}
}

数据类型

Neo4j 具有用于处理和存储数据类型的系统,驱动程序可以将数据在应用程序语言类型和 Neo4j 类型系统之间进行转换,同时为了理解传递参数和过程结果的数据类型,需要知道 Neo4j 类型系统的基础知识和理解 Neo4j 类型在驱动程序中的映射关系。

Neo4j 类型对参数和结果都是有效的:

  • Boolean
  • Integer
  • Float
  • String
  • List
  • Map
  • Node
  • Relationship
  • Path

MapList 可以由数据库处理并在语句中使用。List 的每个元素可以是 Neo4j 的基本类型,也可以存储节点和关系上的属性。节点或关系的属性不能使用 MapList 相组合的类型。

Neo4j Java
Null Null
Boolean java.lang.Boolean
Integer java.lang.Long
Float java.lang.Double
String java.lang.String
List java.util.List
Map java.util.Map<K, V>
Node org.neo4j.driver.v1.types.Node(*)
Relationship org.neo4j.driver.v1.types.Relationship(*)
Path org.neo4j.driver.v1.Path(*)

异常

异常列表如下:

  • ClientException 指示客户端执行的操作不正确,提供的错误代码可用于确定问题的详细原因。
  • ConnectionFailureException 指示底层连接中存在问题,很可能已经被终止。
  • DatabaseException 指示底层数据库中存在问题,提供的错误代码可用于确定问题的详细原因。
  • NoSuchRecordException 每当客户端期望读取不可用的记录(不是服务器返回的)时抛出,通常表示客户端代码和数据库应用程序逻辑之间的期望不匹配。
  • ServiceUnavailableException 表示驱动程序无法与提供的端点通信,在路由 URI 的情况下,可能需要重新创建驱动程序以解决此问题。
  • SessionExpiredException 表示会话不再满足获取他的条件。(例如服务器不再接受写入请求)
  • TransientException 用信号通知可能通过重试来解决的临时故障,提供的错误代码可用于确定问题的详细原因。
1
2
3
4
5
6
7
try {
try (Transaction tx = session.beginTransaction()) {
tx.run("This will case a syntax error").consume();
}
} catch (ClientException e) {

}

引用


个人备注

此博客内容均为作者学习所做笔记,侵删!
若转作其他用途,请注明来源!