Как использовать транзакцию с функциями "async" в Django?
Когда async def call_test(request):
вызывается async def test():
как показано ниже (я использую Django==3.1.7):
async def test():
for _ in range(0, 3):
print("Test")
async def call_test(request):
await test() # Here
return HttpResponse("Call_test")
Ошибки нет, на консоли отображается правильный результат:
Test
Test
Test
Но, когда я накладываю @transaction.atomic()
на async def test():
, как показано ниже:
@transaction.atomic() # Here
async def test():
for _ in range(0, 3):
print("Test")
# ...
Произошла следующая ошибка:
django.core.exceptions.SynchronousOnlyOperation: Вы не можете вызвать это из асинхронного контекста - используйте поток или sync_to_async.
Итак, я поместил @sync_to_async
на @transaction.atomic()
, как показано ниже:
@sync_to_async # Here
@transaction.atomic()
async def test():
for _ in range(0, 3):
print("Test")
# ...
Но произошла другая ошибка, описанная ниже:
RuntimeWarning: coroutine 'test' was never awaited handle = None # Необходим для разрыва циклов при возникновении исключения. RuntimeWarning: Включите tracemalloc для получения трассировки выделения объекта
Итак, как я могу использовать транзакцию с async
функциями в Django?
Я нашел в документации Django 4.1 написано следующее:
Транзакции пока не работают в асинхронном режиме. Если у вас есть часть кода, которой требуется поведение транзакций, мы рекомендуем вам написать эту часть как отдельную синхронную функцию и вызвать ее с помощью sync_to_async().
Так, @transaction.atomic()
нельзя использовать с функциями async
в версии порядка Django 3.1.7, а также
У меня была похожая проблема. Разница была в том, что мне нужно было использовать атомарную транзакцию в качестве асинхронного менеджера контекста. Прочитав, что несколько человек писали, что это невозможно в текущей версии Django 4.2.4
. Начал самостоятельно исследовать кодовую базу
Первое, что я заметил, transaction.atomic()
не реализует логику самостоятельно. Он просто возвращает декоратор контекста Atomic
.
У этого декоратора есть 3 метода, которые важно обработать:
__init__
atomic() передает ему аргументы, поэтому мы должны их обработать.__enter__
и__exit__
, которые важны для работы контекстного менеджера.
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)
Для работы асинхронных менеджеров контекста нам необходимо реализовать __aenter__
и __aexit__
. Исходя из этого, я придумал рабочее решение:
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)
Который можно использовать как стандартный асинхронный менеджер контекста:
async def test():
async with AsyncAtomicContextManager():
...
Тогда вы можете сделать декоратор из этого:
def aatomic(fun, *args, **kwargs):
async def wrapper():
async with AsyncAtomicContextManager():
await fun(*args, **kwargs)
return wrapper
@aatomic
async def test():
...