查询数据库

一旦您已连接到数据库,就可以使用 CypherExecuteQuery() 函数来运行查询。

写入数据库

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

创建两个节点和一个关系
result, err := neo4j.ExecuteQuery(ctx, driver, `
    CREATE (a:Person {name: $name})  (1)
    CREATE (b:Person {name: $friendName})
    CREATE (a)-[:KNOWS]->(b)
    `,
    map[string]any{  (2)
        "name": "Alice",
        "friendName": "David",
    }, neo4j.EagerResultTransformer,
    neo4j.ExecuteQueryWithDatabase("<database-name>"))  (3)
if err != nil {
    panic(err)
}

summary := result.Summary  (4)
fmt.Printf("Created %v nodes in %+v.\n",
    summary.Counters().NodesCreated(),
    summary.ResultAvailableAfter())
1 Cypher 查询语句
2 查询参数映射
3 运行查询的目标数据库
4 服务器返回的执行摘要

从数据库读取

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

检索所有喜欢其他 PersonPerson 节点
result, err := neo4j.ExecuteQuery(ctx, driver, `
    MATCH (p:Person)-[:KNOWS]->(:Person)
    RETURN p.name AS name
    `,
    nil,
    neo4j.EagerResultTransformer,
    neo4j.ExecuteQueryWithDatabase("<database-name>"))
if err != nil {
    panic(err)
}

// Loop through results and do something with them
for _, record := range result.Records {  (1)
    name, _ := record.Get("name")  // .Get() 2nd return is whether key is present
    fmt.Println(name)
    // or
    // fmt.Println(record.AsMap())  // get Record as a map
}

// Summary information  (2)
fmt.Printf("The query `%v` returned %v records in %+v.\n",
    result.Summary.Query().Text(), len(result.Records),
    result.Summary.ResultAvailableAfter())
1 result.Records 包含以 Record 对象数组形式呈现的结果
2 result.Summary 包含服务器返回的执行摘要
访问记录内容时,其所有属性的类型均为 any这意味着如果您想使用这些类型上定义的方法/功能,必须将其转换为相关的 Go 类型。例如,如果来自数据库的 name 属性是字符串,则 record.AsMap()["name"][1] 会在编译时导致无效操作错误。要使其生效,请将其用作字符串之前将值转换为字符串:name := record.AsMap()["name"].(string),然后使用 name[1]

更新数据库

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

更新 Alice 节点以添加 age 属性
result, err := neo4j.ExecuteQuery(ctx, driver, `
    MATCH (p:Person {name: $name})
    SET p.age = $age
    `, map[string]any{
        "name": "Alice",
        "age": 42,
    }, neo4j.EagerResultTransformer,
    neo4j.ExecuteQueryWithDatabase("<database-name>"))
if err != nil {
    panic(err)
}
fmt.Println("Query updated the database?",
    result.Summary.Counters().ContainsUpdates())

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

AliceBob 之间创建 :KNOWS 关系
result, err := neo4j.ExecuteQuery(ctx, driver, `
    MATCH (alice:Person {name: $name})  (1)
    MATCH (bob:Person {name: $friend})  (2)
    CREATE (alice)-[:KNOWS]->(bob)  (3)
    `, map[string]any{
        "name": "Alice",
        "friend": "Bob",
    }, neo4j.EagerResultTransformer,
    neo4j.ExecuteQueryWithDatabase("<database-name>"))
