Query the database

Once you have connected to the database, you can execute Cypher queries through the method Driver.executableQuery().

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:

Create two nodes and a relationship
// import java.util.Map;
// import java.util.concurrent.TimeUnit;
// import org.neo4j.driver.QueryConfig;

var result = driver.executableQuery("""
    CREATE (a:Person {name: $name})  (1)
    CREATE (b:Person {name: $friendName})
    CREATE (a)-[:KNOWS]->(b)
    """)
    .withParameters(Map.of("name", "Alice", "friendName", "David"))  (2)
    .withConfig(QueryConfig.builder().withDatabase("<database-name>").build())  (3)
    .execute();
var summary = result.summary();  (4)
System.out.printf("Created %d records in %d ms.%n",
    summary.counters().nodesCreated(),
    summary.resultAvailableAfter(TimeUnit.MILLISECONDS));
1 The Cypher query
2 A map of query parameters
3 The database to run the query on
4 The summary of execution returned by the server

Read from the database

To retrieve information from the database, use the Cypher clause MATCH:

Retrieve all Person nodes who like other Person s
// import java.util.concurrent.TimeUnit;
// import org.neo4j.driver.QueryConfig;

var result = driver.executableQuery("""
    MATCH (p:Person)-[:KNOWS]->(:Person)
    RETURN p.name AS name
    """)
    .withConfig(QueryConfig.builder().withDatabase("<database-name>").build())
    .execute();

// Loop through results and do something with them
var records = result.records();  (1)
records.forEach(r -> {
    System.out.println(r);  // or r.get("name").asString()
});

// Summary information
var summary = result.summary();  (2)
System.out.printf("The query %s returned %d records in %d ms.%n",
    summary.query(), records.size(),
    summary.resultAvailableAfter(TimeUnit.MILLISECONDS));
1 records contains the result as a list of Record objects
2 summary contains the summary of execution returned by the server

Properties inside a Record object are embedded within Value objects. To extract and cast them to the corresponding Java types, use .as<type>() (eg. .asString(), asInt(), etc). For example, if the name property coming from the database is a string, record.get("name").asString() will yield the property value as a String object. For more information, see Data types and mapping to Cypher types.

Another way of extracting values from returned records is by mapping them to objects.

Update the database

To update a node’s information in the database, use the Cypher clauses MATCH and SET:

Update node Alice to add an age property
// import java.util.Map;
// import org.neo4j.driver.QueryConfig;

var result = driver.executableQuery("""
    MATCH (p:Person {name: $name})
    SET p.age = $age
    """)
    .withConfig(QueryConfig.builder().withDatabase("<database-name>").build())
    .withParameters(Map.of("name", "Alice", "age", 42))
    .execute();
var summary = result.summary();
System.out.println("Query updated the database?");
System.out.println(summary.counters().containsUpdates());

To create a new relationship, linking it to two already existing node, use a combination of the Cypher clauses MATCH and CREATE:

Create a relationship :KNOWS between Alice and Bob
// import java.util.Map;
// import org.neo4j.driver.QueryConfig;

var result = driver.executableQuery("""
    MATCH (alice:Person {name: $name})  (1)
    MATCH (bob:Person {name: $friend})  (2)
    CREATE (alice)-[:KNOWS]->(bob)  (3)
    """)
    .withConfig(QueryConfig.builder().withDatabase("<database-name>").build())
    .withParameters(Map.of("name", "Alice", "friend", "Bob"))
    .execute();
var summary = result.summary();
System.out.println("Query updated the database?");
System.out.println(summary.counters().containsUpdates());
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:

Remove the Alice node and all its relationships
// import java.util.Map;
// import org.neo4j.driver.QueryConfig;

// This does not delete _only_ p, but also all its relationships!
var result = driver.executableQuery("""
    MATCH (p:Person {name: $name})
    DETACH DELETE p
    """)
    .withConfig(QueryConfig.builder().withDatabase("<database-name>").build())
    .withParameters(Map.of("name", "Alice"))
    .execute();
var summary = result.summary();
System.out.println("Query updated the database?");
System.out.println(summary.counters().containsUpdates());

Query parameters

Do not hardcode or concatenate parameters directly into queries. Instead, always use placeholders and provide dynamic data as Cypher parameters, as shown in the previous examples. This is for:

  1. performance benefits: Neo4j compiles and caches queries, but can only do so if the query structure is unchanged;

  2. security reasons: see protecting against Cypher injection.

