Sessions and Transactions

Sessions

Through the Driver, we open Sessions.

A session is a container for a sequence of transactions. Sessions borrow connections from a pool as required and are considered lightweight and disposable.

It is important to remember that sessions are not the same as database connections. When the Driver connects to the database, it opens up multiple TCP connections that can be borrowed by the session. A query may be sent over multiple connections, and results may be received by the driver over multiple connections.

Instead, sessions should be considered a client-side abstraction for grouping units of work, which also handle the underlying connections. The connections themselves are managed internally by the driver and are not directly exposed to the application.

To open a new session, call the session() method on the driver.

python
Open a new Session
with driver.session() as session:

This session method takes an optional configuration argument, which can be used to set the database to run any queries against in a multi-database setup, and the default access mode for any queries run within the transaction.

python
Open a new Session with additional arguments
with driver.session(database="people") as session:

If no database is supplied, the default database will be used. This is configured in the dbms.default_database in neo4j.conf, the default value is neo4j. You cannot create multiple databases in Neo4j Aura or in Neo4j Community Edition.

For more information on multi-database setup, see Managing Multiple Databases.

The default access mode is set to write, but this can be overwritten by explicitly calling the execute_read() or execute_write() functions.

Transactions

Through a Session, we can run one or more Transactions.

A transaction comprises a unit of work performed against a database. It is treated in a coherent and reliable way, independent of other transactions.

ACID Transactions

A transaction, by definition, must be atomic, consistent, isolated, and durable. Many developers are familiar with ACID transactions from their work with relational databases, and as such the ACID consistency model has been the norm for some time.

There are three types of transaction exposed by the driver:

  • Auto-commit Transactions

  • Read Transactions

  • Write Transactions

Auto-commit Transactions

Auto-commit transactions are a single unit of work that are immediately executed against the DBMS and acknowledged immediately. You can run an auto-commit transaction by calling the run() method on the session object, passing in a Cypher statement as a string and optionally an object containing a set of parameters.

python
session.run(
    "MATCH (p:Person {name: $name}) RETURN p", # Query
    name="Tom Hanks" # Named parameters referenced
)                    # in Cypher by prefixing with a $

For one-off queries only

In the event that there are any transient errors when running a query, the driver will not attempt to retry a query when using session.run(). For this reason, these should only be used for one-off queries and shouldn’t be used in production.

Read Transactions

When you intend to read data from Neo4j, you should execute a Read Transaction. In a clustered environment (including Neo4j Aura), read queries are distributed across the cluster.

The session provides an execute_read() function, which expects a single parameter, a function that represents the unit of work. The first argument passed to the function will be a transaction object, on which you can call the run() function to execute a Cypher statement. As with the session.run example above, the first argument for the run() function should be a Cypher statement, and any parameters in the Cypher statement should be passed as named parameters.

python
Running a Read Transaction
# Define a Unit of work to run within a Transaction (`tx`)
def get_movies(tx, title):
    return tx.run("""
        MATCH (p:Person)-[:ACTED_IN]->(m:Movie)
        WHERE m.title = $title // (1)
        RETURN p.name AS name
        LIMIT 10
    """, title=title)

# Execute get_movies within a Read Transaction
session.execute_read(get_movies,
    title="Arthur" # (2)
)

Parameterized Queries

In the query above, the the $ prefix of $title (1) indicates that this value relates to the parameter defined in the second argument (2) of the run() function call.

You do not need to explicitly commit a read transaction.

Write Transactions

If you intend to write data to the database, you should execute a Write Transaction.

In clustered environments, write queries are sent exclusively to the leader of the cluster. The leader of the cluster is then responsible for processing the query and synchronising the transaction across the followers and read-replica servers in the cluster.

The process is identical to running a Read Transaction.

python
Running a Write Transaction
# Call tx.run() to execute the query to create a Person node
def create_person(tx, name):
    return tx.run(
        "CREATE (p:Person {name: $name})",
        name=name
    )


# Execute the `create_person` "unit of work" within a write transaction
session.execute_write(create_person, name="Michael")

If anything goes wrong within of the unit of work or there is a problem on Neo4j’s side, the transaction will be automatically rolled back and the database will remain in its previous state. If the unit of work succeeds, the transaction will be automatically committed.

Unlike session.run(), if a transient error is received by the driver, for example a connectivity issue with the DBMS, the driver will automatically retry the unit of work.

Manually Creating Transactions

It is also possible to explicitly create a transaction object by calling the begin_transaction() function on the session.

python
Creating an Manual Transaction
with session.begin_transaction() as tx:
    # Run queries by calling `tx.run()`

This returns a Transaction object identical to the one passed in to the unit of work function when calling execute_read() or execute_write().

This method differs from the execute_read and execute_write() functions, in that the transaction will have to be manually committed or rolled back depending on the outcome of the unit of work.

You can commit a transaction by calling the tx.commit() function, or roll back the transaction by calling tx.rollback().

python
try:
    # Run a query
    tx.run(query, **params)

    # Commit the transaction
    tx.commit()
except:
    # If something goes wrong in the try block,
    # then rollback the transaction
    tx.rollback()

Closing the Session

Once you are finished with your session, you call the close() method to release any database connections held by that session.

python
Closing a Session
# Close the session
session.close()

A Working Example

Click to reveal a complete working example

The following code defines a function that accepts a name parameter, then executes a write transaction to create a :Person node in the people database.

python
Create a Person node in the customers database
def create_person_work(tx, name):
    return tx.run("CREATE (p:Person {name: $name}) RETURN p",
        name=name).single()

def create_person(name):
    # Create a Session for the `people` database
    session = driver.session(database="people")

    # Create a node within a write transaction
    record = session.execute_write(create_person_work,
                                    name=name)

    # Get the `p` value from the first record
    person = record["p"]

    # Close the session
    session.close()

    # Return the property from the node
    return person["name"]

Check your understanding

1. Valid Query Methods

Which of the following options are valid methods for running a read query through the driver?

  • session.run()

  • session.query()

  • session.execute_read()

  • session.read_transaction()

Hint

You can either run a Cypher statement within an auto-commit transaction or execute a Cypher statement within a managed transaction.

Solution

The answers are session.run() and session.execute_read().

2. Reading from the Database

Now we want to execute the unit_of_work function to query the database within a read transaction.

Which of the functions below should you call?

  • session.read(unit_of_work)

  • session.execute_read(unit_of_work)

  • session.execute_write(unit_of_work)

  • session.read_transaction(unit_of_work)

Hint

You are looking to execute a read query against the database.

Solution

The answer is execute_read

3. Writing to the Database

Now we want to execute the unit_of_work function to create a new node in the database.

Which of the functions below should you call?

  • session.execute_read(unit_of_work)

  • session.execute_write(unit_of_work)

  • session.write_query(unit_of_work)

  • session.write_transaction(unit_of_work)

Hint

You are looking to execute a write query against the database.

Solution

The answer is session.execute_write

Lesson Summary

In this lesson, you have learned about the process of creating sessions and running Cypher queries within transaction functions.

In the next lesson we will look at how we process the results of a query.