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') @ndb.transactional 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') ent.put() return ent greet()
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 five entity groups), by passing
xg=True
:
@ndb.transactional(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') ent.put() 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:
@ndb.transactional(propagation=ndb.TransactionOptions.INDEPENDENT) 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.