You may provide query parameters as a map through the .withParameters() method.

var result = driver.executableQuery("MATCH (p:Person {name: $name}) RETURN p")
    .withParameters(Map.of("name", "Alice"))
    .withConfig(QueryConfig.builder().withDatabase("<database-name>").build())
    .execute();
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, with different exceptions being raised.

When using Driver.executableQuery(), 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 exception will be raised if the operation keeps failing after the configured maximum retry time.

All exceptions coming from the server are subclasses of Neo4jException. You can use an exception’s code (retrievable with .code()) to stably identify a specific error; error messages are instead not stable markers, and should not be relied upon.

Basic error handling
// import org.neo4j.driver.exceptions.Neo4jException;

try {
    var result = driver.executableQuery("MATCH (p:Person) RETURN ")
    .withConfig(QueryConfig.builder().withDatabase("<database-name>").build())
    .execute();
} catch (Neo4jException e) {
    System.out.printf("Neo4j error code: %s\n", e.code());
    System.out.printf("Exception message: %s\n", e.getMessage());
}
/*
Neo4j error code: Neo.ClientError.Statement.SyntaxError
Exception message: Invalid input '': expected an expression, '*', 'ALL' or 'DISTINCT' (line 1, column 24 (offset: 23))
"MATCH (p:Person) RETURN"
                        ^
*/

Exception 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 exception is sometimes found in the optional GQL-status object retrievable with .gqlCause(), which is itself a Neo4jException. You might need to recursively traverse the cause chain before reaching the root cause of the exception you caught. In the example below, the exception’s GQL status code is 42001, but the actual source of the error has status code 42I06.

Usage of Neo4jException with GQL-related methods
// import org.neo4j.driver.exceptions.Neo4jException;

try {
    var result = driver.executableQuery("MATCH (p:Person) RETURN ")
    .withConfig(QueryConfig.builder().withDatabase("<database-name>").build())
    .execute();
} catch (Neo4jException e) {
    System.out.printf("Exception GQL status code: %s\n", e.gqlStatus());
    System.out.printf("Exception GQL status description: %s\n", e.statusDescription());
    System.out.printf("Exception GQL classification: %s\n", e.rawClassification());
    System.out.printf("Exception GQL cause: %s\n", e.gqlCause());
    System.out.printf("Exception GQL diagnostic record: %s\n", e.diagnosticRecord());
}
/*
Exception GQL status code: 42001
Exception GQL status description: error: syntax error or access rule violation - invalid syntax
Exception GQL classification: Optional[CLIENT_ERROR]
Exception GQL cause: Optional[org.neo4j.driver.exceptions.Neo4jException: 42I06: Invalid input '', expected: an expression, '*', 'ALL' or 'DISTINCT'.]
Exception GQL diagnostic record: {_classification="CLIENT_ERROR", OPERATION_CODE="0", OPERATION="", CURRENT_SCHEMA="/", _position={column: 24, offset: 23, line: 1}}
*/

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.

Distinguishing between different error codes
// import org.neo4j.driver.exceptions.Neo4jException;

try {
    var result = driver.executableQuery("CREATE (p:Person {name: $name}) RETURN ")
    .withParameters(Map.of("name", "Frida"))
    .withConfig(QueryConfig.builder().withDatabase("<database-name>").build())
    .execute();
} catch (Neo4jException e) {
    if (e.containsGqlStatus("42001")) {
        // Neo.ClientError.Statement.SyntaxError
        // special handling of syntax error in query
        System.out.println(e.findByGqlStatus("42001").get().getMessage());
    } else if (e.containsGqlStatus("42NFF")) {
        // Neo.ClientError.Security.Forbidden
        // special handling of user not having CREATE permissions
        System.out.println(e.findByGqlStatus("42NFF").get().getMessage());
    } else {
        // handling of all other exceptions
        System.out.println(e.getMessage());
    }
}

The GQL status code 50N42 is returned when an error does not have a GQL-status object. This can happen if the driver is connected to an older Neo4j server. Don’t rely on this status code, as future Neo4j server versions might change it with a more appropriate one.

Query configuration

You can supply further configuration parameters to alter the default behavior of .executableQuery(). You do so through the method .withConfig(), which takes a QueryConfig object.

Database selection

Always specify the database explicitly with the .withDatabase("<dbName>") method, 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.

