查询数据库

一旦您已连接到数据库,就可以通过 Driver.executableQuery() 方法执行 Cypher 查询。

写入数据库

要创建两个分别代表 AliceDavid 的节点,并在它们之间建立 KNOWS 关系,请使用 Cypher 子句 CREATE

创建两个节点和一个关系
// import java.util.Map;
// import java.util.concurrent.TimeUnit;
// import org.neo4j.driver.QueryConfig;

var result = driver.executableQuery("""
    CREATE (a:Person {name: $name})  (1)
    CREATE (b:Person {name: $friendName})
    CREATE (a)-[:KNOWS]->(b)
    """)
    .withParameters(Map.of("name", "Alice", "friendName", "David"))  (2)
    .withConfig(QueryConfig.builder().withDatabase("<database-name>").build())  (3)
    .execute();
var summary = result.summary();  (4)
System.out.printf("Created %d records in %d ms.%n",
    summary.counters().nodesCreated(),
    summary.resultAvailableAfter(TimeUnit.MILLISECONDS));
1 Cypher 查询语句
2 查询参数映射
3 运行查询的目标数据库
4 服务器返回的执行摘要

从数据库读取

要从数据库检索信息,请使用 Cypher 子句 MATCH

检索所有喜欢其他 PersonPerson 节点
// import java.util.concurrent.TimeUnit;
// import org.neo4j.driver.QueryConfig;

var result = driver.executableQuery("""
    MATCH (p:Person)-[:KNOWS]->(:Person)
    RETURN p.name AS name
    """)
    .withConfig(QueryConfig.builder().withDatabase("<database-name>").build())
    .execute();

// Loop through results and do something with them
var records = result.records();  (1)
records.forEach(r -> {
    System.out.println(r);  // or r.get("name").asString()
});

// Summary information
var summary = result.summary();  (2)
System.out.printf("The query %s returned %d records in %d ms.%n",
    summary.query(), records.size(),
    summary.resultAvailableAfter(TimeUnit.MILLISECONDS));
1 records 包含以 Record 对象列表形式呈现的结果
2 summary 包含服务器返回的执行摘要

Record 对象内的属性嵌入在 Value 对象中。要提取并将它们转换为相应的 Java 类型,请使用 .as<type>()(例如 .asString(), asInt() 等)。例如,如果来自数据库的 name 属性是一个字符串,record.get("name").asString() 将把属性值作为 String 对象返回。有关更多信息,请参阅数据类型与 Cypher 类型的映射

从返回的记录中提取值的另一种方法是将其映射为对象

更新数据库

要更新数据库中的节点信息,请使用 Cypher 子句 MATCHSET

更新 Alice 节点以添加 age 属性
// import java.util.Map;
// import org.neo4j.driver.QueryConfig;

var result = driver.executableQuery("""
    MATCH (p:Person {name: $name})
    SET p.age = $age
    """)
    .withConfig(QueryConfig.builder().withDatabase("<database-name>").build())
    .withParameters(Map.of("name", "Alice", "age", 42))
    .execute();
var summary = result.summary();
System.out.println("Query updated the database?");
System.out.println(summary.counters().containsUpdates());

要创建一个链接到两个现有节点的新关系,请结合使用 Cypher 子句 MATCHCREATE

AliceBob 之间创建 :KNOWS 关系
// import java.util.Map;
// import org.neo4j.driver.QueryConfig;

var result = driver.executableQuery("""
    MATCH (alice:Person {name: $name})  (1)
    MATCH (bob:Person {name: $friend})  (2)
    CREATE (alice)-[:KNOWS]->(bob)  (3)
    """)
    .withConfig(QueryConfig.builder().withDatabase("<database-name>").build())
    .withParameters(Map.of("name", "Alice", "friend", "Bob"))
    .execute();
var summary = result.summary();
System.out.println("Query updated the database?");
System.out.println(summary.counters().containsUpdates());
1 检索名为 Alice 的人物节点并将其绑定到变量 alice
2 检索名为 Bob 的人物节点并将其绑定到变量 bob
3 创建一个从 alice 绑定的节点出发,连接到名为 BobPerson 节点的新 :KNOWS 关系

从数据库删除

要删除节点及其所有关联关系,请使用 Cypher 子句 DETACH DELETE

删除 Alice 节点及其所有关系
// import java.util.Map;
// import org.neo4j.driver.QueryConfig;

