Django model transactions

Transactions play an important role in the integrity of model data operations. When you set up a database for a Django project back in Chapter 1, among the many default options described in table 1-3, were the following transaction related settings:

AUTOCOMMIT = True
ATOMIC_REQUESTS = False

The AUTOCOMMIT option set to True ensures all operations that alter data (i.e. Create, Update & Delete) run in their own transaction, and depending on the outcome, are automatically committed to a database if successful or rolled back if they fail. The AUTOCOMMIT=True settings fits the expectations of most applications, as it cuts down on the need to explicitly mark operations final and provides reasonable behaviors (i.e. if the data operation is successful it's made final [a.k.a. commit], if not, then it's reverted [a.k.a. rolled back]).

However, there are occasions when grouping operations that alter data into larger transactions -- all-or-nothing tasks -- is a necessity.

Tip If you set AUTOCOMMIT = False you'll need to explicitly declare commits. A more practical choice is to leave the default AUTOCOMMIT = True and declare larger transactions explicitly on a case by case basis.

Transaction per request: ATOMIC_REQUESTS & decorators

Django supports the ATOMIC_REQUESTS option which is disabled by default. The ATOMIC_REQUEST is used to open a transaction on every request made to a Django application. By setting ATOMIC_REQUEST=True, it ensures the data operations included in a request (i.e. view method) are committed only if a response is successful.

Django atomic requests are helpful when you want the logic in a view method to be an all-or-nothing task. For example, if a view method executes fives sub-tasks associated with data (e.g. credit card verification procedure, sending an email), it can be helpful to ensure that only if all sub-tasks are successful the data operations be considered final, if only one sub-tasks fails, then all sub-tasks are rolled back as if nothing had happened.

Since ATOMIC_REQUEST=True opens a transaction for every request made on a Django application, it can cause a performance impact on high-traffic applications. Due to this factor, it's also possible to selectively disable atomic requests on certain requests when ATOMIC_REQUEST=True or inclusively selectively enable atomic requests on certain requests when ATOMIC_REQUEST=False. Listing 7-28 illustrates how to selectively activate and deactivate atomic requests.

Listing 7-28 Selectively activate and deactivate atomic requests with @non_atomic_requests and @atomic

from django.db import transaction

# When ATOMIC_REQUESTS=True you can individually disable atomic requests
@transaction.non_atomic_requests
def index(request):
# Data operations with transactions commit/rollback individually
# Failure of one operation does not influence other data_operation_1() data_operation_2()
data_operation_3() # When ATOMIC_REQUESTS=False you can individually enable atomic requests @transaction.atomic def detail(request): # Start transaction.
# Failure of any operation, rollbacks other operations data_operation_1() data_operation_2()
data_operation_3()
# Commit transaction if all operation successful

As you can see in listing 7-28, if you decide to use ATOMIC_REQUESTS=True, you can disable transactions per request on a view method with the @transaction.non_atomic_requests decorator from the django.db.transaction package. If you decide to keep the default ATOMIC_REQUESTS=False, you can enable transactions per request on a view method with the @transaction.atomic decorator from the same django.db.transaction package.

Context manager and callbacks: atomic() and on_commit()

In addition to the AUTOCOMMIT and ATOMIC_REQUEST transaction configurations, as well as the view method transaction decorators, it's possible to manage transactions at an intermediate scope. That is, coarser transactions than individual data operations (e.g. save()), but finer transactions than atomic requests (i.e. view methods).

The Python with keyword can invoke a context manager[6] charged with managing transactions. Context managers for transactions use the same django.db.transaction.atomic() method -- used as a decorator in listing 7-28 -- but inside the body of a method, as illustrated in listing 7-29.

Listing 7-29. Transactions with context managers

from django.db import transaction

def login(request):
    # With AUTO_COMMIT=True and ATOMIC_REQUEST=False
# Data operation runs in its own transaction due to AUTO_COMMIT=True data_operation_standalone() # Open new transaction with context manager with transaction.atomic(): # Start transaction. # Failure of any operation, rollbacks other operations data_operation_1() data_operation_2() data_operation_3() # Commit transaction if all operation successful # Data operation runs in its own transaction due to AUTO_COMMIT=True data_operation_standalone2()

As you can see in listing 7-29, it's possible to generate a transaction inside a method without influencing its entire scope vs. atomic requests which run on the entire view method scope. In addition, Django transactions also support callbacks, where by you can run a task once a transaction is successful (i.e. it's committed).

Callbacks are supported through the on_commit() method which is also part the django.db.transaction package. The syntax for the on_commit() method is the following:

transaction.on_commit(only_after_success_operation)
transaction.on_commit(lambda: only_after_success_with_args('success'))

The argument to on_commit() method can be either a non-argument function to run after a successful transaction or a function wrapped in a lambda statement if the function to run after a successful transaction requires arguments.

The transaction.on_commit() method is triggered once the transaction in which the method was declared is successful. If the transaction running at the point where the transaction.on_commit() method is declared fails, the on_commit() callback is never called. If there's no transaction running at the point where the transaction.on_commit() method is declared, the on_commit() callback is trigged immediately.

Tip Use commit=False on a model's save() method -- described in table 7-3 -- to avoid a transaction (i.e. write operation) and still create a model object in-memory.
  1. https://docs.python.org/3/reference/datamodel.html#context-managers