Loading...
Loading...
Compare original and translation side by side
neo4j-cypher-skillneo4j-migration-skillneo4j-cypher-skillneo4j-migration-skillnpm install neo4j-driver # or: yarn add neo4j-drivernpm install neo4j-driver # 或:yarn add neo4j-driverundefinedundefined
```javascript
// npm install dotenv (for Node.js < 20 or when .env auto-load is off)
import 'dotenv/config' // or: require('dotenv').config()
const URI = process.env.NEO4J_URI
const USER = process.env.NEO4J_USERNAME
const PASSWORD = process.env.NEO4J_PASSWORD
const DATABASE = process.env.NEO4J_DATABASE ?? 'neo4j'.env--env-file .env.env
```javascript
// npm install dotenv(适用于Node.js < 20或.env自动加载关闭的情况)
import 'dotenv/config' // 或:require('dotenv').config()
const URI = process.env.NEO4J_URI
const USER = process.env.NEO4J_USERNAME
const PASSWORD = process.env.NEO4J_PASSWORD
const DATABASE = process.env.NEO4J_DATABASE ?? 'neo4j'--env-file .env.env.env// CommonJS
const neo4j = require('neo4j-driver')
// ESM / TypeScript
import neo4j from 'neo4j-driver'
const driver = neo4j.driver(
process.env.NEO4J_URI, // 'neo4j+s://xxx.databases.neo4j.io'
neo4j.auth.basic(process.env.NEO4J_USER, process.env.NEO4J_PASSWORD)
)
await driver.verifyConnectivity() // fail fast on startup if unreachable
// On shutdown:
await driver.close()| Scheme | Transport | Use |
|---|---|---|
| TLS + cluster routing | Aura; production clusters |
| plaintext + cluster routing | local dev cluster |
| TLS, single instance | single Neo4j instance with TLS |
| plaintext, single instance | local single instance |
neo4j.auth.basic(user, password) // username/password
neo4j.auth.bearer(token) // SSO / JWT
neo4j.auth.kerberos(base64Ticket) // Kerberos
neo4j.auth.none() // unauthenticated (dev only)// db.js
let _driver = null
export function getDriver() {
if (!_driver) _driver = neo4j.driver(process.env.NEO4J_URI,
neo4j.auth.basic(process.env.NEO4J_USER, process.env.NEO4J_PASSWORD))
return _driver
}
export async function closeDriver() {
if (_driver) { await _driver.close(); _driver = null }
}maxConnectionPoolSize: 5SIGTERM// CommonJS
const neo4j = require('neo4j-driver')
// ESM / TypeScript
import neo4j from 'neo4j-driver'
const driver = neo4j.driver(
process.env.NEO4J_URI, // 'neo4j+s://xxx.databases.neo4j.io'
neo4j.auth.basic(process.env.NEO4J_USER, process.env.NEO4J_PASSWORD)
)
await driver.verifyConnectivity() // 启动时如果无法连接则快速失败
// 关闭时:
await driver.close()| 协议 | 传输方式 | 适用场景 |
|---|---|---|
| TLS + 集群路由 | Aura环境;生产集群 |
| 明文 + 集群路由 | 本地开发集群 |
| TLS,单实例 | 启用TLS的单个Neo4j实例 |
| 明文,单实例 | 本地单实例 |
neo4j.auth.basic(user, password) // 用户名/密码认证
neo4j.auth.bearer(token) // SSO / JWT认证
neo4j.auth.kerberos(base64Ticket) // Kerberos认证
neo4j.auth.none() // 无认证(仅开发环境使用)// db.js
let _driver = null
export function getDriver() {
if (!_driver) _driver = neo4j.driver(process.env.NEO4J_URI,
neo4j.auth.basic(process.env.NEO4J_USER, process.env.NEO4J_PASSWORD))
return _driver
}
export async function closeDriver() {
if (_driver) { await _driver.close(); _driver = null }
}maxConnectionPoolSize: 5SIGTERM| API | Use when | Auto-retry | Result |
|---|---|---|---|
| Default for most queries | ✅ | eager (all records) |
| Large results, streaming, multi-query tx | ✅ | lazy stream |
| | ❌ | lazy stream |
| API | 适用场景 | 自动重试 | 返回结果 |
|---|---|---|---|
| 大多数查询的默认选择 | ✅ | 立即获取所有记录 |
| 处理大量结果、流式传输、多查询事务 | ✅ | 延迟加载流 |
| | ❌ | 延迟加载流 |
executeQueryexecuteQueryconst { records, summary, keys } = await driver.executeQuery(
'MATCH (p:Person {name: $name})-[:KNOWS]->(f) RETURN f.name AS name',
{ name: 'Alice' },
{ database: 'neo4j', routing: neo4j.routing.READ }
)
for (const record of records) {
console.log(record.get('name')) // use .get() — records are NOT plain objects
}
// Write and count results
const { summary: s } = await driver.executeQuery(
'CREATE (p:Person {name: $name, age: $age})',
{ name: 'Bob', age: neo4j.int(30) },
{ database: 'neo4j' }
)
console.log(s.counters.updates().nodesCreated) // ✅ must call .updates()database// ❌ injection risk + disables plan caching
await driver.executeQuery(`MATCH (p:Person {name: '${name}'}) RETURN p`)
// ✅ parameterised
await driver.executeQuery('MATCH (p:Person {name: $name}) RETURN p', { name })const { records, summary, keys } = await driver.executeQuery(
'MATCH (p:Person {name: $name})-[:KNOWS]->(f) RETURN f.name AS name',
{ name: 'Alice' },
{ database: 'neo4j', routing: neo4j.routing.READ }
)
for (const record of records) {
console.log(record.get('name')) // 使用.get()——记录并非普通对象
}
// 写入操作并统计结果
const { summary: s } = await driver.executeQuery(
'CREATE (p:Person {name: $name, age: $age})',
{ name: 'Bob', age: neo4j.int(30) },
{ database: 'neo4j' }
)
console.log(s.counters.updates().nodesCreated) // ✅ 必须调用.updates()database// ❌ 存在注入风险 + 禁用查询计划缓存
await driver.executeQuery(`MATCH (p:Person {name: '${name}'}) RETURN p`)
// ✅ 使用参数化查询
await driver.executeQuery('MATCH (p:Person {name: $name}) RETURN p', { name })executeReadexecuteWriteexecuteReadexecuteWriteconst session = driver.session({ database: 'neo4j' })
try {
const names = await session.executeRead(async tx => {
const result = await tx.run(
'MATCH (p:Person) WHERE p.name STARTS WITH $prefix RETURN p.name AS name',
{ prefix: 'Al' }
)
// ✅ collect() while tx is open; return plain data
return (await result.collect()).map(r => r.get('name'))
})
await session.executeWrite(async tx => {
await tx.run('MERGE (p:Person {name: $name})', { name: 'Carol' })
})
} finally {
await session.close() // always in finally
}await tx.run()// ❌ returns stream; tx closes; records = []
return await tx.run('MATCH (p:Person) RETURN p.name AS name')
// ✅ collect fully inside callback
const result = await tx.run('MATCH (p:Person) RETURN p.name AS name')
return (await result.collect()).map(r => r.get('name'))
// ✅ or stream with for-await
for await (const record of result) { names.push(record.get('name')) }// ❌ fetch() called on every retry
await session.executeWrite(async tx => {
await fetch('https://api.example.com/notify')
await tx.run('CREATE ...')
})
// ✅ side effects after confirmed commit
await session.executeWrite(async tx => { await tx.run('MERGE ...') })
await fetch('https://api.example.com/notify')const session = driver.session({ database: 'neo4j' })
try {
const names = await session.executeRead(async tx => {
const result = await tx.run(
'MATCH (p:Person) WHERE p.name STARTS WITH $prefix RETURN p.name AS name',
{ prefix: 'Al' }
)
// ✅ 在事务开启时收集数据;返回普通数据
return (await result.collect()).map(r => r.get('name'))
})
await session.executeWrite(async tx => {
await tx.run('MERGE (p:Person {name: $name})', { name: 'Carol' })
})
} finally {
await session.close() // 务必放在finally块中
}await tx.run()// ❌ 返回流;事务关闭;records = []
return await tx.run('MATCH (p:Person) RETURN p.name AS name')
// ✅ 在回调内部完整收集数据
const result = await tx.run('MATCH (p:Person) RETURN p.name AS name')
return (await result.collect()).map(r => r.get('name'))
// ✅ 或使用for-await流式处理
for await (const record of result) { names.push(record.get('name')) }// ❌ 每次重试都会调用fetch()
await session.executeWrite(async tx => {
await fetch('https://api.example.com/notify')
await tx.run('CREATE ...')
})
// ✅ 在事务提交确认后执行副作用操作
await session.executeWrite(async tx => { await tx.run('MERGE ...') })
await fetch('https://api.example.com/notify')session.runsession.runLOAD CSVCALL IN TRANSACTIONSconst session = driver.session({ database: 'neo4j' })
try {
const result = await session.run('CREATE (p:Person {name: $name}) RETURN p', { name: 'Alice' })
console.log(result.summary.counters.updates().nodesCreated)
} finally {
await session.close()
}LOAD CSVCALL IN TRANSACTIONSconst session = driver.session({ database: 'neo4j' })
try {
const result = await session.run('CREATE (p:Person {name: $name}) RETURN p', { name: 'Alice' })
console.log(result.summary.counters.updates().nodesCreated)
} finally {
await session.close()
}finallyfinally// ❌ session leaks if executeRead rejects
session.executeRead(async tx => { ... })
.then(result => doSomething(result))
.then(() => session.close())
// ✅ guaranteed close
try {
const result = await session.executeRead(async tx => { ... })
doSomething(result)
} finally {
await session.close()
}
// ✅ promise-chain equivalent
session.executeRead(async tx => { ... })
.then(doSomething)
.catch(handleError)
.finally(() => session.close())// ❌ 如果executeRead抛出异常,会话会泄漏
session.executeRead(async tx => { ... })
.then(result => doSomething(result))
.then(() => session.close())
// ✅ 保证会话关闭
try {
const result = await session.executeRead(async tx => { ... })
doSomething(result)
} finally {
await session.close()
}
// ✅ Promise链等价写法
session.executeRead(async tx => { ... })
.then(doSomething)
.catch(handleError)
.finally(() => session.close())NumberInteger// Mode 1 (default): Integer class — safe for all values, requires conversion
const driver1 = neo4j.driver(URI, auth)
// record.get('count') → Integer { low: 42, high: 0 }
// Mode 2: native JS number — only safe within Number.MAX_SAFE_INTEGER
const driver2 = neo4j.driver(URI, auth, { disableLosslessIntegers: true })
// record.get('count') → 42
// Mode 3: BigInt — precise but breaks JSON.stringify
const driver3 = neo4j.driver(URI, auth, { useBigInt: true })
// record.get('count') → 42nconst count = record.get('count') // Integer { low: 42, high: 0 }
neo4j.isInt(count) // true
neo4j.integer.inSafeRange(count) // check before toNumber()
count.toNumber() // 42 (only safe within MAX_SAFE_INTEGER)
count.toString() // '42' (always safe)
count.toBigInt() // 42n
// Send integer parameter — plain JS number sends as FLOAT
await driver.executeQuery('CREATE (p:Person {age: $age})', { age: neo4j.int(30) })// ❌ Integer → {"low":42,"high":0}
JSON.stringify({ age: record.get('age') })
// ❌ BigInt → TypeError: Do not know how to serialize a BigInt
// ✅ convert first
JSON.stringify({ age: record.get('age').toNumber() })
// ✅ or use disableLosslessIntegers: true
// ❌ temporal types → {} silently
JSON.stringify({ dt: record.get('created') })
// ✅
JSON.stringify({ dt: record.get('created').toString() })NumberInteger// 模式1(默认):Integer类 — 支持所有数值,但需要转换
const driver1 = neo4j.driver(URI, auth)
// record.get('count') → Integer { low: 42, high: 0 }
// 模式2:原生JS数字 — 仅在Number.MAX_SAFE_INTEGER范围内安全
const driver2 = neo4j.driver(URI, auth, { disableLosslessIntegers: true })
// record.get('count') → 42
// 模式3:BigInt — 精度准确,但会导致JSON.stringify失败
const driver3 = neo4j.driver(URI, auth, { useBigInt: true })
// record.get('count') → 42nconst count = record.get('count') // Integer { low: 42, high: 0 }
neo4j.isInt(count) // true
neo4j.integer.inSafeRange(count) // 在调用toNumber()前检查是否安全
count.toNumber() // 42 (仅在MAX_SAFE_INTEGER范围内安全)
count.toString() // '42' (始终安全)
count.toBigInt() // 42n
// 传递整数参数 — 普通JS数字会作为FLOAT发送
await driver.executeQuery('CREATE (p:Person {age: $age})', { age: neo4j.int(30) })// ❌ Integer → {"low":42,"high":0}
JSON.stringify({ age: record.get('age') })
// ❌ BigInt → TypeError: Do not know how to serialize a BigInt
// ✅ 先转换
JSON.stringify({ age: record.get('age').toNumber() })
// ✅ 或使用disableLosslessIntegers: true
// ❌ 时间类型 → 静默转为{}
JSON.stringify({ dt: record.get('created') })
// ✅
JSON.stringify({ dt: record.get('created').toString() }).get()const record = records[0]
record.get('name') // ✅ by key
record.get(0) // ✅ by index
record.keys // ['name', 'age']
record.has('name') // true
record.name // ❌ undefined
record['name'] // ❌ undefinedrecord.toObject().get()const record = records[0]
record.get('name') // ✅ 通过键名访问
record.get(0) // ✅ 通过索引访问
record.keys // ['name', 'age']
record.has('name') // true
record.name // ❌ undefined
record['name'] // ❌ undefinedrecord.toObject()import { Neo4jError, SERVICE_UNAVAILABLE, SESSION_EXPIRED } from 'neo4j-driver'
try {
await driver.executeQuery('...', {}, { database: 'neo4j' })
} catch (err) {
if (err instanceof Neo4jError) {
if (err.code === 'Neo.ClientError.Schema.ConstraintValidationFailed') { /* unique constraint */ }
if (err.code === SERVICE_UNAVAILABLE) { /* unreachable */ }
if (err.code === SESSION_EXPIRED) { /* open a new session */ }
if (err.retriable) { /* transient — executeQuery already retried to exhaustion */ }
}
}executeQueryexecuteRead/Writesession.runimport { Neo4jError, SERVICE_UNAVAILABLE, SESSION_EXPIRED } from 'neo4j-driver'
try {
await driver.executeQuery('...', {}, { database: 'neo4j' })
} catch (err) {
if (err instanceof Neo4jError) {
if (err.code === 'Neo.ClientError.Schema.ConstraintValidationFailed') { /* 唯一约束错误 */ }
if (err.code === SERVICE_UNAVAILABLE) { /* 无法连接 */ }
if (err.code === SESSION_EXPIRED) { /* 开启新会话 */ }
if (err.retriable) { /* 临时错误 — executeQuery已自动重试至上限 */ }
}
}executeQueryexecuteRead/Writesession.runimport neo4j, { Driver, Session, ManagedTransaction, Record, Node, Integer } from 'neo4j-driver'
const driver: Driver = neo4j.driver(URI, neo4j.auth.basic(USER, PASSWORD))
const session: Session = driver.session({ database: 'neo4j' })
const names: string[] = await session.executeRead(
async (tx: ManagedTransaction): Promise<string[]> => {
const result = await tx.run('MATCH (p:Person) RETURN p.name AS name')
return (await result.collect()).map((r: Record) => r.get('name') as string)
}
)
// Typed node — Integer generic changes with disableLosslessIntegers
const node = record.get('p') as Node<Integer>
const age: number = node.properties.age.toNumber()
// With disableLosslessIntegers: true → Node<number>; age is already numberimport neo4j, { Driver, Session, ManagedTransaction, Record, Node, Integer } from 'neo4j-driver'
const driver: Driver = neo4j.driver(URI, neo4j.auth.basic(USER, PASSWORD))
const session: Session = driver.session({ database: 'neo4j' })
const names: string[] = await session.executeRead(
async (tx: ManagedTransaction): Promise<string[]> => {
const result = await tx.run('MATCH (p:Person) RETURN p.name AS name')
return (await result.collect()).map((r: Record) => r.get('name') as string)
}
)
// 带类型的节点 — Integer泛型会随disableLosslessIntegers设置变化
const node = record.get('p') as Node<Integer>
const age: number = node.properties.age.toNumber()
// 当disableLosslessIntegers: true时 → Node<number>; age已经是number类型// ❌ one transaction per item
for (const p of people) { await driver.executeQuery('CREATE ...', p) }
// ✅ single transaction
await driver.executeQuery(
`UNWIND $people AS person
MERGE (p:Person {name: person.name})
SET p.age = person.age`,
{ people }, // array of plain objects; numeric fields must be JS numbers, not neo4j.int()
{ database: 'neo4j' }
)// ❌ 每个条目对应一个事务
for (const p of people) { await driver.executeQuery('CREATE ...', p) }
// ✅ 单个事务完成
await driver.executeQuery(
`UNWIND $people AS person
MERGE (p:Person {name: person.name})
SET p.age = person.age`,
{ people }, // 普通对象数组;数字字段必须是JS数字,而非neo4j.int()
{ database: 'neo4j' }
)| Mistake | Fix |
|---|---|
| Template literal Cypher | Use |
| |
| |
| |
| |
Omit | Always |
Return | Return |
| |
| Side effects inside tx callback | Move outside — callback may retry |
| New driver per request | Create once at startup |
| Use |
| Integer in UNWIND array | Convert to plain JS |
| Use 5–10 per function instance |
| 错误做法 | 修复方案 |
|---|---|
| 使用模板字符串拼接Cypher | 使用 |
| 使用 |
对Integer直接使用 | 先调用 |
对时间类型直接使用 | 先调用 |
| 使用 |
省略 | 始终指定 |
从事务回调返回 | 返回 |
| 使用 |
| 在事务回调内部执行副作用操作 | 移到外部——回调可能会重试 |
| 为每个请求创建新驱动实例 | 在启动时创建一次,全局共享 |
在浏览器中使用 | 使用 |
| 在UNWIND数组中使用Integer | 先转换为普通JS |
在无服务器环境中设置 | 每个函数实例设置5–10 |
toNative()rxSession.run()toNative()rxSession.run()databaseawait driver.verifyConnectivity()session.close()finally.get().toNumber().toString().toString()summary.counters.updates()$paramneo4j.int()executeReadexecuteWriteneo4j+s://databaseawait driver.verifyConnectivity()finallysession.close().get().toNumber().toString().toString()summary.counters.updates()$paramneo4j.int()executeReadexecuteWriteneo4j+s://