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.
// Open a new session
var session = driver.session();
The Session
needs to be closed again, fortunately it’s an auto-closeable, so that a try-with-resources construct works well for us, where the session is automatically closed when the scope of the try
ends.
// Open a new session
try (var session = driver.session()) {
// Do something with the session...
// Close the session automatically in try-with-resources block
}
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 (either READ
or WRITE
).
// Create a Session for the `people` database
var sessionConfig = SessionConfig.builder()
.withDefaultAccessMode(AccessMode.WRITE)
.withDatabase("people")
.build();
try (var session = driver.session(sessionConfig)) {
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.
The default access mode is set to WRITE
, but this can be overwritten by explicitly calling the executeRead()
or executeWrite()
methods.
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.
var query = "MATCH () RETURN count(*) AS count";
var params = Values.parameters();
// Run a query in an auto-commit transaction
var res = session.run(query, params).single().get("count").asLong();
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 usingsession.run()
.
For this reason, these should only be used for one-off queries and shouldn’t be used in production with Neo4j clusters.Read Transactions
When you intend to read data from Neo4j, you should execute a Read Transaction. In a clustered environment (including Neo4j AuraDB), read queries are distributed across the database cluster.
The session provides an executeRead()
method, which expects a single parameter, a callback function that represents the unit of work.
The function will accept a single parameter, a Transaction object, on which you can call the tx.run()
method with two arguments: the Cypher statement as a string and an optional set of query parameters.
// Run a query within a Read Transaction
var res = session.readTransaction(tx -> {
return tx.run("""
MATCH (p:Person)-[:ACTED_IN]->(m:Movie)
WHERE m.title = $title // (1)
RETURN p.name AS name
LIMIT 10
""",
Values.parameters("title", "Arthur") // (2)
).list(r -> r.get("name").asString());
Parameterized Queries
In the query above, the$
prefix of $title
(1) indicates that this value relates to the parameter defined in the second argument (2) of the run()
method call.You do not need to explicitly commit a read transaction. 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.
Additionally, 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.
Write Transactions
If you intend to write data to the database, you should execute a Write Transaction.
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 and the changes applied and synchronized.
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 a write-quorum of the followers and eventually read-replica servers in the cluster.
session.writeTransaction(tx -> {
return tx.run(
"CREATE (p:Person {name: $name})",
Values.parameters("name", "Michael")).consume();
});
Manually Creating Transactions
It is also possible to explicitly create a transaction object by calling the beginTransaction()
method on the session.
// Open a new session
var session = driver.session(
SessionConfig.builder()
.withDefaultAccessMode(AccessMode.WRITE)
.build());
// Manually create a transaction
var tx = session.beginTransaction();
This returns a Transaction object identical to the one passed in to the unit of work function when calling executeRead()
or executeWrite()
.
This method differs from the executeRead
and executeWrite()
methods, 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()
method, or roll back the transaction by calling tx.rollback()
.
try {
// Perform an action
tx.run(query, params);
// Commit the transaction
tx.commit();
} catch (Exception e) {
// If something went wrong, rollback the transaction
tx.rollback();
}
Closing the Session
Usually the session is auto-closed by the try-with-resources setup
Only if you manage/pass around the session manually, you need to close it explicitly by calling the close()
method to release any resources held by that session.
// Close the session
session.close();
A Working Example
Click to reveal a complete working example
The following code defines a method that accepts a name parameter, then executes a write transaction to create a :Person
node in the people
database.
private static Map<String,Object> createPerson(String name) {
// Create a Session for the `people` database
var sessionConfig = SessionConfig.builder()
.withDefaultAccessMode(AccessMode.WRITE)
.withDatabase("people")
.build();
try (var session = driver.session(sessionConfig)) {
// Create a node within a write transaction
var res = session.writeTransaction(tx ->
tx.run("CREATE (p:Person {name: $name}) RETURN p",
Values.parameters("name", name))
.single());
// Get the `p` value from the first record
var p = res.get("p").asNode();
// Return the properties of the node
return p.asMap();
// Autoclose the sesssion
}
}
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.read()
-
✓
session.executeRead()
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.executeRead()
.
2. Reading from the Database
Say we want to create a new transaction that reads. We want any queries from this method to be distributed across the cluster.
Use the dropdown in the code block below to select the correct method.
var res = session./*select:readMethod*/(tx -> {
// Use tx.run to read from the database
})
-
❏ read
-
❏ readQuery
-
✓ executeRead
-
❏ readTransaction
Hint
You are looking to execute a read query against the database.
Solution
The answer is executeRead
3. Writing to the Database
Now we want to create a new node in the database.
Use the dropdown in the code block below to select the correct method.
var res = session./*select:writeMethod*/(tx -> {
// Use tx.run to write to the database
})
-
❏ insert
-
❏ write
-
❏ writeQuery
-
✓ executeWrite
Hint
You are looking to execute a write query against the database.
Solution
The answer is executeWrite
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.