4.1. Simple sessions

This section describes simple sessions, transactions executed within a simple session and the results thereof.

Available in:

  • .NET Driver
  • Java Driver

Simple sessions provide a "classic" blocking style API for Cypher execution. In general, simple sessions provide the easiest programming style to work with since API calls are executed in a strictly sequential fashion.

4.1.1. Lifecycle

The session lifetime extends from session construction to session closure. In languages that support them, simple sessions are usually scoped within a context block; this ensures that they are properly closed and that any underlying connections are released and not leaked.

Example 4.1. Session construction and closure
using (var session = driver.Session(...)) {
    // transactions go here
  }
try (Session session = driver.session(...)) {
    // transactions go here
}

Sessions can be configured in a number of different ways. This is carried out by supplying configuration inside the session constructor. See Section 4.4, “Session configuration” for more details.

4.1.2. Transaction functions

Transaction functions are used for containing transactional units of work. This form of transaction requires minimal boilerplate code and allows for a clear separation of database queries and application logic.

Transaction functions are also desirable since they encapsulate retry logic and allow for the greatest degree of flexibility when swapping out a single instance of server for a cluster.

Transaction functions can be called as either read or write operations. This choice will route the transaction to an appropriate server within a clustered environment. If you are operating in a single instance environment, this routing has no impact. It does give you flexibility if you choose to adopt a clustered environment later on.

Before writing a transaction function it is important to ensure that it is designed to be idempotent. This is because a function may be executed multiple times if initial runs fail.

Any query results obtained within a transaction function should be consumed within that function, as connection-bound resources cannot be managed correctly when out of scope. To that end, transaction functions can return values but these should be derived values rather than raw results.

Transaction functions are the recommended form for containing transactional units of work.

When a transaction fails, the driver retry logic is invoked. For several failure cases, the transaction can be immediately retried against a different server. These cases include connection issues, server role changes (e.g. leadership elections) and transient errors. Retry logic can be configured when creating a session.

Example 4.2. Transaction function
public void AddPerson(string name)
{
    using (var session = Driver.Session())
    {
        session.WriteTransaction(tx => tx.Run("CREATE (a:Person {name: $name})", new {name}));
    }
}
public void addPerson( final String name )
{
    try ( Session session = driver.session() )
    {
        session.writeTransaction( tx -> {
            tx.run( "CREATE (a:Person {name: $name})", parameters( "name", name ) );
            return 1;
        } );
    }
}

4.1.3. Auto-commit transactions

An auto-commit transaction is a basic but limited form of transaction. Such a transaction consists of only one Cypher query and is not automatically replayed on failure. Therefore any error scenarios must be handled by the client application itself.

Auto-commit transactions are intended to be used for simple use cases such as when learning Cypher or writing one-off scripts.

It is not recommended to use auto-commit transactions in production environments.

Unlike other kinds of Cypher Query, PERIODIC COMMIT queries do not participate in the causal chain.

Therefore, the only way to execute PERIODIC COMMIT from a driver is to use auto-commit transactions.

Please refer to the Cypher Manual → PERIODIC COMMIT query hint.

Example 4.3. Simple auto-commit transactions
public void AddPerson(string name)
{
    using (var session = Driver.Session())
    {
        session.Run("CREATE (a:Person {name: $name})", new {name});
    }
}
public void addPerson( String name )
{
    try ( Session session = driver.session() )
    {
        session.run( "CREATE (a:Person {name: $name})", parameters( "name", name ) );
    }
}

4.1.4. Consuming results

Query results are typically consumed as a stream of records. The drivers provide a language-idiomatic way to iterate through that stream.

Example 4.4. Consuming results
public List<string> GetPeople()
{
    using (var session = Driver.Session())
    {
        return session.ReadTransaction(tx =>
        {
            var result = tx.Run("MATCH (a:Person) RETURN a.name ORDER BY a.name");
            return result.Select(record => record[0].As<string>()).ToList();
        });
    }
}
public List<String> getPeople()
{
    try ( Session session = driver.session() )
    {
        return session.readTransaction( tx -> {
            List<String> names = new ArrayList<>();
            Result result = tx.run( "MATCH (a:Person) RETURN a.name ORDER BY a.name" );
            while ( result.hasNext() )
            {
                names.add( result.next().get( 0 ).asString() );
            }
            return names;
        } );
    }
}

4.1.5. Retaining results

Within a session, only one result stream can be active at any one time. Therefore, if the result of one query is not fully consumed before another query is executed, the remainder of the first result will be automatically buffered within the result object.

Figure 4.1. Result buffer
driver result buffer

This buffer provides a staging point for results, and divides result handling into fetching (moving from the network to the buffer) and consuming (moving from the buffer to the application).

For large results, the result buffer may require a significant amount of memory.

For this reason, it is recommended to consume results in order wherever possible.

Client applications can choose to take control of more advanced query patterns by explicitly retaining results. Such explicit retention may also be useful when a result needs to be saved for future processing. The drivers offer support for this process, as per the example below:

Example 4.5. Retain results for further processing
public int AddEmployees(string companyName)
{
    using (var session = Driver.Session())
    {
        var persons =
            session.ReadTransaction(tx => tx.Run("MATCH (a:Person) RETURN a.name AS name").ToList());
        return persons.Sum(person => session.WriteTransaction(tx =>
        {
            tx.Run("MATCH (emp:Person {name: $person_name}) " +
                   "MERGE (com:Company {name: $company_name}) " +
                   "MERGE (emp)-[:WORKS_FOR]->(com)",
                new {person_name = person["name"].As<string>(), company_name = companyName});
            return 1;
        }));
    }
}
public int addEmployees( final String companyName )
{
    try ( Session session = driver.session() )
    {
        int employees = 0;
        List<Record> persons = session.readTransaction( new TransactionWork<List<Record>>()
        {
            @Override
            public List<Record> execute( Transaction tx )
            {
                return matchPersonNodes( tx );
            }
        } );
        for ( final Record person : persons )
        {
            employees += session.writeTransaction( new TransactionWork<Integer>()
            {
                @Override
                public Integer execute( Transaction tx )
                {
                    tx.run( "MATCH (emp:Person {name: $person_name}) " +
                            "MERGE (com:Company {name: $company_name}) " +
                            "MERGE (emp)-[:WORKS_FOR]->(com)",
                            parameters( "person_name", person.get( "name" ).asString(), "company_name",
                                    companyName ) );
                    return 1;
                }
            } );
        }
        return employees;
    }
}

private static List<Record> matchPersonNodes( Transaction tx )
{
    return tx.run( "MATCH (a:Person) RETURN a.name AS name" ).list();
}