查询数据库

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

由于使用了 async/await,本页面中的示例需要包裹在一个 async 函数中。如果您不确定如何操作,请参阅完整示例

写入数据库

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

创建两个节点和一个关系
let { records, summary } = await driver.executeQuery(`  (1)
  CREATE (a:Person {name: $name})
  CREATE (b:Person {name: $friendName})
  CREATE (a)-[:KNOWS]->(b)
  `,
  { name: 'Alice', friendName: 'David' },  (2)
  { database: '<database-name>' }  (3)
)
console.log(
  `Created ${summary.counters.updates().nodesCreated} nodes ` +
  `in ${summary.resultAvailableAfter} ms.`
)
1 Cypher 查询语句。
2 查询参数对象。
3 运行查询的目标数据库

从数据库读取

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

检索所有喜欢其他 PersonPerson 节点
let { records, summary } = await driver.executeQuery(`
  MATCH (p:Person)-[:KNOWS]->(:Person)
  RETURN p.name AS name
  `,
  {},
  { database: '<database-name>' }
)

// Loop through users and do something with them
for(let record of records) {  (1)
  console.log(`Person with name: ${record.get('name')}`)
  console.log(`Available properties for this node are: ${record.keys}\n`)
}

// Summary information
console.log(  (2)
  `The query \`${summary.query.text}\` ` +
  `returned ${records.length} nodes.\n`
)
1 records 包含实际结果,表现为 Record 对象的列表。
2 summary 包含服务器返回的执行摘要

更新数据库

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

更新 Alice 节点以添加 age 属性
let { _, summary } = await driver.executeQuery(`
  MATCH (p:Person {name: $name})
  SET p.age = $age
  `,
  { name: 'Alice', age: 42 },
  { database: '<database-name>' }
)
console.log('Query counters:')
console.log(summary.counters.updates())

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

AliceBob 之间创建 :KNOWS 关系
let { records, summary } = await driver.executeQuery(`
  MATCH (alice:Person {name: $name})  (1)
  MATCH (bob:Person {name: $friendName})  (2)
  CREATE (alice)-[:KNOWS]->(bob)  (3)
  `, { name: 'Alice', friendName: 'Bob' },
  { database: '<database-name>' }
)
console.log('Query counters:')
console.log(summary.counters.updates())
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!
let { _, summary } = await driver.executeQuery(`
  MATCH (p:Person WHERE p.name = $name)
  DETACH DELETE p
  `, { name: 'Alice' },
  { database: '<database-name>' }
)
console.log('Query counters:')
console.log(summary.counters.updates())

查询参数

切勿将参数硬编码或直接拼接进查询中。相反,请始终使用占位符,并以 Cypher 参数的形式提供动态数据。这样做是为了

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

  2. 安全原因防范 Cypher 注入

在某些情况下,查询结构可能无法在所有部分使用参数。对于这些极少数用例,请参见属性键、关系类型和标签中的动态值

错误处理

查询运行可能会因多种原因而失败。

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

所有来自服务器的错误都是 Neo4jError 的子类。您可以使用异常的代码来稳定地识别特定错误;错误信息并非稳定的标识符,不应依赖它们。

基本错误处理
try {
  let err = await driver.executeQuery(
    'MATCH (p:Person) RETURN ',
    {},
    { database: '<database-name>' }
  )
} catch (err) {
  console.log('Neo4j error code:', err.code)
  console.log('Error message:', err.message)
}
/*
Neo4j error code: Neo.ClientError.Statement.SyntaxError
Error message: Neo4jError: Invalid input '': expected an expression, '*', 'ALL' or 'DISTINCT' (line 1, column 25 (offset: 24))
"MATCH (p:Person) RETURN"
                         ^
*/

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

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

Neo4jError 与 GQL 相关方法的使用
try {
  let err = await driver.executeQuery(
    'MATCH (p:Person) RETURN ',
    {},
    { database: '<database-name>' }
  )
} catch (err) {
  console.log('Error GQL status:', err.gqlStatus)
  console.log('Error GQL status description:', err.gqlStatusDescription)
  console.log('Error GQL classification:', err.classification)
  console.log('Error GQL cause:', err.cause.message)
  console.log('Error GQL diagnostic record:', err.diagnosticRecord)
}
/*
Error GQL status: 42001
Error GQL status description: error: syntax error or access rule violation - invalid syntax
Error GQL classification: CLIENT_ERROR
Error GQL cause: GQLError: 42I06: Invalid input '', expected: an expression, '*', 'ALL' or 'DISTINCT'.
Error GQL diagnostic record: {
  OPERATION: '',
  OPERATION_CODE: '0',
  CURRENT_SCHEMA: '/',
  _classification: 'CLIENT_ERROR',
  _position: {
    line: Integer { low: 1, high: 0 },
    column: Integer { low: 25, high: 0 },
    offset: Integer { low: 24, high: 0 }
  }
}
*/

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