// import org.neo4j.driver.QueryConfig;

var result = driver.executableQuery("MATCH (p:Person) RETURN p.name")
    .withConfig(QueryConfig.builder().withDatabase("<database-name>").build())
    .execute();
Specifying the database through the configuration method 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 method .withRouting(RoutingControl.READ) to route a query to the read nodes.

// import org.neo4j.driver.QueryConfig;
// import org.neo4j.driver.RoutingControl;

var result = driver.executableQuery("MATCH (p:Person) RETURN p.name")
    .withConfig(QueryConfig.builder()
        .withDatabase("<database-name>")
        .withRouting(RoutingControl.READ)
        .build())
    .execute();

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 method .withAuthToken(). 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.).

// import org.neo4j.driver.AuthTokens;
// import org.neo4j.driver.QueryConfig;

var authToken = AuthTokens.basic("<username>", "<password>");
var result = driver.executableQuery("MATCH (p:Person) RETURN p.name")
    .withAuthToken(authToken)
    .withConfig(QueryConfig.builder()
        .withDatabase("<database-name>")
        .build())
    .execute();

The method .withImpersonatedUser() 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.

// import org.neo4j.driver.QueryConfig;

var result = driver.executableQuery("MATCH (p:Person) RETURN p.name")
    .withConfig(QueryConfig.builder()
        .withDatabase("<database-name>")
        .withImpersonatedUser("<username>")
        .build())
    .execute();

A full example

package demo;

import java.util.Map;
import java.util.List;
import java.util.concurrent.TimeUnit;

import org.neo4j.driver.AuthTokens;
import org.neo4j.driver.GraphDatabase;
import org.neo4j.driver.Record;
import org.neo4j.driver.QueryConfig;
import org.neo4j.driver.RoutingControl;
import org.neo4j.driver.exceptions.Neo4jException;

public class App {

    private static final String dbUri = "<database-uri>";
    private static final String dbUser = "<username>";
    private static final String dbPassword = "<password>";

    public static void main(String... args) {

        try (var driver = GraphDatabase.driver(dbUri, AuthTokens.basic(dbUser, dbPassword))) {

            List<Map> people = List.of(
                Map.of("name", "Alice", "age", 42, "friends", List.of("Bob", "Peter", "Anna")),
                Map.of("name", "Bob", "age", 19),
                Map.of("name", "Peter", "age", 50),
                Map.of("name", "Anna", "age", 30)
            );

            try {

                //Create some nodes
                people.forEach(person -> {
                    var result = driver.executableQuery("CREATE (p:Person {name: $person.name, age: $person.age})")
                        .withConfig(QueryConfig.builder().withDatabase("<database-name>").build())
                        .withParameters(Map.of("person", person))
                        .execute();
                });

                // Create some relationships
                people.forEach(person -> {
                    if(person.containsKey("friends")) {
                        var result = driver.executableQuery("""
                            MATCH (p:Person {name: $person.name})
                            UNWIND $person.friends AS friend_name
                            MATCH (friend:Person {name: friend_name})
                            CREATE (p)-[:KNOWS]->(friend)
                             """)
                            .withConfig(QueryConfig.builder().withDatabase("<database-name>").build())
                            .withParameters(Map.of("person", person))
                            .execute();
                    }
                });

                // Retrieve Alice's friends who are under 40
                var result = driver.executableQuery("""
                    MATCH (p:Person {name: $name})-[:KNOWS]-(friend:Person)
                    WHERE friend.age < $age
                    RETURN friend
                     """)
                    .withConfig(QueryConfig.builder()
                        .withDatabase("<database-name>")
                        .withRouting(RoutingControl.READ)
                        .build())
                    .withParameters(Map.of("name", "Alice", "age", 40))
                    .execute();

                // Loop through results and do something with them
                result.records().forEach(r -> {
                    System.out.println(r);
                });

                // Summary information
                System.out.printf("The query %s returned %d records in %d ms.%n",
                    result.summary().query(), result.records().size(),
                    result.summary().resultAvailableAfter(TimeUnit.MILLISECONDS));

            } catch (Neo4jException e) {
                if (e.gqlStatus().equals("42NFF")) {
                    System.out.println("There was a permission issue. Make sure you have correct permissions and try again.");
                } else {
                    System.out.println(e.getMessage());
                    System.exit(1);
                }
            }
        }
    }
}

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 or executeWrite 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.