Transactions

This section outlines the way neomodel handles transaction management. For a thorough background on how the Neo4J DBMS handles sessions and transactions, please refer to the documentation.

Basic usage

Transactions can be used via a context manager:

from neomodel import db

with db.transaction:
    Person(name='Bob').save()

or as a function decorator:

@db.transaction
def update_user_name(uid, name):
    user = Person.nodes.filter(uid=uid)[0]
    user.name = name
    user.save()

or manually:

db.begin()
try:
    new_user = Person(name=username, email=email).save()
    send_email(new_user)
    db.commit()
except Exception as e:
    db.rollback()

Transactions are local to the thread as is the db object (see threading.local). If you’re using celery or another task scheduler it’s advised to wrap each task within a transaction:

@task
@db.transaction  # comes after the task decorator
def send_email(user):
    ...

Explicit Transactions

Neomodel also supports explicit transactions that are pre-designated as either read or write.

This is vital when using neomodel over a Neo4J causal cluster because internally, queries will be rerouted to different servers depending on their designation.

Note here that this functionality is enabled when bolt+routing:// has been specified as the scheme of the connection URL, as opposed to bolt:// which is more common in single instance deployments.

Read transactions do not modify the database state and therefore only include CYPHER operations that simply return results. Write transactions however do modify the database state and therefore include all CYPHER operations that can potentially Create, Update or Delete Nodes and / or Relationships.

By default, starting a transaction without explicitly specifying its type, results in a WRITE transaction.

Similarly to Basic Usage, Neomodel designates transactions in the following ways:

With distinct context managers:

with db.read_transaction:
    ...

with db.write_transaction:
    ...

with db.transaction:
    # This designates the transaction as WRITE even if
    # the the enclosed block of code will not modify the
    # database state.

With function decorators:

@db.write_transaction
def update_user_name(uid, name):
    user = Person.nodes.filter(uid=uid)[0]
    user.name = name
    user.save()

@db.read_transaction
def get_all_users():
    return Person.nodes.all()

@db.transaction # By default a WRITE transaction
...

With explicit designation:

db.begin("WRITE")
...
db.begin("READ")
...
db.begin() # By default a **WRITE** transaction

Bookmarks

Neomodel also supports bookmarks. When using neomodel over a Neo4J causal cluster there is no guarantee that a read will see all of the data from an earlier committed write transaction. Each transaction returns a bookmark that identifies the transaction. When starting a new transaction one or more bookmarks may be passed in and the read will not complete until data from all of the bookmarked transactions is available.

With context managers one or more bookmarks may be set in the transaction before entering the context manager and the resulting bookmark may be extracted only after the context manager has exited successfully:

transaction = db.transaction
transaction.bookmarks = [bookmark1, bookmark2]
with transaction:
    # All database access happens after completion of the transactions
    # listed in bookmark1 and bookmark2

bookmark = transaction.last_bookmark

Bookmarks are strings and may be passed between processes. transaction.bookmarks may be set to a single bookmark, a sequence of bookmarks, or None.

With function decorators use the with_bookmarks attribute on the transaction. The decorator will accept an optional bookmarks keyword-only parameter with the bookmarks to be passed in to the transaction. This parameter is removed and not passed to the decorated function. Any returned value from the decorated function becomes the first element of a tuple with the last bookmark as the second element:

@db.write_transaction.with_bookmarks
def update_user_name(uid, name):
    user = Person.nodes.filter(uid=uid)[0]
    user.name = name
    user.save()

@db.read_transaction.with_bookmarks
def get_all_users():
    return Person.nodes.all()


result, bookmark = update_user_name(uid, name)

users, last_bookmark = get_all_users(bookmarks=[bookmark])
for user in users:
    ...

or manually:

db.begin(bookmarks=[bookmark])
try:
    new_user = Person(name=username, email=email).save()
    send_email(new_user)
    bookmark = db.commit()
except Exception as e:
    db.rollback()

Impersonation

Neo4j Enterprise feature

Impersonation (see Neo4j driver documentation <https://neo4j.com/docs/api/python-driver/current/api.html#impersonated-user-ref>`) can be enabled via a context manager:

from neomodel import db

with db.impersonate(user="writeuser"):
    Person(name='Bob').save()

or as a function decorator:

@db.impersonate(user="writeuser")
def update_user_name(uid, name):
    user = Person.nodes.filter(uid=uid)[0]
    user.name = name
    user.save()

This can be mixed with other context manager like transactions:

from neomodel import db

@db.impersonate(user="tempuser")
# Both transactions will be run as the same impersonated user
def func0():
    @db.transaction()
    def func1():
        ...

    @db.transaction()
    def func2():
        ...