if err != nil {
    panic(err)
}
fmt.Println("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!
result, err := neo4j.ExecuteQuery(ctx, driver, `
    MATCH (p:Person {name: $name})
    DETACH DELETE p
    `, map[string]any{
        "name": "Alice",
    }, neo4j.EagerResultTransformer,
    neo4j.ExecuteQueryWithDatabase("<database-name>"))
if err != nil {
    panic(err)
}
fmt.Println("Query updated the database?",
    result.Summary.Counters().ContainsUpdates())

查询参数

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

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

  2. 安全原因:参见防范 Cypher 注入

查询参数应分组为一个映射,并作为第二个参数传递给 ExecuteQuery()。如果查询没有参数,您可以传入 nil 而不是空映射。

parameters := map[string]any{
    "name": "Alice",
    "age": 42,
}
neo4j.ExecuteQuery(ctx, driver,
    "MERGE (:Person {name: $name, age: $age})",
    parameters,
    neo4j.EagerResultTransformer,
    neo4j.ExecuteQueryWithDatabase("<database-name>"))
在某些情况下,查询结构可能无法在所有部分使用参数。对于这些极少数用例,请参见属性键、关系类型和标签中的动态值

错误处理

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

所有来自服务器的错误类型均为 Neo4jError。您可以使用异常代码稳定地识别特定错误;错误消息不是稳定的标记,不应依赖它们。

基本错误处理
result, err := neo4j.ExecuteQuery(ctx, driver, "MATCH (p:Person) RETURN", nil,
    neo4j.EagerResultTransformer,
    neo4j.ExecuteQueryWithDatabase("<database-name>"))
if err != nil {
    var neo4jErr *neo4j.Neo4jError
    if errors.As(err, &neo4jErr) {
        // error is of type Neo4jError
        fmt.Println("Neo4j error code:", neo4jErr.Code)
        fmt.Println("Error message:", neo4jErr.Msg)
    } else {
        fmt.Printf("Non-Neo4j error: %s\n", err.Error())
    }
}
/*
Neo4j error code: Neo.ClientError.Statement.SyntaxError
Error 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 错误代码。

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

Neo4jError 与 GQL 相关方法的使用
result, err := neo4j.ExecuteQuery(ctx, driver, "MATCH (p:Person) RETURN", nil,
    neo4j.EagerResultTransformer,
    neo4j.ExecuteQueryWithDatabase("<database-name>"))
if err != nil {
    var neo4jErr *neo4j.Neo4jError
    if errors.As(err, &neo4jErr) {
        // Error is of type Neo4jError
        fmt.Println("Error GQL status code:", neo4jErr.GqlStatus)
        fmt.Println("Error GQL status description:", neo4jErr.GqlStatusDescription)
        fmt.Println("Error GQL classification:", neo4jErr.GqlClassification)
        if neo4jErr.GqlDiagnosticRecord != nil {
            fmt.Printf("Error GQL diagnostic record: %+v\n", neo4jErr.GqlDiagnosticRecord)
        }
        if neo4jErr.GqlCause != nil {
            fmt.Println("Error GQL cause:", neo4jErr.GqlCause.Error())
        }
    } else {
        fmt.Println("Non-Neo4j error:", err.Error())
    }
}
/*
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: map[CURRENT_SCHEMA:/ OPERATION: OPERATION_CODE:0 _classification:CLIENT_ERROR _position:map[column:24 line:1 offset:23]]
Error GQL cause: Neo4jError: Neo.DatabaseError.General.UnknownError (42I06: Invalid input '', expected: an expression, '*', 'ALL' or 'DISTINCT'.)
*/

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

区分不同的错误代码
result, err := neo4j.ExecuteQuery(ctx, driver, "MATCH (p:Person) RETURN", nil,
    neo4j.EagerResultTransformer,
    neo4j.ExecuteQueryWithDatabase("<database-name>"))
if err != nil {
    var neo4jErr *neo4j.Neo4jError
    if errors.As(err, &neo4jErr) {
        if neo4jErr.ContainsGqlStatus("42001") {
            // Neo.ClientError.Statement.SyntaxError
            // special handling of syntax error in query
            fmt.Println(neo4jErr.Error())
        } else if neo4jErr.ContainsGqlStatus("42NFF") {
            // Neo.ClientError.Security.Forbidden
            // special handling of user not having CREATE permissions
            fmt.Println(neo4jErr.Error())
        } else {
            // handling of all other exceptions
            fmt.Println(neo4jErr.Error())
        }
    }
}

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

瞬态服务器错误可以在无需更改原始请求的情况下进行重试。您可以通过 neo4j.IsRetryable(error) 函数来发现错误是否为瞬态错误,该函数能洞察进一步尝试是否可能成功。这在显式事务中运行查询时特别有用,可以判断失败的查询是否值得重新运行。

查询配置

您可以提供进一步的配置参数来更改 ExecuteQuery() 的默认行为。这些参数作为从第 4 个函数参数开始的任意数量的回调提供。

数据库选择

请务必使用 neo4j.ExecuteQueryWithDatabase("<dbName>") 回调显式指定数据库,即使在单数据库实例中也是如此。这允许驱动程序更高效地工作,因为它节省了向服务器解析主数据库所需的网络往返。如果未指定数据库,则使用用户的默认数据库

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

请求路由

在集群环境中,所有查询默认导向主节点(leader node)。为了提高读取查询的性能,您可以使用回调 neo4j.ExecuteQueryWithReadersRouting() 将查询路由到读取节点。

neo4j.ExecuteQuery(ctx, driver,
    "MATCH (p:Person) RETURN p.name",
    nil,
    neo4j.EagerResultTransformer,
    neo4j.ExecuteQueryWithDatabase("<database-name>"),
    neo4j.ExecuteQueryWithReadersRouting())

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

以其他用户身份运行查询

您可以通过配置回调 neo4j.ExecuteQueryWithAuthToken() 以其他用户身份执行查询。在查询级别切换用户比创建新的 Driver 对象成本更低。查询随后将在给定用户的安全上下文中运行(例如默认数据库、权限等)。

queryAuth := neo4j.BasicAuth("<username>", "<password>", "")
neo4j.ExecuteQuery(ctx, driver,
    "MATCH (p:Person) RETURN p.name",
    nil,
    neo4j.EagerResultTransformer,
    neo4j.ExecuteQueryWithDatabase("<database-name>"),
    neo4j.ExecuteQueryWithAuthToken(queryAuth))

回调 neo4j.ExecuteQueryWithImpersonatedUser() 提供了类似的功能。区别在于您无需知道用户的密码即可模拟他们,但创建 Driver 时所用的用户需要拥有相应的权限

neo4j.ExecuteQuery(ctx, driver,
    "MATCH (p:Person) RETURN p.name",
    nil,
    neo4j.EagerResultTransformer,
    neo4j.ExecuteQueryWithDatabase("<database-name>"),
    neo4j.ExecuteQueryWithImpersonatedUser("<username>"))

完整示例

package main

import (
    "fmt"
    "context"
    "github.com/neo4j/neo4j-go-driver/v6/neo4j"
)

func main() {
    ctx := context.Background()

    // Connection to database
    dbUri := "<database-uri>"
    dbUser := "<username>"
    dbPassword := "<password>"
    driver, err := neo4j.NewDriver(
        dbUri,
        neo4j.BasicAuth(dbUser, dbPassword, ""))
    if err != nil {
        panic(err)
    }
    defer driver.Close(ctx)
    err = driver.VerifyConnectivity(ctx)
    if err != nil {
        panic(err)
    }

    // Prepare data
    people := []map[string]any {
       {"name": "Alice", "age": 42, "friends": []string{"Bob", "Peter", "Anna"},},
       {"name": "Bob", "age": 19,},
       {"name": "Peter", "age": 50,},
       {"name": "Anna", "age": 30,},
    }

    // Create some nodes
    for _, person := range people {
        _, err := neo4j.ExecuteQuery(ctx, driver,
            "MERGE (p:Person {name: $person.name, age: $person.age})",
            map[string]any{
                "person": person,
            }, neo4j.EagerResultTransformer,
            neo4j.ExecuteQueryWithDatabase("<database-name>"))
        if err != nil {
            panic(err)
        }
    }

    // Create some relationships
    for _, person := range people {
        if person["friends"] != "" {
            _, err := neo4j.ExecuteQuery(ctx, driver, `
                MATCH (p:Person {name: $person.name})
                UNWIND $person.friends AS friend_name
                MATCH (friend:Person {name: friend_name})
                MERGE (p)-[:KNOWS]->(friend)
                `, map[string]any{
                    "person": person,
                }, neo4j.EagerResultTransformer,
                neo4j.ExecuteQueryWithDatabase("<database-name>"))
            if err != nil {
                panic(err)
            }
        }
    }

    // Retrieve Alice's friends who are under 40
    result, err := neo4j.ExecuteQuery(ctx, driver, `
        MATCH (p:Person {name: $name})-[:KNOWS]-(friend:Person)
        WHERE friend.age < $age
        RETURN friend
        `, map[string]any{
            "name": "Alice",
            "age": 40,
        }, neo4j.EagerResultTransformer,
        neo4j.ExecuteQueryWithDatabase("<database-name>"))
    if err != nil {
        panic(err)
    }

    // Loop through results and do something with them
    for _, record := range result.Records {
        person, _ := record.Get("friend")
        fmt.Println(person)
        // or
        // fmt.Println(record.AsMap())
    }

    // Summary information
    fmt.Printf("\nThe query `%v` returned %v records in %+v.\n",
        result.Summary.Query().Text(), len(result.Records),
        result.Summary.ResultAvailableAfter())
}

更多信息请参见 API 文档 → ExecuteQuery()

术语表

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 数据库建立连接所需的详细信息。