[Django]-How to use transaction with "async" functions in Django?

4๐Ÿ‘

I had a similar problem. The difference was, that I needed to use an atomic transaction as an asynchronous context manager. After reading, that few people wrote it is impossible in the current version of Django 4.2.4. Started to investigate the code base myself.

The first thing I noticed, transaction.atomic() does not implement logic itself. It is just returning Atomic context decorator.
This decorator has 3 methods that are important to handle:

  • __init__ atomic() passes arguments to it, so we have to handle them.
  • __enter__ and __exit__ which are important for context manager to work.
class Atomic(ContextDecorator):
    ...
    def __init__(self, using, savepoint, durable):
    ...
    def __enter__(self):
    ...
    def __exit__(self, exc_type, exc_value, traceback):
    ...

def atomic(using=None, savepoint=True, durable=False):
    # Bare decorator: @atomic -- although the first argument is called
    # `using`, it's actually the function being decorated.
    if callable(using):
        return Atomic(DEFAULT_DB_ALIAS, savepoint, durable)(using)
    # Decorator: @atomic(...) or context manager: with atomic(...): ...
    else:
        return Atomic(using, savepoint, durable)

For asynchronous context managers to work, we need to implement __aenter__ and __aexit__. With this in mind, I came up with working solution:

from django.db.transaction import Atomic
from asgiref.sync import sync_to_async

class AsyncAtomicContextManager(Atomic):
    def __init__(self, using=None, savepoint=True, durable=False):
        super().__init__(using, savepoint, durable)

    async def __aenter__(self):
        await sync_to_async(super().__enter__)()
        return self

    async def __aexit__(self, exc_type, exc_value, traceback):
        await sync_to_async(super().__exit__)(exc_type, exc_value, traceback)

Which can be used as a standard async context manager:

async def test():
    async with AsyncAtomicContextManager():
        ...

Then you can make a decorator from this:

def aatomic(fun, *args, **kwargs):
    async def wrapper():
        async with AsyncAtomicContextManager():
            await fun(*args, **kwargs)
    return wrapper

@aatomic
async def test():
    ...
๐Ÿ‘คcozajeden

2๐Ÿ‘

I found the documentation of Django 4.1 says below:

Transactions do not yet work in async mode. If you have a piece of code that needs transactions behavior, we recommend you write that piece as a single synchronous function and call it using sync_to_async().

So, @transaction.atomic() cannot be used with async functions in the order version Django 3.1.7 as well.

Leave a comment