使用 Neo4j 和 Go 构建应用程序

Neo4j Go 驱动程序是通过 Go 应用程序与 Neo4j 实例交互的官方库。

Neo4j 的核心是 Cypher,它是与 Neo4j 数据库交互的查询语言。虽然本指南并不 *要求* 您是经验丰富的 Cypher 查询员,但如果您已经了解一些 Cypher,则可以更轻松地专注于 Go 特定的部分。因此,虽然本指南 *也* 在过程中提供了对 Cypher 的简要介绍,但如果您是第一次接触,请考虑查看 入门 → Cypher,以更详细地了解图数据库建模和查询。然后,您可以在遵循本指南开发 Go 应用程序时应用这些知识。

安装

在模块内,使用 go get 安装 Neo4j Go 驱动程序

go get github.com/neo4j/neo4j-go-driver/v5

连接到数据库

通过创建 DriverWithContext 对象并提供 URL 和身份验证令牌来连接到数据库。获得 DriverWithContext 实例后,使用 .VerifyConnectivity() 方法确保可以建立工作连接。

package main

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

func main() {
    ctx := context.Background()
    // URI examples: "neo4j://localhost", "neo4j+s://xxx.databases.neo4j.io"
    dbUri := "<URI for Neo4j database>"
    dbUser := "<Username>"
    dbPassword := "<Password>"
    driver, err := neo4j.NewDriverWithContext(
        dbUri,
        neo4j.BasicAuth(dbUser, dbPassword, ""))
    defer driver.Close(ctx)

    err = driver.VerifyConnectivity(ctx)
    if err != nil {
        panic(err)
    }
    fmt.Println("Connection established.")
}

查询数据库

使用函数 ExecuteQuery() 执行 Cypher 语句。不要硬编码或连接参数:使用占位符并将参数指定为关键字参数。

// Get the name of all 42 year-olds
result, _ := neo4j.ExecuteQuery(ctx, driver,
    "MATCH (p:Person {age: $age}) RETURN p.name AS name",
    map[string]any{
        "age": "42",
    }, neo4j.EagerResultTransformer,
    neo4j.ExecuteQueryWithDatabase("neo4j"))

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

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

运行您自己的事务

对于更高级的使用案例,您可以运行 事务。使用方法 Session.ExecuteRead()Session.ExecuteWrite() 来运行托管事务。

包含多个查询、客户端逻辑和潜在回滚的事务
package main

import (
    "fmt"
    "context"
    "strconv"
    "errors"
    "github.com/neo4j/neo4j-go-driver/v5/neo4j"
)

