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.