// This does not delete _only_ p, but also all its relationships!
var result = driver.executableQuery("""
    MATCH (p:Person {name: $name})
    DETACH DELETE p
    """)
    .withConfig(QueryConfig.builder().withDatabase("<database-name>").build())
    .withParameters(Map.of("name", "Alice"))
    .execute();
var summary = result.summary();
System.out.println("Query updated the database?");
System.out.println(summary.counters().containsUpdates());

查询参数

切勿将参数直接硬编码或拼接在查询中。相反,始终使用占位符,并如前例所示,以 Cypher 参数的形式提供动态数据。这是为了:

  1. 性能优势:Neo4j 会编译并缓存查询,但前提是查询结构保持不变;

  2. 出于安全考虑:请参阅防止 Cypher 注入

您可以通过 .withParameters() 方法以映射(Map)的形式提供查询参数。

var result = driver.executableQuery("MATCH (p:Person {name: $name}) RETURN p")
    .withParameters(Map.of("name", "Alice"))
    .withConfig(QueryConfig.builder().withDatabase("<database-name>").build())
    .execute();
在某些情况下,查询结构可能无法在所有部分使用参数。对于这些极少数用例,请参见属性键、关系类型和标签中的动态值

错误处理

查询运行可能会因多种原因失败,并抛出不同的异常

当使用 Driver.executableQuery() 时,如果失败被认为是瞬态的(例如由于服务器暂时不可用),驱动程序会自动重试运行失败的查询。如果操作在配置的最大重试时间之后仍然失败,则会抛出异常。

所有来自服务器的异常都是 Neo4jException 的子类。您可以使用异常的代码(通过 .code() 获取)来稳定地识别特定错误;错误消息并不是稳定的标记,不应依赖它们。

基本错误处理
// import org.neo4j.driver.exceptions.Neo4jException;

try {
    var result = driver.executableQuery("MATCH (p:Person) RETURN ")
    .withConfig(QueryConfig.builder().withDatabase("<database-name>").build())
    .execute();
} catch (Neo4jException e) {
    System.out.printf("Neo4j error code: %s\n", e.code());
    System.out.printf("Exception message: %s\n", e.getMessage());
}
/*
Neo4j error code: Neo.ClientError.Statement.SyntaxError
Exception message: Invalid input '': expected an expression, '*', 'ALL' or 'DISTINCT' (line 1, column 24 (offset: 23))
"MATCH (p:Person) RETURN"
                        ^
*/

异常对象还将错误公开为 GQL 状态对象。Neo4j 错误代码GQL 错误代码的主要区别在于 GQL 代码更细粒度:单个 Neo4j 错误代码可能会被拆分为多个更具体的 GQL 错误代码。

触发异常的实际原因有时可以在通过 .gqlCause() 获取的可选 GQL-status 对象中找到,该对象本身也是一个 Neo4jException。您可能需要递归遍历原因链才能找到捕获异常的根本原因。在下面的示例中,异常的 GQL 状态代码为 42001,但错误的实际来源状态代码为 42I06

Neo4jException 与 GQL 相关方法的使用
// import org.neo4j.driver.exceptions.Neo4jException;

try {
    var result = driver.executableQuery("MATCH (p:Person) RETURN ")
    .withConfig(QueryConfig.builder().withDatabase("<database-name>").build())
    .execute();
} catch (Neo4jException e) {
    System.out.printf("Exception GQL status code: %s\n", e.gqlStatus());
    System.out.printf("Exception GQL status description: %s\n", e.statusDescription());
    System.out.printf("Exception GQL classification: %s\n", e.rawClassification());
    System.out.printf("Exception GQL cause: %s\n", e.gqlCause());
    System.out.printf("Exception GQL diagnostic record: %s\n", e.diagnosticRecord());
}
/*
Exception GQL status code: 42001
Exception GQL status description: error: syntax error or access rule violation - invalid syntax
Exception GQL classification: Optional[CLIENT_ERROR]
Exception GQL cause: Optional[org.neo4j.driver.exceptions.Neo4jException: 42I06: Invalid input '', expected: an expression, '*', 'ALL' or 'DISTINCT'.]
Exception GQL diagnostic record: {_classification="CLIENT_ERROR", OPERATION_CODE="0", OPERATION="", CURRENT_SCHEMA="/", _position={column: 24, offset: 23, line: 1}}
*/

当您希望应用程序根据服务器抛出的确切错误采取不同行为时,GQL 状态码特别有用。

区分不同的错误代码
// import org.neo4j.driver.exceptions.Neo4jException;