区分不同的错误代码
try {
  let err = await driver.executeQuery(
    'MATCH (p:Person) RETURN ',
    {},
    { database: '<database-name>' }
  )
} catch (err) {
    if (err.findByGqlStatus('42001')) {
        // Neo.ClientError.Statement.SyntaxError
        // special handling of syntax error in query
        console.log(err.message)
    } else if (err.findByGqlStatus('42NFF')) {
        // Neo.ClientError.Security.Forbidden
        // special handling of user not having CREATE permissions
        console.log(err.message)
    } else {
        // handling of all other errors
        console.log(err.message)
    }
}

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

瞬时服务器错误无需更改原始请求即可重试。您可以通过 Neo4jError.isRetryable() 方法判断错误是否为瞬时错误,该方法能让您了解进一步尝试是否可能成功。这在显式事务中运行查询时特别有用,以便了解失败的查询是否值得重新运行。

查询配置

您可以提供一个 QueryConfig 对象作为第三个(可选)参数,以改变 .executeQuery() 的默认行为。

数据库选择

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

await driver.executeQuery(
  'MATCH (p:Person) RETURN p.name',
  {},
  {
    database: '<database-name>'
  }
)
通过配置参数指定数据库优于使用 USE Cypher 子句。如果服务器在集群上运行,使用 USE 的查询需要启用服务器端路由。查询的执行时间也可能更长,因为它们可能无法在第一次尝试时就到达正确的集群成员,并需要被路由到包含所请求数据库的节点。

请求路由

在集群环境中,所有查询默认指向主节点(Leader)。为了提高读取查询的性能,您可以使用配置 routing: 'READ' 将查询路由到读取节点。

await driver.executeQuery(
  'MATCH (p:Person) RETURN p.name',
  {},
  {
    routing: 'READ',  // short for neo4j.routing.READ
    database: '<database-name>'
  }
)

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

以其他用户身份运行查询

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

await driver.executeQuery(
  'MATCH (p:Person) RETURN p.name',
  {},
  {
    auth: neo4j.auth.basic('<username>', '<password>'),
    database: '<database-name>'
  }
)

参数 impersonatedUser 提供了类似的功能。区别在于,您无需知道用户的密码即可模拟他们,但创建 Driver 时所使用的用户必须具有相应的权限

await driver.executeQuery(
  'MATCH (p:Person) RETURN p.name',
  {},
  {
    impersonatedUser: '<username>',
    database: '<database-name>'
  }
)

完整示例

const neo4j = require('neo4j-driver');

(async () => {
  const URI = '<database-uri>'
  const USER = '<username>'
  const PASSWORD = '<password>'
  let driver, result

  let people = [{name: 'Alice', age: 42, friends: ['Bob', 'Peter', 'Anna']},
                {name: 'Bob', age: 19},
                {name: 'Peter', age: 50},
                {name: 'Anna', age: 30}]

  // Connect to database
  try {
    driver = neo4j.driver(URI,  neo4j.auth.basic(USER, PASSWORD))
    await driver.verifyConnectivity()
  } catch(err) {
    console.log(`Connection error\n${err}\nCause: ${err.cause}`)
    await driver.close()
    return
  }

  // Create some nodes
  for(let person of people) {
    await driver.executeQuery(
      'MERGE (p:Person {name: $person.name, age: $person.age})',
      { person: person },
      { database: '<database-name>' }
    )
  }

  // Create some relationships
  for(let person of people) {
    if(person.friends != undefined) {
      await driver.executeQuery(`
        MATCH (p:Person {name: $person.name})
        UNWIND $person.friends AS friendName
        MATCH (friend:Person {name: friendName})
        MERGE (p)-[:KNOWS]->(friend)
        `, { person: person },
        { database: '<database-name>' }
      )
    }
  }

  // Retrieve Alice's friends who are under 40
  result = await driver.executeQuery(`
    MATCH (p:Person {name: $name})-[:KNOWS]-(friend:Person)
    WHERE friend.age < $age
    RETURN friend
    `, { name: 'Alice', age: 40 },
    { database: '<database-name>' }
  )

  // Loop through results and do something with them
  for(let person of result.records) {
    // `person.friend` is an object of type `Node`
    console.log(person.get('friend'))
  }

  // Summary information
  console.log(
    `The query \`${result.summary.query.text}\` ` +
    `returned ${result.records.length} records ` +
    `in ${result.summary.resultAvailableAfter} ms.`
  )

  await driver.close()
})();

术语表

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