NDB Transactions

A transaction is an operation or set of operations that either succeeds completely or fails completely. An application can perform multiple operations and calculations in a single transaction. Using the NDB asynchronous API, an application can manage multiple transactions simultaneously if they are independent. The synchronous API offers a simplified API using the @ndb.transactional() decorator. The decorated function is executed in the context of the transaction.

key = ndb.Key(Greeting, 'joe')

def greet():
  # 'key' here uses the key variable in the outer scope;
  # the callback function is a closure.
  ent = key.get()
  if ent is None:
    ent = Greeting(key=key, message='Hey Joe')
  return ent


If the transaction "collides" with another, it fails; NDB automatically retries such failed transactions a few times. Thus, the function may be called multiple times if the transaction is retried. There is a limit (default 3) to the number of retries attempted; if the transaction still does not succeed, NDB raises TransactionFailedError. You can change the retry count by passing retries=N to the transactional() decorator. A retry count of 0 means the transaction is attempted once but not retried if it fails; a retry count of N means that the transaction may be attempted a total of N+1 times. Example:

@ndb.transactional(retries=1) # Total of 2 tries
def greet():
  # do greeting

In transactions, only ancestor queries are allowed. By default, a transaction can only work with entities in the same entity group (entities whose keys have the same "ancestor").

You can specify cross-group ("XG") transactions (which allow up to twenty-five entity groups), by passing xg=True:

def greet_a_variety_of_things():
  # do greeting

If the function raises an exception, the transaction is immediately aborted and NDB re-raises the exception so that the calling code sees it. You can force a transaction to fail silently by raising the ndb.Rollback exception (the function call returns None in this case). There is no mechanism to force a retry.

You might have a function that you don't always want to run in a transaction. Instead of decorating such a function with @ndb.transactional, pass it as a callback function to ndb.transaction()

def get_or_insert(keyname):
  key = ndb.Key(Greeting, keyname)
  ent = key.get()
  if ent is None:
    ent = Greeting(key=key, message='Hey Rodrigo')
  return ent

moraes = ndb.transaction(lambda: get_or_insert('rodrigo'))

To test whether some code is running inside a transaction, use the in_transaction() function.

You can specify how a "transactional" function should behave if invoked by code that's already in a transaction. The @ndb.non_transactional decorator specifies that a function should not run in a transaction; if called in a transaction, it runs outside the transaction. The @ndb.transactional decorator and ndb.transaction function take a propagation keyword argument. For example, if a function should start a new, independent transaction, decorate it like so:

def IndependentGreet():
  # ... greet ...

The propagation types are listed with the other Context Options and Transaction Options

Transaction behavior and NDB's caching behavior can combine to confuse you if you don't know what's going on. If you modify an entity inside a transaction but have not yet committed the transaction, then NDB's context cache has the modified value but the underlying datastore still has the unmodified value.