try {
    var result = driver.executableQuery("CREATE (p:Person {name: $name}) RETURN ")
    .withParameters(Map.of("name", "Frida"))
    .withConfig(QueryConfig.builder().withDatabase("<database-name>").build())
    .execute();
} catch (Neo4jException e) {
    if (e.containsGqlStatus("42001")) {
        // Neo.ClientError.Statement.SyntaxError
        // special handling of syntax error in query
        System.out.println(e.findByGqlStatus("42001").get().getMessage());
    } else if (e.containsGqlStatus("42NFF")) {
        // Neo.ClientError.Security.Forbidden
        // special handling of user not having CREATE permissions
        System.out.println(e.findByGqlStatus("42NFF").get().getMessage());
    } else {
        // handling of all other exceptions
        System.out.println(e.getMessage());
    }
}

当错误没有 GQL 状态对象时,会返回 GQL 状态码 50N42。如果驱动程序连接到较旧的 Neo4j 服务器,可能会发生这种情况。请勿依赖此状态码,因为未来的 Neo4j 服务器版本可能会将其更改为更合适的状态码。

查询配置

您可以提供额外的配置参数来改变 .executableQuery() 的默认行为。可以通过 .withConfig() 方法来实现,该方法接收一个 QueryConfig 对象。

数据库选择

请始终显式指定数据库,使用 .withDatabase("<dbName>") 方法,即使在单数据库实例上也应如此。这允许驱动程序更高效地工作,因为它节省了一次到服务器解析主数据库的网络往返。如果没有指定数据库,则使用用户的默认数据库 (home database)

// import org.neo4j.driver.QueryConfig;

var result = driver.executableQuery("MATCH (p:Person) RETURN p.name")
    .withConfig(QueryConfig.builder().withDatabase("<database-name>").build())
    .execute();
建议通过配置方法指定数据库,而不是使用 USE Cypher 子句。如果服务器在集群上运行,带有 USE 的查询需要启用 服务器端路由。查询的执行时间也可能更长,因为它们第一次尝试时可能无法到达正确的集群成员,需要被路由到包含所请求数据库的集群成员。

请求路由

在集群环境中,所有查询默认都会定向到主节点(Leader)。为了提高读取查询的性能,您可以使用 .withRouting(RoutingControl.READ) 方法将查询路由到只读节点。

// import org.neo4j.driver.QueryConfig;
// import org.neo4j.driver.RoutingControl;

var result = driver.executableQuery("MATCH (p:Person) RETURN p.name")
    .withConfig(QueryConfig.builder()
        .withDatabase("<database-name>")
        .withRouting(RoutingControl.READ)
        .build())
    .execute();

尽管在读取模式下执行写入查询会导致运行时错误,但您不应依赖此功能进行访问控制。这两种模式的区别在于:读取事务将被路由到集群中的任何节点,而写入事务会被定向到主节点(primaries)。不能保证以读取模式提交的写入查询一定会遭到拒绝。

以其他用户身份运行查询

您可以使用 .withAuthToken() 方法通过其他用户执行查询。在查询级别切换用户比创建新的 Driver 对象成本更低。查询将在给定用户的安全上下文(即默认数据库、权限等)内运行。

// import org.neo4j.driver.AuthTokens;
// import org.neo4j.driver.QueryConfig;

var authToken = AuthTokens.basic("<username>", "<password>");
var result = driver.executableQuery("MATCH (p:Person) RETURN p.name")
    .withAuthToken(authToken)
    .withConfig(QueryConfig.builder()
        .withDatabase("<database-name>")
        .build())
    .execute();

.withImpersonatedUser() 方法提供了类似的功能。不同之处在于,您不需要知道用户的密码即可模拟该用户,但创建 Driver 时所使用的用户必须具有相应的权限

// import org.neo4j.driver.QueryConfig;

var result = driver.executableQuery("MATCH (p:Person) RETURN p.name")
    .withConfig(QueryConfig.builder()
        .withDatabase("<database-name>")
        .withImpersonatedUser("<username>")
        .build())
    .execute();

完整示例

package demo;

import java.util.Map;
import java.util.List;
import java.util.concurrent.TimeUnit;

import org.neo4j.driver.AuthTokens;
import org.neo4j.driver.GraphDatabase;
import org.neo4j.driver.Record;
import org.neo4j.driver.QueryConfig;
import org.neo4j.driver.RoutingControl;
import org.neo4j.driver.exceptions.Neo4jException;

public class App {

    private static final String dbUri = "<database-uri>";
    private static final String dbUser = "<username>";
    private static final String dbPassword = "<password>";

