查询数据库

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

写入数据库

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

创建两个节点和一个关系
var result = await driver.ExecutableQuery(@"  (1)
    CREATE (a:Person {name: $name})
    CREATE (b:Person {name: $friendName})
    CREATE (a)-[:KNOWS]->(b)
    ")
    .WithParameters(new { name = "Alice", friendName = "David" })  (2)
    .WithConfig(new QueryConfig(database: "<database-name>"))  (3)
    .ExecuteAsync();
var summary = result.Summary;  (4)
Console.WriteLine($"Created {summary.Counters.NodesCreated} nodes in {summary.ResultAvailableAfter.Milliseconds} ms.");
1 Cypher 查询语句
2 查询参数的映射
3 运行查询的目标数据库
4 服务器返回的执行摘要

从数据库读取

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

检索所有喜欢其他 PersonPerson 节点
var result = await driver.ExecutableQuery(@"
    MATCH (p:Person)-[:KNOWS]->(:Person)
    RETURN p.name AS name
    ")
    .WithConfig(new QueryConfig(database: "<database-name>"))
    .ExecuteAsync();

// Loop through results and print people's name
foreach (var record in result.Result) {  (1)
    Console.WriteLine(record.Get<string>("name"));  (2)
}

// Summary information
var summary = result.Summary;  (3)
Console.WriteLine($"The query `{summary.Query.Text}` returned {result.Result.Count()} results in {summary.ResultAvailableAfter.Milliseconds} ms.");
1 result.Result 包含以 IRecord 对象列表形式呈现的结果
2 .Get<type>(key) 从返回的记录中提取名为 key 的条目,并将其转换为 type。有关类型的更多信息,请参阅数据类型及其与 Cypher 类型的映射
3 服务器返回的执行摘要

若要整理返回的数据结构,请在运行查询时使用 .WithMap() 方法。当返回节点而非单个属性时,此方法特别方便。例如,

var result = await driver.ExecutableQuery(@"
    MATCH (p:Person)-[:KNOWS]->(:Person)
    RETURN p.name AS name
    ")
    .WithConfig(new QueryConfig(database: "<database-name>"))
    .WithMap(record => record["name"].As<string>())
    .ExecuteAsync();
foreach (var name in result.Result) {
    Console.WriteLine(name);
}

处理记录的另一种方法是将它们映射到对象

更新数据库

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

更新 Alice 节点以添加 age 属性
var result = await driver.ExecutableQuery(@"
        MATCH (p:Person {name: $name})
        SET p.age = $age
    ")
    .WithConfig(new QueryConfig(database: "<database-name>"))
    .WithParameters(new { name = "Alice", age = 42 })
    .ExecuteAsync();
Console.WriteLine($"Query updated the database? {result.Summary.Counters.ContainsUpdates}");

若要创建一个新的关系,将其链接到两个已存在的节点,请结合使用 Cypher 子句 MATCHCREATE

AliceBob 之间创建 :KNOWS 关系
var result = await driver.ExecutableQuery(@"
        MATCH (alice:Person {name: $name})  (1)
        MATCH (bob:Person {name: $friend})  (2)
        CREATE (alice)-[:KNOWS]->(bob)  (3)
    ")
    .WithConfig(new QueryConfig(database: "<database-name>"))
    .WithParameters(new { name = "Alice", friend = "Bob" })
    .ExecuteAsync();
Console.WriteLine($"Query updated the database? {result.Summary.Counters.ContainsUpdates}");
1 检索名为 Alice 的人物节点并将其绑定到变量 alice
2 检索名为 Bob 的人物节点并将其绑定到变量 bob
3 创建一个从 alice 绑定的节点出发,连接到名为 BobPerson 节点的新 :KNOWS 关系

从数据库删除

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

删除 Alice 节点及其所有关系
// This does not delete _only_ `p`, but also all its relationships!
var result = await driver.ExecutableQuery(@"
        MATCH (p:Person {name: $name})
        DETACH DELETE p
    ")
    .WithConfig(new QueryConfig(database: "<database-name>"))
    .WithParameters(new { name = "Alice" })
    .ExecuteAsync();
Console.WriteLine($"Query updated the database? {result.Summary.Counters.ContainsUpdates}");

查询参数

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

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

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

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

await driver.ExecutableQuery("MATCH (p:Person {name: $name}) RETURN p")
    .WithConfig(new QueryConfig(database: "<database-name>"))
    .WithParameters(new { name = "Alice" })
    .ExecuteAsync();
在某些情况下,查询结构可能导致无法在所有部分使用参数。对于那些罕见的用例,请参阅属性键、关系类型和标签中的动态值

错误处理

查询运行可能会因多种原因而失败。使用 IDriver.ExecutableQuery() 时,如果失败被认为是瞬态的(例如由于服务器临时不可用),驱动程序会自动重试运行失败的查询。如果操作在配置的最大重试时间后仍然失败,则会抛出错误。

所有来自服务器的错误均为 Neo4jException 类型(文档链接)。您可以使用异常代码稳定地标识特定错误;相反,错误消息不是稳定的标记,不应依赖它们。

基本错误处理
try {
    await driver.ExecutableQuery("MATCH (p:Person) RETURN")
        .WithConfig(new QueryConfig(database: "<database-name>"))
        .ExecuteAsync();
} catch (Neo4jException e) {
    Console.WriteLine($"Neo4j error code: {e.Code}");
    Console.WriteLine($"Exception message: {e.Message}");
}
/*
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 错误代码。

触发异常的实际原因有时可以在可选的内部异常 IGqlStatusObject.InnerException 中找到,该异常本身是一个 GQL 状态对象。在到达所捕获异常的根本原因之前,您可能需要递归遍历原因链。在下面的示例中,异常的 GQL 状态代码为 42001,但错误的实际来源状态代码为 42I06

Neo4jException 与 GQL 相关方法结合使用
try {
    await driver.ExecutableQuery("MATCH (p:Person) RETURN")
        .WithConfig(new QueryConfig(database: "<database-name>"))
        .ExecuteAsync();
} catch (Neo4jException e) {
    Console.WriteLine($"Error GQL status code: {e.GqlStatus}");
    Console.WriteLine($"Error GQL status description: {e.GqlStatusDescription}");
    Console.WriteLine($"Error GQL classification: {e.GqlClassification}");
    Console.WriteLine("Error GQL diagnostic record: {");
    foreach (KeyValuePair<string, object> kvp in e.GqlDiagnosticRecord) {
        Console.WriteLine("  {0}: {1}", kvp.Key, kvp.Value);
    }
    Console.WriteLine("}");
    Console.WriteLine($"Error GQL cause: {e.InnerException}");
}
/*
Error GQL status code: 42001
Error GQL status description: error: syntax error or access rule violation - invalid syntax
Error GQL classification: CLIENT_ERROR
Error GQL diagnostic record: {
  _classification: CLIENT_ERROR
  _position: System.Collections.Generic.Dictionary`2[System.String,System.Object]
  OPERATION:
  OPERATION_CODE: 0
  CURRENT_SCHEMA: /
}
Error GQL cause: Neo4j.Driver.DatabaseException: 42I06: Invalid input '', expected: an expression, '*' or 'DISTINCT'.
*/

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

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

瞬态服务器错误可以在无需更改原始请求的情况下重试。您可以通过属性 IsRetriable 发现错误是否为瞬态错误,该属性提供了进一步尝试是否可能成功的见解。这在显式事务中运行查询时特别有用,可以判断失败的查询是否值得重新运行。

查询配置

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

数据库选择

请始终明确指定数据库,即使在单数据库实例中也是如此。这使得驱动程序能更高效地工作,因为它节省了向服务器解析主数据库所需的网络往返。如果未指定数据库,则使用用户的默认主数据库

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

请求路由

在集群环境中,所有查询默认定向到主节点(Leader)。为了提高读取查询的性能,请使用 routing: RoutingControl.Readers 将查询路由到只读节点。

await driver.ExecutableQuery("MATCH (p:Person) RETURN p.name")
    .WithConfig(new QueryConfig(
        database: "<database-name>",
        routing: RoutingControl.Readers
    ))
    .ExecuteAsync();

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

以其他用户身份运行查询

您可以使用配置键 authToken 通过不同的用户执行查询。在查询级别切换用户比创建新的 IDriver 对象成本更低。查询将在给定用户的安全上下文中运行(例如,主数据库、权限等)。有关身份验证方法的可用原生实现,请参阅 AuthTokens

await driver.ExecutableQuery("MATCH (p:Person) RETURN p.name")
    .WithConfig(new QueryConfig(
        database: "<database-name>",
        authToken: AuthTokens.Basic("<username>", "<password>")
    ))
    .ExecuteAsync();

QueryConfigimpersonatedUser 提供了类似的功能:不同之处在于,您不需要知道用户的密码即可模拟他们,但创建 IDriver 时使用的用户需要拥有 IMPERSONATE 权限

await driver.ExecutableQuery("MATCH (p:Person) RETURN p.name")
    .WithConfig(new QueryConfig(
        database: "<database-name>",
        impersonatedUser: "<username>"
    ))
    .ExecuteAsync();

完整示例

此示例使用了 Cypher 子句 MERGE,以避免在代码运行多次时产生数据重复。

using Neo4j.Driver;

// URI examples: "neo4j://", "neo4j+s://xxx.databases.neo4j.io"
const string dbUri = "<database-uri>";
const string dbUser = "<username>";
const string dbPassword = "<password>";

await using var driver = GraphDatabase.Driver(dbUri, AuthTokens.Basic(dbUser, dbPassword));
await driver.VerifyConnectivityAsync();
Console.WriteLine("Connection established.");

// Toy dataset
var people = new List<Dictionary<string, dynamic>>();
people.Add(
    new Dictionary<string, dynamic>() {
        {"name", "Alice"},
        {"age", 42},
        {"friends", new List<string>(){"Bob", "Peter", "Anna"}},
    }
);
people.Add(
    new Dictionary<string, dynamic>() {
        {"name", "Bob"},
        {"age", 19},
    }
);
people.Add(
    new Dictionary<string, dynamic>() {
        {"name", "Peter"},
        {"age", 50},
    }
);
people.Add(
    new Dictionary<string, dynamic>() {
        {"name", "Anna"},
        {"age", 33},
    }
);

try {
    //Create some nodes
    foreach (var person in people) {
        await driver
            .ExecutableQuery("MERGE (:Person {name: $person.name, age: $person.age})")
            .WithConfig(new QueryConfig(database: "<database-name>"))
            .WithParameters(new { person = person })
            .ExecuteAsync();
    }

    // Create some relationships
    foreach (var person in people) {
        if (person.ContainsKey("friends")) {
            await driver.ExecutableQuery(@"
                    MATCH (p:Person {name: $person.name})
                    UNWIND $person.friends AS friend_name
                    MATCH (friend:Person {name: friend_name})
                    MERGE (p)-[:KNOWS]->(friend)
                ")
                .WithConfig(new QueryConfig(database: "<database-name>"))
                .WithParameters(new { person = person })
                .ExecuteAsync();
        }
    }

    // Retrieve Alice's friends who are under 40
    Console.WriteLine("Alice's friends under 40:");
    var result = await driver.ExecutableQuery(@"
            MATCH (p:Person {name: $name})-[:KNOWS]-(friend:Person)
            WHERE friend.age < $age
            RETURN friend
        ")
        .WithConfig(new QueryConfig(database: "<database-name>", routing: RoutingControl.Readers))
        .WithParameters(new { name = "Alice", age = 40 })
        .ExecuteAsync();

    // Loop through results and do something with them
    foreach (var record in result.Result) {
        Console.WriteLine(record.Get<INode>("friend").Get<string>("name"));
    }

    // Summary information
    var summary = result.Summary;
    Console.WriteLine($"The query `{summary.Query.Text}` returned {result.Result.Count()} results in {summary.ResultAvailableAfter.Milliseconds} ms.");

} catch (Neo4jException e) {
    Console.WriteLine(e);
    Environment.Exit(1);
}

有关更多信息,请参阅 API 文档 → IDriver.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

事务

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

背压

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

书签

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

事务函数

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

IDriver

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