Transactions and Batch

Transactions in Cypher

Neo4j provides, since its version 2.0.0, a transactional endpoint for Cypher queries. That feature is wrapped in ‘neo4jrestclient’ in the gdb.transaction() method. But for backwards compatibility issues (there were transactions before), you need to add an extra parameter for_query=True in order to enable it.

Object-based

The easiest way to use a transaction is by creating a tx object:

>>> tx = gdb.transaction(for_query=True)

While the transaction is alive, a property finished is set to False. The property expires has a string with the date sent by the server.

>>> tx.finished
False
>>> tx.expires
"Sun, 08 Dec 2013 15:05:52 +0000"

Now, regular Cypher queries can be added to the transaction and executed or in server:

>>> tx.append("CREATE (a) RETURN a", returns=client.Node)
>>> tx.append("CREATE (b) RETURN b", params={}  )
>>> results = tx.execute()
>>> len(results) == 2
True

Both methods, execute() and commit(), return a QuerySequence with the results of the queries sent to the server. You can now perform any check on the returned objects, and if there is something wrong, rollback the transaction and restore the previous state of the database.

>>> tx.rollback()
>>> len(results)
0

Or you can commit and get the remaining results returned by server:

>>> tx.append("MERGE (c:Person {name:'Carol'})")
>>> tx.append("MERGE (d:Person {name:'Dave'})")
>>> results = tx.commit()
>>> len(results) == 2
True

After commit() or rollback(), the transaction is destroyed and no queries can be appended.

Inside a with statement

For your convinience and wider control of the logic of your application, transactions can be written inside a with statement. This way, you don’t need a tx object and can use the regular syntax for queries. Each independent query is executed in the transaction, so you have the returned values and can operate with them:

>>> q = "start n=node(*) match n-[r:`{rel}`]-() return n, n.name, r, r.since"
>>> params = {"rel": "Knows"}
>>> returns = (client.Node, str, client.Relationship)
>>> with self.gdb.transaction(for_query=True) as tx:
...     self.gdb.query("MERGE (c:Person {name:'Carol'})")
...     results = self.gdb.query(q, params=params, returns=returns)
...     node = results[0][0]
...     if node["name"] == "Carol":
...         tx.rollback()

Batch-based Transactions

The transaction support for regular opertations, like CRUD and indexing on nodes and relationships, is based on the REST endpoint for batch operations, therefore there is some limitations because it is not a real transaction. When a batch of operations is sent to the server, Neo4j executes it in a transaction, but there is no option to rollback and recover a previous status of the database. In this sense, batch-emulated transactions for operations on creation, edition and deletion of elements are useful, but you won’t be able to perform checks on the elements modified until the batch is sent to the server and the transaction is commited.

Deletion

Basic usage for deletion:

>>> n = gdb.nodes.create()
>>> n["age"] = 25
>>> n["place"] = "Houston"
>>> n.properties
{'age': 25, 'place': 'Houston'}
>>> with gdb.transaction():
...     n.delete("age")
...
>>> n.properties
{u'place': u'Houston'}

Creation

Apart from update or deletion of properties, there is also creation. In this case, the object just created is returned through a TransactionOperationProxy object, which is automatically converted in the proper object when the transaction ends. This is the second part of the commit process and a parameter in the transaction, commit can be added to avoid the commit:

>>> n1 = gdb.nodes.create()
>>> n2 = gdb.nodes.create()
>>> with gdb.transaction() as tx:
   .....:         for i in range(1, 11):
   .....:             n1.relationships.create("relation_%s" % i, n2)
   .....:
>>> len(n1.relationships) != 0
True

Auto-update and auto-commit

When a transaction is performed, the values of the properties of the objects are updated automatically. However, this can be controled by hand adding a parameter in the transaction:

>>> n = gdb.nodes.create()
>>> n["age"] = 25
>>> with gdb.transaction(update=False):
   ....:         n.delete("age")
   ....:
>>> n.properties
{'age': 25}
>>> n.update()
>>> n.properties
{}

You can also set commit=False and commit manually after the with block is over:

>>> with gdb.transaction(commit=False) as tx:
   ....:         n.delete("age")
   ....:
>>> n.properties
{'age': 25}
>>> tx.commit()
>>> n.properties
{}

The commit method of the transaction object returns True if there’s no any fail. Otherwise, it returns ‘None’:

>>> tx.commit()
True
>>> len(n1.relationships)
10

Globals and nesting

In order to avoid the need of setting the transaction variable, ‘neo4jrestclient’ uses a global variable to handle all the transactions. The name of the variable can be changed using de options:

>>> client.options.TX_NAME = "_tx"  # Default value

And this behaviour can be disabled adding the right param in the transaction: using_globals. Even is possible (although not very recommendable) to handle different transactions in the same time and control when they are commited. There are many ways to set the transaction of a intruction (operation):

>>> n = gdb.nodes.create()
>>> n["age"] = 25
>>> n["name"] = "John"
>>> n["place"] = "Houston"
>>> with gdb.transaction(commit=False, using_globals=False) as tx1, \
   ....:      gdb.transaction(commit=False, using_globals=False) as tx2:
   ....:         n.delete("age", tx=tx1)
   ....:     n["name"] = tx2("Jonathan")
   ....:     n["place", tx2] = "Toronto"
   ....:
>>> "age" in n.properties
True
>>> tx1.commit()
True
>>> "age" in n.properties
False
>>> n["name"] == "John"
True
>>> n["place"] == "Houston"
True
>>> tx2.commit()
True
>>> n["name"] == "John"
False
>>> n["place"] == "Houston"
False