Introduction
You have learned how to execute one-off Cypher statements using the executableQuery()
method.
The drawback of this method is that the entire record set is only available once the final result is returned. For longer running queries or larger datasets, this can consume a lot of memory and a long wait for the final result.
In a production application, you may also need finer control of database transactions or to run multiple related queries as part of a single transaction.
Transaction methods allow you to run multiple queries in a single transaction while accessing results immediately.
Understanding Transactions
Neo4j is an ACID-compliant transactional database, which means queries are executed as part of a single atomic transaction. This ensures your data operations are consistent and reliable.
Sessions
To execute transactions, you need to open a session. The session object manages the underlying database connections and provides methods for executing transactions. For async applications, use the AsyncSession
.
try (var session = driver.session()) {
// Call transaction functions here
}
Consuming a session within a try-with-resources
will automatically close the session and release any underlying connections when the block is exited.
Specifying a database
In a multi-database instance, you can specify the database to use when creating a session using SessionConfig
.
import org.neo4j.driver.SessionConfig;
try (var session = driver.session(
SessionConfig.builder().withDatabase("databaseName").build()
)) {
// Call transaction functions here
}
Transaction functions
The Session
object provides two methods for managing transactions:
-
Session.executeRead()
-
Session.executeWrite()
If the entire function runs successfully, the transaction is committed automatically. If any errors occur, the entire transaction is rolled back.
Transient errors
These functions will also retry if the transaction fails due to a transient error, for example, a network issue.
Unit of work patterns
A unit of work groups operations into a single method, which is executed using the Session
:
// Unit of work
public static int createPerson(TransactionContext tx, String name, int age) { // (1)
var result = tx.run("""
CREATE (p:Person {name: $name, age: $age}) RETURN p
""", Map.of("name", name, "age", age)); // (2)
return result.list().size();
}
// Execute the unit of work
try (var session = driver.session()) { // (3)
var count = session.executeWrite(tx -> createPerson(tx, name, age));
}
-
The first argument to the transaction function is always a
TransactionContext
object. Any additional arguments are passed from the call toSession.executeRead
/Session.executeWrite
. -
The
run()
method on theTransactionContext
object is called to execute a Cypher statement. -
The
executeWrite()
method is called on the session object to execute the transaction function. The result of the transaction function is returned to the caller.
Multiple Queries in One Transaction
You can execute multiple queries within the same transaction function to ensure that all operations are completed or fail as a single unit.
public static void transferFunds(TransactionContext tx, String fromAccount, String toAccount, double amount) {
tx.run(
"MATCH (a:Account {id: $from_}) SET a.balance = a.balance - $amount",
Map.of("from_", fromAccount, "amount", amount)
);
tx.run(
"MATCH (a:Account {id: $to_}) SET a.balance = a.balance + $amount",
Map.of("to_", toAccount, "amount", amount)
);
}
Transaction state
Transaction state is maintained in the DBMS’s memory, so be mindful of running too many operations in a single transaction. Break up very large operations into smaller transactions when possible.
Handling outputs
The TransactionContext.run()
method returns a Result
object.
The records contained within the result will be iterated over as soon as they are available.
The result must be consumed within the transaction function.
The consume()
method discards any remaining records and returns a ResultSummary
object that can be used to access metadata about the Cypher statement.
The Session.executeRead
/ Session.executeWrite
method will return the result of the transaction function upon successful execution.
public static ResultSummary getAnswer(TransactionContext tx, String answer) {
var result = tx.run("RETURN $answer AS answer", Map.of("answer", answer));
return result.consume();
}
String result = "Hello, World!";
try (var session = driver.session()) {
ResultSummary summary = session.executeWrite(tx -> getAnswer(tx, result));
System.out.println(
String.format(
"Results available after %d ms and consumed after %d ms",
summary.resultAvailableAfter(TimeUnit.MILLISECONDS),
summary.resultConsumedAfter(TimeUnit.MILLISECONDS)
)
);
}
Check your understanding
Lesson Summary
In this lesson, you learned how to use transaction functions for read and write operations, implement the unit of work pattern, and execute multiple queries within a single transaction.
You should use transaction functions for read and write operations when you to start consuming results as soon as they are available.
In the next lesson, you will take a quiz to test your knowledge of using transactions.