Query the database
Once you have connected to the database, you can run Cypher queries through the method Driver.executeQuery()
.
Due to the usage of |
Write to the database
To create two nodes representing persons named Alice
and David
, and a relationship KNOWS
between them, use the Cypher clause 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 | The Cypher query. |
2 | An object of query parameters. |
3 | The database to run the query on |
Read from the database
To retrieve information from the database, use the Cypher clause MATCH
:
Person
nodes who like other Person
slet { 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 contains the actual result as a list of Record objects. |
2 | summary contains the summary of execution returned by the server. |
Update the database
Alice
to add an age
propertylet { _, 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())
To create a new relationship, linking it to two already existing node, use a combination of the Cypher clauses MATCH
and CREATE
:
:KNOWS
between Alice
and Bob
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 | Retrieve the person node named Alice and bind it to a variable alice |
2 | Retrieve the person node named Bob and bind it to a variable bob |
3 | Create a new :KNOWS relationship outgoing from the node bound to alice and attach to it the Person node named Bob |
Delete from the database
To remove a node and any relationship attached to it, use the Cypher clause DETACH DELETE
:
Alice
node// 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())
Query parameters
Do not hardcode or concatenate parameters directly into queries. Instead, always use placeholders and provide dynamic data as Cypher parameters. This is for:
-
performance benefits: Neo4j compiles and caches queries, but can only do so if the query structure is unchanged;
-
security reasons: protecting against Cypher injection.
There can be circumstances where your query structure prevents the usage of parameters in all its parts. For those rare use cases, see Dynamic values in property keys, relationship types, and labels. |
Error handling
A query run may fail for a number of reasons.
When using Driver.executeQuery()
, the driver automatically retries to run a failed query if the failure is deemed to be transient (for example due to temporary server unavailability).
An error will be raised if the operation keeps failing after the configured maximum retry time.
All errors coming from the server are subclasses of Neo4jError
.
You can use an exception’s code to stably identify a specific error; error messages are instead not stable markers, and should not be relied upon.
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"
^
*/
Error objects also expose errors as GQL-status objects. The main difference between Neo4j error codes and GQL error codes is that the GQL ones are more granular: a single Neo4j error code might be broken in several, more specific GQL error codes.
The actual cause that triggered an error is sometimes found in the optional GQL-status object .cause
, which is itself a Neo4jError
.
You might need to recursively traverse the cause chain before reaching the root cause of the error you caught.
In the example below, the error’s GQL status code is 42001
, but the actual source of the error has status code 42I06
.
Neo4jError
with GQL-related methodstry {
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 status codes are particularly helpful when you want your application to behave differently depending on the exact error that was raised by the server.
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)
}
}
The GQL status code |
Transient server errors can be retried without need to alter the original request.
You can discover whether an error is transient via the method |
Query configuration
You can supply a QueryConfig
object as third (optional) parameter to alter the default behavior of .executeQuery()
.
Database selection
Always specify the database explicitly with the database
parameter, even on single-database instances.
This allows the driver to work more efficiently, as it saves a network round-trip to the server to resolve the home database.
If no database is given, the user’s home database is used.
await driver.executeQuery(
'MATCH (p:Person) RETURN p.name',
{},
{
database: '<database-name>'
}
)
Specifying the database through the configuration parameter is preferred over the USE Cypher clause.
If the server runs on a cluster, queries with USE require server-side routing to be enabled.
Queries can also take longer to execute as they may not reach the right cluster member at the first attempt, and need to be routed to one containing the requested database.
|
Request routing
In a cluster environment, all queries are directed to the leader node by default.
To improve performance on read queries, you can use the configuration routing: 'READ'
to route a query to the read nodes.
await driver.executeQuery(
'MATCH (p:Person) RETURN p.name',
{},
{
routing: 'READ', // short for neo4j.routing.READ
database: '<database-name>'
}
)
Although executing a write query in read mode results in a runtime error, you should not rely on this for access control. The difference between the two modes is that read transactions will be routed to any node of a cluster, whereas write ones are directed to primaries. There is no security guarantee that a write query submitted in read mode will be rejected. |
Run queries as a different user
You can execute a query through a different user with the configuration parameter auth
.
Switching user at the query level is cheaper than creating a new Driver
object.
The query is then run within the security context of the given user (i.e., home database, permissions, etc.).
await driver.executeQuery(
'MATCH (p:Person) RETURN p.name',
{},
{
auth: neo4j.auth.basic('<username>', '<password>'),
database: '<database-name>'
}
)
The parameter impersonatedUser
provides a similar functionality.
The difference is that you don’t need to know a user’s password to impersonate them, but the user under which the Driver
was created needs to have the appropriate permissions.
await driver.executeQuery(
'MATCH (p:Person) RETURN p.name',
{},
{
impersonatedUser: '<username>',
database: '<database-name>'
}
)
A full example
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()
})();
Glossary
- LTS
-
A Long Term Support release is one guaranteed to be supported for a number of years. Neo4j 4.4 and 5.26 are LTS versions.
- Aura
-
Aura is Neo4j’s fully managed cloud service. It comes with both free and paid plans.
- Cypher
-
Cypher is Neo4j’s graph query language that lets you retrieve data from the database. It is like SQL, but for graphs.
- APOC
-
Awesome Procedures On Cypher (APOC) is a library of (many) functions that can not be easily expressed in Cypher itself.
- Bolt
-
Bolt is the protocol used for interaction between Neo4j instances and drivers. It listens on port 7687 by default.
- ACID
-
Atomicity, Consistency, Isolation, Durability (ACID) are properties guaranteeing that database transactions are processed reliably. An ACID-compliant DBMS ensures that the data in the database remains accurate and consistent despite failures.
- eventual consistency
-
A database is eventually consistent if it provides the guarantee that all cluster members will, at some point in time, store the latest version of the data.
- causal consistency
-
A database is causally consistent if read and write queries are seen by every member of the cluster in the same order. This is stronger than eventual consistency.
- NULL
-
The null marker is not a type but a placeholder for absence of value. For more information, see Cypher → Working with
null
. - transaction
-
A transaction is a unit of work that is either committed in its entirety or rolled back on failure. An example is a bank transfer: it involves multiple steps, but they must all succeed or be reverted, to avoid money being subtracted from one account but not added to the other.
- backpressure
-
Backpressure is a force opposing the flow of data. It ensures that the client is not being overwhelmed by data faster than it can handle.
- bookmark
-
A bookmark is a token representing some state of the database. By passing one or multiple bookmarks along with a query, the server will make sure that the query does not get executed before the represented state(s) have been established.
- transaction function
-
A transaction function is a callback executed by an
executeRead
orexecuteWrite
call. The driver automatically re-executes the callback in case of server failure. - Driver
-
A
Driver
object holds the details required to establish connections with a Neo4j database.