Transactions

Transactions on the Cloud Datastore behave differently to those of a SQL database. It's worth reading the Cloud Datastore transactions documentation to familiarise yourself with their behaviour. Crucially:

"queries and lookups inside a Datastore mode transaction do not see the results of previous writes inside that transaction".

This is partially mitigated by our caching system. For more info on how querying and caching work when using gcloudc.db.transaction see the caching docs.

Django's atomic decorator/context manager will have no effect on the Datastore.

However, gcloudc.db.transaction provides a separate set of decorators/context managers for managing transactions.

atomic

atomic(
    independent=False,
    mandatory=False,
    using="default",
    read_only=False,
    enable_cache=True,
) -> Transaction

This can be used either as a decorator or a context manager to execute a block of code within a Datastore transaction.

If used as a context manager, it will return the Transaction object. If used as a function decorator, you will need to call current_transaction() to get the Transaction object.

See Transaction object.

The kwargs are:

  • independent: forces the start of a new, independent transaction, even if you are currently already in a transaction.
  • mandatory: forces a check to ensure that you are already within an outer transaction. Raises TransactionFailedError if not.
  • using: the database connection name to use (works the same as Django's using parameter for database operations).
  • read_only: for creating a read-only transaction. This functionality is not yet implemented.
  • enable_cache: enables gcloudc's caching of queries which can only return a singular, unique result.

For more info on caching and transactions see the caching docs

non_atomic

non_atomic(using="default")

This can be used either as a decorator or a context manager to break out and execute a block of code outside of the current transaction.

in_atomic_block

in_atomic_block(using="default") -> bool

This function tells you whether or not you are currently inside an atomic transaction.

on_commit

on_commit(func, using=None)

The same as Django's on_commit, this registers a function to be called when the current transaction is committed. If the current transaction is rolled back, func will not be called.

Transaction objects

When using atomic as a context manager, or using current_transaction(), you will get a Transaction object. This can and should be used to perform operations within the transaction.

A Transaction object has the following methods:

refresh_if_unread

refresh_if_unread(instance) -> None

This method should be used to refresh instances of Django models from the database.

Remember that "queries and lookups inside a Datastore mode transaction do not see the results of previous writes inside that transaction", therefore, if you query for an object inside a transaction, then modify it, save it and fetch it from the DB again, the object that you get back will be the original (unmodified) version, as it existed before the transaction started. So if you then modify and save the object again, you'll overwrite the changes which you made earlier in the transaction. Therefore, to avoid this problem, this method will only re-fetch the given object from the database if it has not yet been fetched within the current transaction.

Note: the current implementation of caching should not require this method. We're investigating if it's still necessary or not. If not it might get deprecated soon.

has_already_been_read

has_already_been_read(instance) -> bool

This method tells you whether or not the given Django model instance has been read from the database within the current transaction.

Example usage

from gclouc.db.transaction import atomic, current_transaction, on_commit

# Using atomic() as a context manager
counter = MyCounter.objects.get(pk=1)
with atomic() as transaction:
    on_commit(log_counter_increment)  # Will only happen if the transaction is successful
    transaction.refresh_if_unread(counter)
    counter.count = counter.count + 1
    counter.save()

# Using atomic() as a decorator
@atomic()
def increment_counter(counter):
    transaction = current_transaction()
    on_commit(log_counter_increment)  # Will only happen if the transaction is successful
    transaction.refresh_if_unread(obj)
    counter.count = counter.count + 1
    counter.save()

counter = MyCounter.objects.get(pk=1)
increment_counter(counter)

def log_counter_increment():
    logging.info("Incremented counter!")