func main() {
    ctx := context.Background()
    var employeeThreshold int64 = 10  // Neo4j's integer maps to Go's int64

    // Connection to database
    dbUri := "<URI for Neo4j database>"
    dbUser := "<Username>"
    dbPassword := "<Password>"
    driver, err := neo4j.NewDriverWithContext(
        dbUri,
        neo4j.BasicAuth(dbUser, dbPassword, ""))
    if err != nil {
        panic(err)
    }
    defer driver.Close(ctx)
    err = driver.VerifyConnectivity(ctx)
    if err != nil {
        panic(err)
    }

    session := driver.NewSession(ctx, neo4j.SessionConfig{DatabaseName: "neo4j"})
    defer session.Close(ctx)

    // Create 100 people and assign them to various organizations
    for i := 0; i < 100; i++ {
        name := "Thor" + strconv.Itoa(i)
        orgId, err := session.ExecuteWrite(ctx,
            func(tx neo4j.ManagedTransaction) (any, error) {
                var orgId string

                // Create new Person node with given name, if not exists already
                _, err := tx.Run(
                    ctx,
                    "MERGE (p:Person {name: $name})",
                    map[string]any{
                        "name": name,
                    })
                if err != nil {
                    return nil, err
                }

                // Obtain most recent organization ID and the number of people linked to it
                result, err := tx.Run(
                    ctx, `
                    MATCH (o:Organization)
                    RETURN o.id AS id, COUNT{(p:Person)-[r:WORKS_FOR]->(o)} AS employeesN
                    ORDER BY o.createdDate DESC
                    LIMIT 1
                    `, nil)
                if err != nil {
                    return nil, err
                }
                org, err := result.Single(ctx)

                // If no organization exists, create one and add Person to it
                if org == nil {
                    orgId, _ = createOrganization(ctx, tx)
                    fmt.Println("No orgs available, created", orgId)
                    err = addPersonToOrganization(ctx, tx, name, orgId)
                    if err != nil {
                        return nil, errors.New("Failed to add person to new org")
                        // Transaction will roll back
                        // -> not even Person and/or Organization is created!
                    }
                } else {
                    orgId = org.AsMap()["id"].(string)
                    if employeesN := org.AsMap()["employeesN"].(int64);
                       employeesN == 0 {
                        return nil, errors.New("Most recent organization is empty")
                        // Transaction will roll back
                        // -> not even Person is created!
                    }

                    // If org does not have too many employees, add this Person to it
                    if employeesN := org.AsMap()["employeesN"].(int64);
                       employeesN < employeeThreshold {
                        err = addPersonToOrganization(ctx, tx, name, orgId)
                        if err != nil {
                            return nil, err
                            // Transaction will roll back
                            // -> not even Person is created!
                        }
                    // Otherwise, create a new Organization and link Person to it
                    } else {
                        orgId, err = createOrganization(ctx, tx)
                        if err != nil {
                            return nil, err
                            // Transaction will roll back
                            // -> not even Person is created!
                        }
                        fmt.Println("Latest org is full, created", orgId)
                        err = addPersonToOrganization(ctx, tx, name, orgId)
                        if err != nil {
                            return nil, err
                            // Transaction will roll back
                            // -> not even Person and/or Organization is created!
                        }
                    }
                }
                // Return the Organization ID to which the new Person ends up in
                return orgId, nil
            })
        if err != nil {
            fmt.Println(err)
        } else {
            fmt.Println("User", name, "added to organization", orgId)
        }
    }
}

func createOrganization(ctx context.Context, tx neo4j.ManagedTransaction) (string, error) {
    result, err := tx.Run(
        ctx, `
        CREATE (o:Organization {id: randomuuid(), createdDate: datetime()})
        RETURN o.id AS id
        `, nil)
    if err != nil {
        return "", err
    }
    org, err := result.Single(ctx)
    if err != nil {
        return "", err
    }
    orgId, _ := org.AsMap()["id"]
    return orgId.(string), err
}

func addPersonToOrganization(ctx context.Context, tx neo4j.ManagedTransaction, personName string, orgId string) (error) {
    _, err := tx.Run(
        ctx, `
        MATCH (o:Organization {id: $orgId})
        MATCH (p:Person {name: $name})
        MERGE (p)-[:WORKS_FOR]->(o)
        `, map[string]any{
            "orgId": orgId,
            "name": personName,
        })
    return err
}

关闭连接和会话

在所有 DriverWithContextSessionWithContext 实例上调用 .close() 方法以释放它们持有的任何资源。最佳做法是在创建新对象后立即使用 defer 关键字调用这些方法。

driver, err := neo4j.NewDriverWithContext(dbUri, neo4j.BasicAuth(dbUser, dbPassword, ""))
defer driver.Close(ctx)
session := driver.NewSession(ctx, neo4j.SessionConfig{DatabaseName: "neo4j"})
defer session.Close(ctx)

API 文档

有关驱动程序功能的深入信息,请查看 API 文档.

词汇表

LTS

长期支持 版本是保证支持数年的一种版本。Neo4j 4.4 是 LTS,Neo4j 5 也将有一个 LTS 版本。

Aura

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

Cypher

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

APOC

Cypher 上的强大程序 (APOC) 是一个包含 (许多) 函数的库,这些函数无法轻松地在 Cypher 本身中表达。

Bolt

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

ACID

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

最终一致性

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

因果一致性

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

NULL

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

事务

事务是工作单元,要么 *完整提交*,要么在发生故障时 *回滚*。例如银行转账:它包含多个步骤,但它们必须 *全部* 成功或被恢复,以避免从一个帐户中扣除资金,而未将其添加到另一个帐户中。

背压

背压是反对数据流动的力。它确保客户端不会被超出其处理能力的数据淹没。

事务函数

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

DriverWithContext

DriverWithContext 对象包含建立与 Neo4j 数据库连接所需的详细信息。