Синхронизация с Async Django ORM queryset foreign key property
Казалось бы, простая ситуация: Django модель имеет внешний ключ:
class Invite(models.Model):
inviter = models.ForeignKey(User, on_delete=models.CASCADE)
...
В контексте async я делаю:
# get invite with sync_to_async decorator, then
print(invite.inviter)
Получение любимой ошибки async:
You cannot call this from an async context - use a thread or sync_to_async
print(sync_to_async(invite.inviter)) # -> throws the same error
Конечно, я могу сделать:
@sync_to_async
def get_inviter(self, invite):
return invite.inviter
Но это старческий маразм, если я должен делать это для каждого вызова свойства queryset.
Есть ли разумный способ справиться с этим?
Возможно, есть способ сделать это для всех подобных вызовов сразу?
Да, разрешите дополнительные поля, используя select_related
:
# Good: pick the foreign_key fields using select_related
user = await Invite.objects.select_related('user').aget(key=key).user
Ваши другие строковые неиностранные атрибуты, такие как strings и ints, уже должны существовать. уже существуют в модели.
Не будут работать, (хотя им кажется, что они должны работать)
# Error django.core.exceptions.SynchronousOnlyOperation ... use sync_to_async
user = await Model.objects.aget(key=key).user
# Error (The field is actually missing from the `_state` fields cache.
user = await sync_to_async(Invite.objects.get)(key=key).user
Другие примеры для исследования
Стандартный aget
, за которым следует проверка внешнего ключа, дает ошибку SynchronousOnlyOperation
.
У меня есть строка key
, и ForeignKey user
к стандартной модели пользователя.
class Invite(models.Model):
user = fields.user_fk()
key = fields.str_uuid()
Пример с альтернативами, которые в основном не работают:
Invite = get_model('invites.Invite')
User = get_user_model()
def _get_invite(key):
return Invite.objects.get(key=key)
async def invite_get(self, key):
# (a) works, the related field is populated on response.
user = await Invite.objects.select_related('user').aget(key=key).user
async def intermediate_examples(self, key):
# works, but is clunky.
user_id = await Invite.objects.aget(key=key).user_id
# The `user_id` (any `_id` key) exists for a FK
user = await User.objects.aget(id=user_id)
async def failure_examples(self, key):
# (b) does not work.
user = await sync_to_async(Invite.objects.get)(key=key).user
invite = await sync_to_async(Invite.objects.get)(key=key)
# (c) these are not valid, although the error may say so.
user = await invite.user
user = await sync_to_async(invite.user)
# same as the example (b)
get_invite = sync_to_async(_get_invite, thread_sensitive=True)
invite = get_invite(key)
user = invite.user # Error
# (d) Does not populate the additional model
user = await Invite.objects.aget(key=key).user # Error
print(sync_to_async(invite.inviter)) # -> throws the same error
Это потому, что это эквивалентно:
i = invite.inviter # -> throws the error here
af = sync_to_async(i)
print(af)
Правильное использование:
f = lambda: invite.inviter
af = sync_to_async(f)
i = await af()
print(i)
# As a one-liner
print(await sync_to_async(lambda: invite.inviter)())
Есть ли разумный способ справиться с этим?
Возможно, есть способ сделать это для всех подобных вызовов сразу?
(Отказ от ответственности: не проверено на производстве.)
С помощью nest_asyncio можно сделать следующее:
def do(f):
import nest_asyncio
nest_asyncio.apply()
return asyncio.run(sync_to_async(f)())
print(do(lambda: invite.inviter))
Или пойти еще дальше:
class SynchronousOnlyAttributeHandler:
def __getattribute__(self, item):
from django.core.exceptions import SynchronousOnlyOperation
try:
return super().__getattribute__(item)
except SynchronousOnlyOperation:
from asgiref.sync import sync_to_async
import asyncio
import nest_asyncio
nest_asyncio.apply()
return asyncio.run(sync_to_async(lambda: self.__getattribute__(item))())
class Invite(models.Model, AsyncUnsafeAttributeHandler):
inviter = models.ForeignKey(User, on_delete=models.CASCADE, null=True)
...
# Do this even in async context
print(invite.inviter)
Работает ли что-то подобное? Вместо invite.inviter
вы делаете await async_resolve_attributes(invite, "inviter")
@sync_to_async
def async_resolve_attributes(instance, *attributes):
current_instance = instance
for attribute in attributes:
current_instance = getattr(current_instance, attribute)
resolved_attribute = current_instance
return resolved_attribute