what is it?

a unit of work that is either committed (applied to the database) or rolled back (undone from the database)

default django behaviour

autocommit: each query starts a transaction and either commits or rolls back the transaction as well. If you have a view with three queries, then each will run one-by-one. If one fails, the other two will still be committed

def test_view(request):
    user = User.objects.create_user('john', '[email protected]', 'johnpassword')
    logger.info(f'create user {user.pk}')
    raise Exception('test')

in the above, the user is still created, even though an exception was thrown

changing behaviour to atomic

atomic allows us to create a block of code within which the atomicity on the database is guaranteed. If the block of code is successfully completed, the changes are committed to the database. If there is an exception, the changes are rolled back.

from django.db import transaction
 
def transaction_test(request):
    with transaction.atomic():
        user = User.objects.create_user('john1', '[email protected]', 'johnpassword')
        logger.info(f'create user {user.pk}')
        raise Exception('force transaction to rollback')

the user create operation will roll back when the exception is raised, so the user will not be created in the end

decorators

@transaction.atomic
def transaction_test2(request):
    user = User.objects.create_user('john1', '[email protected]', 'johnpassword')
    logger.info(f'create user {user.pk}')
    raise Exception('force transaction to rollback')

to ensure a method runs in autocommit mode: @transaction.non_atomic_requests

configure globally

in settings.py:

settings.py
ATOMIC_REQUESTS=True
 
# or
 
DATABASES["default"]["ATOMIC_REQUESTS"] = True

waiting for an atomic commit to complete

on_commit() allows you to register callbacks that will be executed after the open transaction is successfully committed

views.py
from django.db import transaction
from functools import partial
 
@transaction.atomic
def transaction_celery(request):
    username = random_username()
    user = User.objects.create_user(username, '[email protected]', 'johnpassword')
    logger.info(f'create user {user.pk}')
    # the task does not get called until after the transaction is committed
    transaction.on_commit(partial(task_send_welcome_email.delay, user.pk))
 
    time.sleep(1)
    return HttpResponse('test')

partial is used (instead of lambda) to bind the user.pk to the task_send_welcome_email.delay function, this can help avoid the late binding bug