    public static void main(String... args) {

        try (var driver = GraphDatabase.driver(dbUri, AuthTokens.basic(dbUser, dbPassword))) {

            List<Map> people = List.of(
                Map.of("name", "Alice", "age", 42, "friends", List.of("Bob", "Peter", "Anna")),
                Map.of("name", "Bob", "age", 19),
                Map.of("name", "Peter", "age", 50),
                Map.of("name", "Anna", "age", 30)
            );

            try {

                //Create some nodes
                people.forEach(person -> {
                    var result = driver.executableQuery("CREATE (p:Person {name: $person.name, age: $person.age})")
                        .withConfig(QueryConfig.builder().withDatabase("<database-name>").build())
                        .withParameters(Map.of("person", person))
                        .execute();
                });

                // Create some relationships
                people.forEach(person -> {
                    if(person.containsKey("friends")) {
                        var result = driver.executableQuery("""
                            MATCH (p:Person {name: $person.name})
                            UNWIND $person.friends AS friend_name
                            MATCH (friend:Person {name: friend_name})
                            CREATE (p)-[:KNOWS]->(friend)
                             """)
                            .withConfig(QueryConfig.builder().withDatabase("<database-name>").build())
                            .withParameters(Map.of("person", person))
                            .execute();
                    }
                });

                // Retrieve Alice's friends who are under 40
                var result = driver.executableQuery("""
                    MATCH (p:Person {name: $name})-[:KNOWS]-(friend:Person)
                    WHERE friend.age < $age
                    RETURN friend
                     """)
                    .withConfig(QueryConfig.builder()
                        .withDatabase("<database-name>")
                        .withRouting(RoutingControl.READ)
                        .build())
                    .withParameters(Map.of("name", "Alice", "age", 40))
                    .execute();

                // Loop through results and do something with them
                result.records().forEach(r -> {
                    System.out.println(r);
                });

                // Summary information
                System.out.printf("The query %s returned %d records in %d ms.%n",
                    result.summary().query(), result.records().size(),
                    result.summary().resultAvailableAfter(TimeUnit.MILLISECONDS));

            } catch (Neo4jException e) {
                if (e.gqlStatus().equals("42NFF")) {
                    System.out.println("There was a permission issue. Make sure you have correct permissions and try again.");
                } else {
                    System.out.println(e.getMessage());
                    System.exit(1);
                }
            }
        }
    }
}

更多信息请参阅 API 文档 → Driver.executableQuery()

术语表

LTS (长期支持版)

长期支持 (Long Term Support) 版本是保证在若干年内得到支持的版本。Neo4j 4.4 和 5.26 是 LTS 版本。

Aura

Aura 是 Neo4j 的全托管云服务。它提供免费和付费计划。

Cypher

Cypher 是 Neo4j 的图查询语言,允许您从数据库中检索数据。它就像 SQL,但专用于图数据库。

APOC

Awesome Procedures On Cypher (APOC) 是一个包含(许多)函数的库,这些函数在 Cypher 本身中难以轻松实现。

Bolt

Bolt 是用于 Neo4j 实例和驱动程序之间交互的协议。默认监听 7687 端口。

ACID

原子性 (Atomicity)、一致性 (Consistency)、隔离性 (Isolation)、持久性 (Durability) (ACID) 是保证数据库事务可靠处理的属性。符合 ACID 的 DBMS 确保即使发生故障,数据库中的数据也能保持准确和一致。

最终一致性

如果一个数据库能保证所有集群成员在某个时间点都存储了数据的最新版本,则该数据库具有最终一致性。

因果一致性

如果读写查询被集群中的每个成员以相同的顺序看到,则数据库具有因果一致性。这比最终一致性更强。

NULL

空标记不是一种类型,而是缺失值的占位符。更多信息,请参阅 Cypher → 使用 null

事务

事务是一个工作单元,要么被提交,要么在失败时被回滚。例如银行转账:它涉及多个步骤,但它们必须全部成功或全部撤销,以避免钱从一个账户扣除却未存入另一个账户的情况。

背压

背压是对数据流的抵抗力。它确保客户端不会被过快发送的数据压垮,从而超出其处理能力。

书签

书签是代表数据库某种状态的标记。通过将一个或多个书签与查询一起传递,服务器将确保在所表示的状态建立之前,该查询不会被执行。

事务函数

事务函数是由 executeReadexecuteWrite 调用执行的回调。如果发生服务器故障,驱动程序会自动重新执行该回调。

驱动程序 (Driver)

Driver 对象保存了与 Neo4j 数据库建立连接所需的详细信息。