Cypher workflow
Overview
The Neo4j Drivers expose a Cypher Channel over which database work can be carried out (see the Cypher Manual for more information on the Cypher Query Language).
Work itself is organized into sessions, transactions and queries, as follows:
Sessions are always bound to a single transaction context, which is typically an individual database.
Using the bookmark mechanism, sessions also provide a guarantee of correct transaction sequencing, even when transactions occur over multiple cluster members. This effect is known as causal chaining.
Sessions
Sessions are lightweight containers for causally chained sequences of transactions (see Operations Manual → Causal consistency). They essentially provide context for storing transaction sequencing information in the form of bookmarks.
When a transaction begins, the session in which it is contained acquires a connection from the driver connection pool. On commit (or rollback) of the transaction, the session releases that connection again. This means that it is only when a session is carrying out work that it occupies a connection resource. When idle, no such resource is in use.
Due to the sequencing guaranteed by a session, sessions may only host one transaction at a time. For parallel execution, multiple sessions should be used. In languages where thread safety is an issue, sessions should not be considered thread-safe.
Closing a session forces any open transaction to be rolled back, and its associated connection to consequently be released back into the pool.
Sessions are bound to a single transactional context, specified on construction. Neo4j exposes each database inside its own context, thereby prohibiting cross-database transactions (or sessions) by design. Similarly, sessions bound to different databases may not be causally chained by propagating bookmarks between them.
Individual language drivers provide several session classes, each oriented around a particular programming style. Each session class provides a similar set of functionality but offers client applications a choice based on how the application is structured and what frameworks are in use, if any.
The session classes are described in The session API. For more details, please see the API documentation for your language.
Transactions
Transactions are atomic units of work containing one or more Cypher Queries. Transactions may contain read or write work, and will generally be routed to an appropriate server for execution, where they will be carried out in their entirety. In case of a transaction failure, the transaction needs to be retried from the beginning. This is the responsibility of the transaction manager.
The Neo4j Drivers provide transaction management via the transaction function mechanism. This mechanism is exposed through methods on the Session object which accept a function object that can be played multiple times against different servers until it either succeeds or a timeout is reached. This approach is recommended for most client applications.
A convenient short-form alternative is the auto-commit transaction mechanism.
This provides a limited form of transaction management for single-query transactions, as a trade-off for a slightly smaller code overhead.
This form of transaction is useful for quick scripts and environments where high availability guarantees are not required.
It is also the required form of transaction for running PERIODIC COMMIT
queries,
which are the only type of Cypher Query to manage their own transactions.
A lower-level unmanaged transaction API is also available for advanced use cases. This is useful when an alternative transaction management layer is applied by the client, in which error handling and retries need to be managed in a custom way. To learn more about how to use unmanaged transactions, see API documentation for the relevant language.
Queries and results
Queries consist of a request to the server to execute a Cypher statement, followed by a response back to the client with the result. Results are transmitted as a stream of records, along with header and footer metadata, and can be incrementally consumed by a client application. With reactive capabilities, the semantics of the record stream can be enhanced by allowing a Cypher result to be paused or cancelled part-way through.
To execute a Cypher Query, the query text is required along with an optional set of named parameters. The text can contain parameter placeholders that are substituted with the corresponding values at runtime. While it is possible to run non-parameterized Cypher Queries, good programming practice is to use parameters in Cypher Queries whenever possible. This allows for the caching of queries within the Cypher Engine, which is beneficial for performance. Parameter values should adhere to /docs/cypher-manual/4.0/syntax/values.
A result summary is also generally available.
This contains additional information relating to the query execution and the result content.
For an EXPLAIN
or PROFILE
query, this is where the query plan is returned.
See Cypher Manual → Profiling a query for more information on these queries.
Causal chaining and bookmarks
When working with a Causal Cluster, transactions can be chained, via a session, to ensure causal consistency. This means that for any two transactions, it is guaranteed that the second transaction will begin only after the first has been successfully committed. This is true even if the transactions are carried out on different physical cluster members. For more information on Causal Clusters, please refer to Operations Manual → Clustering.
Internally, causal chaining is carried out by passing bookmarks between transactions. Each bookmark records one or more points in transactional history for a particular database, and can be used to inform cluster members to carry out units of work in a particular sequence. On receipt of a bookmark, the server will block until it has caught up with the relevant transactional point in time.
An initial bookmark is sent from client to server on beginning a new transaction, and a final bookmark is returned on successful completion. Note that this applies to both read and write transactions.
Bookmark propagation is carried out automatically within sessions and does not require any explicit signal or setting from the application. To opt out of this mechanism, for unrelated units of work, applications can use multiple sessions. This avoids the small latency overhead of the causal chain.
Bookmarks can be passed between sessions by extracting the last bookmark from a session and passing this into the construction of another. Multiple bookmarks can also be combined if a transaction has more than one logical predecessor. Note that it is only when chaining across sessions that an application will need to work with bookmarks directly.
// Create a company node
private IResult AddCompany(ITransaction tx, string name)
{
return tx.Run("CREATE (a:Company {name: $name})", new {name});
}
// Create a person node
private IResult AddPerson(ITransaction tx, string name)
{
return tx.Run("CREATE (a:Person {name: $name})", new {name});
}
// Create an employment relationship to a pre-existing company node.
// This relies on the person first having been created.
private IResult Employ(ITransaction tx, string personName, string companyName)
{
return tx.Run(@"MATCH (person:Person {name: $personName})
MATCH (company:Company {name: $companyName})
CREATE (person)-[:WORKS_FOR]->(company)", new {personName, companyName});
}
// Create a friendship between two people.
private IResult MakeFriends(ITransaction tx, string name1, string name2)
{
return tx.Run(@"MATCH (a:Person {name: $name1})
MATCH (b:Person {name: $name2})
MERGE (a)-[:KNOWS]->(b)", new {name1, name2});
}
// Match and display all friendships.
private int PrintFriendships(ITransaction tx)
{
var result = tx.Run("MATCH (a)-[:KNOWS]->(b) RETURN a.name, b.name");
var count = 0;
foreach (var record in result)
{
count++;
Console.WriteLine($"{record["a.name"]} knows {record["b.name"]}");
}
return count;
}
public void AddEmployAndMakeFriends()
{
// To collect the session bookmarks
var savedBookmarks = new List<Bookmark>();
// Create the first person and employment relationship.
using (var session1 = Driver.Session(o => o.WithDefaultAccessMode(AccessMode.Write)))
{
session1.WriteTransaction(tx => AddCompany(tx, "Wayne Enterprises"));
session1.WriteTransaction(tx => AddPerson(tx, "Alice"));
session1.WriteTransaction(tx => Employ(tx, "Alice", "Wayne Enterprises"));
savedBookmarks.Add(session1.LastBookmark);
}
// Create the second person and employment relationship.
using (var session2 = Driver.Session(o => o.WithDefaultAccessMode(AccessMode.Write)))
{
session2.WriteTransaction(tx => AddCompany(tx, "LexCorp"));
session2.WriteTransaction(tx => AddPerson(tx, "Bob"));
session2.WriteTransaction(tx => Employ(tx, "Bob", "LexCorp"));
savedBookmarks.Add(session2.LastBookmark);
}
// Create a friendship between the two people created above.
using (var session3 = Driver.Session(o =>
o.WithDefaultAccessMode(AccessMode.Write).WithBookmarks(savedBookmarks.ToArray())))
{
session3.WriteTransaction(tx => MakeFriends(tx, "Alice", "Bob"));
session3.ReadTransaction(PrintFriendships);
}
}
func addCompanyTxFunc(name string) neo4j.TransactionWork {
return func(tx neo4j.Transaction) (interface{}, error) {
return tx.Run("CREATE (a:Company {name: $name})", map[string]interface{}{"name": name})
}
}
func addPersonTxFunc(name string) neo4j.TransactionWork {
return func(tx neo4j.Transaction) (interface{}, error) {
return tx.Run("CREATE (a:Person {name: $name})", map[string]interface{}{"name": name})
}
}
func employTxFunc(person string, company string) neo4j.TransactionWork {
return func(tx neo4j.Transaction) (interface{}, error) {
return tx.Run(
"MATCH (person:Person {name: $personName}) "+
"MATCH (company:Company {name: $companyName}) "+
"CREATE (person)-[:WORKS_FOR]->(company)", map[string]interface{}{"personName": person, "companyName": company})
}
}
func makeFriendTxFunc(person1 string, person2 string) neo4j.TransactionWork {
return func(tx neo4j.Transaction) (interface{}, error) {
return tx.Run(
"MATCH (a:Person {name: $name1}) "+
"MATCH (b:Person {name: $name2}) "+
"MERGE (a)-[:KNOWS]->(b)", map[string]interface{}{"name1": person1, "name2": person2})
}
}
func printFriendsTxFunc() neo4j.TransactionWork {
return func(tx neo4j.Transaction) (interface{}, error) {
result, err := tx.Run("MATCH (a)-[:KNOWS]->(b) RETURN a.name, b.name", nil)
if err != nil {
return nil, err
}
for result.Next() {
fmt.Printf("%s knows %s\n", result.Record().Values[0], result.Record().Values[1])
}
return result.Consume()
}
}
func addAndEmploy(driver neo4j.Driver, person string, company string) (string, error) {
session := driver.NewSession(neo4j.SessionConfig{AccessMode: neo4j.AccessModeWrite})
defer session.Close()
if _, err := session.WriteTransaction(addCompanyTxFunc(company)); err != nil {
return "", err
}
if _, err := session.WriteTransaction(addPersonTxFunc(person)); err != nil {
return "", err
}
if _, err := session.WriteTransaction(employTxFunc(person, company)); err != nil {
return "", err
}
return session.LastBookmark(), nil
}
func makeFriend(driver neo4j.Driver, person1 string, person2 string, bookmarks ...string) (string, error) {
session := driver.NewSession(neo4j.SessionConfig{AccessMode: neo4j.AccessModeWrite, Bookmarks: bookmarks})
defer session.Close()
if _, err := session.WriteTransaction(makeFriendTxFunc(person1, person2)); err != nil {
return "", err
}
return session.LastBookmark(), nil
}
func addEmployAndMakeFriends(driver neo4j.Driver) error {
var bookmark1, bookmark2, bookmark3 string
var err error
if bookmark1, err = addAndEmploy(driver, "Alice", "Wayne Enterprises"); err != nil {
return err
}
if bookmark2, err = addAndEmploy(driver, "Bob", "LexCorp"); err != nil {
return err
}
if bookmark3, err = makeFriend(driver, "Bob", "Alice", bookmark1, bookmark2); err != nil {
return err
}
session := driver.NewSession(neo4j.SessionConfig{AccessMode: neo4j.AccessModeRead, Bookmarks: []string{bookmark1, bookmark2, bookmark3}})
defer session.Close()
if _, err = session.ReadTransaction(printFriendsTxFunc()); err != nil {
return err
}
return nil
}
import java.util.ArrayList;
import java.util.List;
import org.neo4j.driver.AccessMode;
import org.neo4j.driver.Record;
import org.neo4j.driver.Session;
import org.neo4j.driver.Result;
import org.neo4j.driver.Transaction;
import org.neo4j.driver.Bookmark;
import static org.neo4j.driver.Values.parameters;
import static org.neo4j.driver.SessionConfig.builder;
// Create a company node
private Result addCompany(final Transaction tx, final String name )
{
return tx.run( "CREATE (:Company {name: $name})", parameters( "name", name ) );
}
// Create a person node
private Result addPerson(final Transaction tx, final String name )
{
return tx.run( "CREATE (:Person {name: $name})", parameters( "name", name ) );
}
// Create an employment relationship to a pre-existing company node.
// This relies on the person first having been created.
private Result employ(final Transaction tx, final String person, final String company )
{
return tx.run( "MATCH (person:Person {name: $person_name}) " +
"MATCH (company:Company {name: $company_name}) " +
"CREATE (person)-[:WORKS_FOR]->(company)",
parameters( "person_name", person, "company_name", company ) );
}
// Create a friendship between two people.
private Result makeFriends(final Transaction tx, final String person1, final String person2 )
{
return tx.run( "MATCH (a:Person {name: $person_1}) " +
"MATCH (b:Person {name: $person_2}) " +
"MERGE (a)-[:KNOWS]->(b)",
parameters( "person_1", person1, "person_2", person2 ) );
}
// Match and display all friendships.
private Result printFriends(final Transaction tx )
{
Result result = tx.run( "MATCH (a)-[:KNOWS]->(b) RETURN a.name, b.name" );
while ( result.hasNext() )
{
Record record = result.next();
System.out.println( String.format( "%s knows %s", record.get( "a.name" ).asString(), record.get( "b.name" ).toString() ) );
}
return result;
}
public void addEmployAndMakeFriends()
{
// To collect the session bookmarks
List<Bookmark> savedBookmarks = new ArrayList<>();
// Create the first person and employment relationship.
try ( Session session1 = driver.session( builder().withDefaultAccessMode( AccessMode.WRITE ).build() ) )
{
session1.writeTransaction( tx -> addCompany( tx, "Wayne Enterprises" ) );
session1.writeTransaction( tx -> addPerson( tx, "Alice" ) );
session1.writeTransaction( tx -> employ( tx, "Alice", "Wayne Enterprises" ) );
savedBookmarks.add( session1.lastBookmark() );
}
// Create the second person and employment relationship.
try ( Session session2 = driver.session( builder().withDefaultAccessMode( AccessMode.WRITE ).build() ) )
{
session2.writeTransaction( tx -> addCompany( tx, "LexCorp" ) );
session2.writeTransaction( tx -> addPerson( tx, "Bob" ) );
session2.writeTransaction( tx -> employ( tx, "Bob", "LexCorp" ) );
savedBookmarks.add( session2.lastBookmark() );
}
// Create a friendship between the two people created above.
try ( Session session3 = driver.session( builder().withDefaultAccessMode( AccessMode.WRITE ).withBookmarks( savedBookmarks ).build() ) )
{
session3.writeTransaction( tx -> makeFriends( tx, "Alice", "Bob" ) );
session3.readTransaction( this::printFriends );
}
}
// Create a company node
function addCompany (tx, name) {
return tx.run('CREATE (a:Company {name: $name})', { name: name })
}
// Create a person node
function addPerson (tx, name) {
return tx.run('CREATE (a:Person {name: $name})', { name: name })
}
// Create an employment relationship to a pre-existing company node.
// This relies on the person first having been created.
function addEmployee (tx, personName, companyName) {
return tx.run(
'MATCH (person:Person {name: $personName}) ' +
'MATCH (company:Company {name: $companyName}) ' +
'CREATE (person)-[:WORKS_FOR]->(company)',
{ personName: personName, companyName: companyName }
)
}
// Create a friendship between two people.
function makeFriends (tx, name1, name2) {
return tx.run(
'MATCH (a:Person {name: $name1}) ' +
'MATCH (b:Person {name: $name2}) ' +
'MERGE (a)-[:KNOWS]->(b)',
{ name1: name1, name2: name2 }
)
}
// To collect friend relationships
const friends = []
// Match and display all friendships.
function findFriendships (tx) {
const result = tx.run('MATCH (a)-[:KNOWS]->(b) RETURN a.name, b.name')
result.subscribe({
onNext: record => {
const name1 = record.get(0)
const name2 = record.get(1)
friends.push({ name1: name1, name2: name2 })
}
})
}
// To collect the session bookmarks
const savedBookmarks = []
// Create the first person and employment relationship.
const session1 = driver.session({ defaultAccessMode: neo4j.WRITE })
const first = session1
.writeTransaction(tx => addCompany(tx, 'Wayne Enterprises'))
.then(() => session1.writeTransaction(tx => addPerson(tx, 'Alice')))
.then(() =>
session1.writeTransaction(tx =>
addEmployee(tx, 'Alice', 'Wayne Enterprises')
)
)
.then(() => {
savedBookmarks.push(session1.lastBookmark())
})
.then(() => session1.close())
// Create the second person and employment relationship.
const session2 = driver.session({ defaultAccessMode: neo4j.WRITE })
const second = session2
.writeTransaction(tx => addCompany(tx, 'LexCorp'))
.then(() => session2.writeTransaction(tx => addPerson(tx, 'Bob')))
.then(() =>
session2.writeTransaction(tx => addEmployee(tx, 'Bob', 'LexCorp'))
)
.then(() => {
savedBookmarks.push(session2.lastBookmark())
})
.then(() => session2.close())
// Create a friendship between the two people created above.
const last = Promise.all([first, second]).then(() => {
const session3 = driver.session({
defaultAccessMode: neo4j.WRITE,
bookmarks: savedBookmarks
})
return session3
.writeTransaction(tx => makeFriends(tx, 'Alice', 'Bob'))
.then(() =>
session3.readTransaction(findFriendships).then(() => session3.close())
)
})
from neo4j import GraphDatabase
class BookmarksExample:
def __init__(self, uri, auth):
self.driver = GraphDatabase.driver(uri, auth=auth)
def close(self):
self.driver.close()
# Create a person node.
@classmethod
def create_person(cls, tx, name):
tx.run("CREATE (:Person {name: $name})", name=name)
# Create an employment relationship to a pre-existing company node.
# This relies on the person first having been created.
@classmethod
def employ(cls, tx, person_name, company_name):
tx.run("MATCH (person:Person {name: $person_name}) "
"MATCH (company:Company {name: $company_name}) "
"CREATE (person)-[:WORKS_FOR]->(company)",
person_name=person_name, company_name=company_name)
# Create a friendship between two people.
@classmethod
def create_friendship(cls, tx, name_a, name_b):
tx.run("MATCH (a:Person {name: $name_a}) "
"MATCH (b:Person {name: $name_b}) "
"MERGE (a)-[:KNOWS]->(b)",
name_a=name_a, name_b=name_b)
# Match and display all friendships.
@classmethod
def print_friendships(cls, tx):
result = tx.run("MATCH (a)-[:KNOWS]->(b) RETURN a.name, b.name")
for record in result:
print("{} knows {}".format(record["a.name"], record["b.name"]))
def main(self):
saved_bookmarks = [] # To collect the session bookmarks
# Create the first person and employment relationship.
with self.driver.session() as session_a:
session_a.write_transaction(self.create_person, "Alice")
session_a.write_transaction(self.employ, "Alice", "Wayne Enterprises")
saved_bookmarks.append(session_a.last_bookmark())
# Create the second person and employment relationship.
with self.driver.session() as session_b:
session_b.write_transaction(self.create_person, "Bob")
session_b.write_transaction(self.employ, "Bob", "LexCorp")
saved_bookmarks.append(session_b.last_bookmark())
# Create a friendship between the two people created above.
with self.driver.session(bookmarks=saved_bookmarks) as session_c:
session_c.write_transaction(self.create_friendship, "Alice", "Bob")
session_c.read_transaction(self.print_friendships)
Routing transactions using access modes
Transactions can be executed in either read or write mode; this is known as the access mode. In a Causal Cluster, each transaction will be routed to an appropriate server based on the mode. When using a single instance, all transactions will be passed to that one server.
Routing Cypher by identifying reads and writes can improve the utilization of available cluster resources. Since read servers are typically more plentiful than write servers, it is beneficial to direct read traffic to read servers instead of the write server. Doing so helps in keeping write servers available for write transactions.
Access mode is generally specified by the method used to call the transaction function. Session classes provide a method for calling reads and another for writes.
As a fallback for auto-commit and unmanaged transactions, a default access mode can also be provided at session level. This is only used in cases when the access mode cannot otherwise be specified. In case a transaction function is used within that session, the default access mode will be overridden.
The driver does not parse Cypher and therefore cannot automatically determine whether a transaction is intended to carry out read or write operations. As a result, a write transaction tagged as a read will still be sent to a read server, but will fail on execution. |
public long AddPerson(string name)
{
using (var session = Driver.Session())
{
session.WriteTransaction(tx => CreatePersonNode(tx, name));
return session.ReadTransaction(tx => MatchPersonNode(tx, name));
}
}
private static IResult CreatePersonNode(ITransaction tx, string name)
{
return tx.Run("CREATE (a:Person {name: $name})", new {name});
}
private static long MatchPersonNode(ITransaction tx, string name)
{
var result = tx.Run("MATCH (a:Person {name: $name}) RETURN id(a)", new {name});
return result.Single()[0].As<long>();
}
func addPersonNodeTxFunc(name string) neo4j.TransactionWork {
return func(tx neo4j.Transaction) (interface{}, error) {
result, err := tx.Run("CREATE (a:Person {name: $name})", map[string]interface{}{"name": name})
if err != nil {
return nil, err
}
return result.Consume()
}
}
func matchPersonNodeTxFunc(name string) neo4j.TransactionWork {
return func(tx neo4j.Transaction) (interface{}, error) {
result, err := tx.Run("MATCH (a:Person {name: $name}) RETURN id(a)", map[string]interface{}{"name": name})
if err != nil {
return nil, err
}
if result.Next() {
return result.Record().Values[0], nil
}
return nil, errors.New("one record was expected")
}
}
func addPersonNode(driver neo4j.Driver, name string) (int64, error) {
session := driver.NewSession(neo4j.SessionConfig{AccessMode: neo4j.AccessModeWrite})
defer session.Close()
if _, err := session.WriteTransaction(addPersonNodeTxFunc(name)); err != nil {
return -1, err
}
var id interface{}
var err error
if id, err = session.ReadTransaction(matchPersonNodeTxFunc(name)); err != nil {
return -1, err
}
return id.(int64), nil
}
import org.neo4j.driver.Session;
import org.neo4j.driver.Result;
import org.neo4j.driver.Transaction;
import org.neo4j.driver.TransactionWork;
import static org.neo4j.driver.Values.parameters;
public long addPerson( final String name )
{
try ( Session session = driver.session() )
{
session.writeTransaction( new TransactionWork<Void>()
{
@Override
public Void execute( Transaction tx )
{
return createPersonNode( tx, name );
}
} );
return session.readTransaction( new TransactionWork<Long>()
{
@Override
public Long execute( Transaction tx )
{
return matchPersonNode( tx, name );
}
} );
}
}
private static Void createPersonNode( Transaction tx, String name )
{
tx.run( "CREATE (a:Person {name: $name})", parameters( "name", name ) );
return null;
}
private static long matchPersonNode( Transaction tx, String name )
{
Result result = tx.run( "MATCH (a:Person {name: $name}) RETURN id(a)", parameters( "name", name ) );
return result.single().get( 0 ).asLong();
}
const session = driver.session()
try {
await session.writeTransaction(tx =>
tx.run('CREATE (a:Person {name: $name})', { name: personName })
)
const result = await session.readTransaction(tx =>
tx.run('MATCH (a:Person {name: $name}) RETURN id(a)', {
name: personName
})
)
const singleRecord = result.records[0]
const createdNodeId = singleRecord.get(0)
console.log('Matched created node with id: ' + createdNodeId)
} finally {
await session.close()
}
def create_person_node(tx, name):
tx.run("CREATE (a:Person {name: $name})", name=name)
def match_person_node(tx, name):
result = tx.run("MATCH (a:Person {name: $name}) RETURN count(a)", name=name)
return result.single()[0]
def add_person(name):
with driver.session() as session:
session.write_transaction(create_person_node, name)
persons = session.read_transaction(match_person_node, name)
return persons
Databases and execution context
Neo4j offers the ability to work with multiple databases within the same DBMS.
For Community Edition, this is limited to one user database, plus the system
database.
From a driver API perspective, a database can be selected on session construction, and is used as an execution context for the transactions within that session. It is not currently possible to execute a transaction across multiple databases.
In a multi-database environment, the server tags one database as default. This is selected whenever a session is created without naming a particular database as default. In an environment with a single database, that database is always the default.
For more information about managing multiple databases within the same DBMS, refer to Cypher Manual → Neo4j databases and graphs which has a full breakdown of the Neo4j data storage hierarchy.
The system
database
Some administrative operations must be carried out against the system
database.
To do this, construct a session targeting the database called system
and execute Cypher as usual.
Please see Cypher Manual → Databases for more information.
Database selection
The database selection on the client side happens on the session API.
You pass the name of the database to the driver during session creation.
If you don’t specify a name, the default database is used.
The database name must not be null
, nor an empty string.
The selection of database is only possible when the driver is connected against Neo4j Enterprise Edition. Changing to any other database than the default database in Neo4j Community Edition will lead to a runtime error. |
Please note that the database that is requested must exist.
using (var session = _driver.Session(SessionConfigBuilder.ForDatabase("examples")))
{
session.Run("CREATE (a:Greeting {message: 'Hello, Example-Database'}) RETURN a").Consume();
}
void SessionConfig(SessionConfigBuilder configBuilder) =>
configBuilder.WithDatabase("examples")
.WithDefaultAccessMode(AccessMode.Read)
.Build();
using (var session = _driver.Session(SessionConfig))
{
var result = session.Run("MATCH (a:Greeting) RETURN a.message as msg");
var msg = result.Single()[0].As<string>();
Console.WriteLine(msg);
}
session := driver.NewSession(neo4j.SessionConfig{DatabaseName: "example"})
import org.neo4j.driver.AccessMode;
import org.neo4j.driver.Session;
import org.neo4j.driver.SessionConfig;
try ( Session session = driver.session( SessionConfig.forDatabase( "examples" ) ) )
{
session.run( "CREATE (a:Greeting {message: 'Hello, Example-Database'}) RETURN a" ).consume();
}
SessionConfig sessionConfig = SessionConfig.builder()
.withDatabase( "examples" )
.withDefaultAccessMode( AccessMode.READ )
.build();
try ( Session session = driver.session( sessionConfig ) )
{
String msg = session.run( "MATCH (a:Greeting) RETURN a.message as msg" ).single().get( "msg" ).asString();
System.out.println(msg);
}
const session = driver.session({ database: 'examples' })
try {
const result = await session.writeTransaction(tx =>
tx.run(
'CREATE (a:Greeting {message: "Hello, Example-Database"}) RETURN a.message'
)
)
const singleRecord = result.records[0]
const greeting = singleRecord.get(0)
console.log(greeting)
} finally {
await session.close()
}
const readSession = driver.session({
database: 'examples',
defaultAccessMode: neo4j.READ
})
try {
const result = await readSession.writeTransaction(tx =>
tx.run('MATCH (a:Greeting) RETURN a.message')
)
const singleRecord = result.records[0]
const greeting = singleRecord.get(0)
console.log(greeting)
} finally {
await readSession.close()
}
from neo4j import READ_ACCESS
with driver.session(database="example") as session:
session.run("CREATE (a:Greeting {message: 'Hello, Example-Database'}) RETURN a").consume()
with driver.session(database="example", default_access_mode=READ_ACCESS) as session:
message = session.run("MATCH (a:Greeting) RETURN a.message as msg").single().get("msg")
print(message)
Type mapping
Drivers translate between application language types and the Cypher Types.
To pass parameters and process results, it is important to know the basics of how Cypher works with types and to understand how the Cypher Types are mapped in the driver.
The table below shows the available data types. All types can be potentially found in the result, although not all types can be used as parameters.
Cypher Type | Parameter | Result |
---|---|---|
|
✔ |
✔ |
|
✔ |
✔ |
|
✔ |
✔ |
|
✔ |
✔ |
|
✔ |
✔ |
|
✔ |
✔ |
|
✔ |
✔ |
|
✔ |
✔ |
|
✔ |
✔ |
|
✔ |
✔ |
|
✔ |
✔ |
|
✔ |
✔ |
|
✔ |
✔ |
|
✔ |
✔ |
|
✔ |
✔ |
|
✔ |
|
|
✔ |
|
|
✔ |
* The null marker is not a type but a placeholder for absence of value.
For information on how to work with null in Cypher, please refer to Cypher Manual → Working with null
.
** Nodes, relationships and paths are passed in results as snapshots of the original graph entities. While the original entity IDs are included in these snapshots, no permanent link is retained back to the underlying server-side entities, which may be deleted or otherwise altered independently of the client copies. Graph structures may not be used as parameters because it depends on application context whether such a parameter would be passed by reference or by value, and Cypher provides no mechanism to denote this. Equivalent functionality is available by simply passing either the ID for pass-by-reference, or an extracted map of properties for pass-by-value.
The Neo4j Drivers map Cypher Types to and from native language types as depicted in the table below. Custom types (those not available in the language or standard library) are highlighted in bold.
Neo4j Cypher Type | .NET Type |
---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
* Time zone names adhere to the IANA system, rather than the Windows system. Inbound conversion is carried out using Extended Windows-Olson zid mapping as defined by Unicode CLDR.
Neo4j type | Go type |
---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
* When a time.Time
value is sent/received through the driver and its Zone()
returns a name of Offset
, the value is stored with its offset value rather than its zone name.
Neo4j Cypher Type | Java Type |
---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
* A Duration
or Period
passed as a parameter will always be implicitly converted to IsoDuration
.
Neo4j Cypher Type | JavaScript Type |
---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
* JavaScript has no native integer type so a custom type is provided. For convenience, this can be disabled through configuration so that the native Number type is used instead. Note that this can lead to a loss of precision.
Neo4j Type | Python 3 Type |
---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
* A datetime.timedelta
object passed as a parameter will always be implicitly converted to neo4j.time.Duration
† Where tzinfo
is not None
†† Where tzinfo
is None
Exceptions and error handling
When executing Cypher or carrying out other operations with the driver, certain exceptions and error cases may arise. Server-generated exceptions are each associated with a status code that describes the nature of the problem and a message that provides more detail.
The classifications are listed in the table below.
Classification | Description |
---|---|
ClientError |
The client application has caused an error. The application should amend and retry the operation. |
DatabaseError |
The server has caused an error. Retrying the operation will generally be unsuccessful. |
TransientError |
A temporary error has occurred. The application should retry the operation. |
Service unavailable
A Service Unavailable exception will be signalled when the driver is no longer able to establish communication with the server, even after retries.
Encountering this condition usually indicates a fundamental networking or database problem.
While certain mitigations can be made by the driver to avoid this issue, there will always be cases when this is impossible. As such, it is highly recommended to ensure client applications contain a code path that can be followed when the client is no longer able to communicate with the server.
Transient errors
Transient errors are those which are generated by the server and marked as safe to retry without alteration to the original request. Examples of such errors are deadlocks and memory issues.
When using transaction functions, the driver will usually be able to automatically retry when a transient failure occurs.