Django Channels中@database_sync_to_async函数依赖print语句才能运行的问题
问题说明
用Django Channels开发功能时,遇到一个异常问题:被@database_sync_to_async装饰的get_driver_location函数,必须保留print(location)语句才能正常运行;一旦注释掉该print语句,就会抛出错误:You cannot call this from an async context - use a thread or sync_to_async.
相关代码
@database_sync_to_async def get_driver_data(self, data): driver = DriverAdditional.objects.get(user_id=data) return driver @database_sync_to_async def get_driver_location(self, data): location = CabLocation.objects.get(driver_id=data) # print(location) # 注释掉这行就触发报错 return location async def send_amount(self, text_data): print("send amount") user = self.scope['user'].id print("user", user) driver = await self.get_driver_data(user) message = json.loads(text_data) data = message.get('data') try: location = await self.get_driver_location(driver) print(location) except Exception as e: print(f'error is {e}') await self.channel_layer.group_send( group=data['rider'], message={ 'type': 'echo.message', 'message_type': 'send_amount', 'data': f"{driver, location} {data['amount']}", } )
移除print语句后的报错信息
Exception inside application: You cannot call this from an async context - use a thread or sync_to_async. Traceback (most recent call last): File "D:\Najam\June\development\backend\Lib\site-packages\django\db\models\fields\related_descriptors.py", line 218, in __get__ rel_obj = self.field.get_cached_value(instance) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "D:\Najam\June\development\backend\Lib\site-packages\django\db\models\fields\mixins.py", line 15, in get_cached_value return instance._state.fields_cache[cache_name] ~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^ KeyError: 'driver_id' During handling of the above exception, another exception occurred: Traceback (most recent call last): File "D:\Najam\June\development\backend\Lib\site-packages\django\contrib\staticfiles\handlers.py", line 101, in __call__ return await self.application(scope, receive, send) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "D:\Najam\June\development\backend\Lib\site-packages\channels\routing.py", line 62, in __call__ return await application(scope, receive, send) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "D:\Najam\June\development\backend\Lib\site-packages\channels\sessions.py", line 47, in __call__ return await self.inner(dict(scope, cookies=cookies), receive, send) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "D:\Najam\June\development\backend\Lib\site-packages\channels\sessions.py", line 263, in __call__ return await self.inner(wrapper.scope, receive, wrapper.send) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "D:\Najam\June\development\backend\Lib\site-packages\channels\auth.py", line 185, in __call__ return await super().__call__(scope, receive, send) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "D:\Najam\June\development\backend\Lib\site-packages\channels\middleware.py", line 24, in __call__ return await self.inner(scope, receive, send) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "D:\Najam\June\development\backend\Lib\site-packages\channels\routing.py", line 116, in __call__ return await application( ^^^^^^^^^^^^^^^^^^ File "D:\Najam\June\development\backend\Lib\site-packages\channels\consumer.py", line 94, in app return await consumer(scope, receive, send) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "D:\Najam\June\development\backend\Lib\site-packages\channels\consumer.py", line 58, in __call__ await await_many_dispatch( File "D:\Najam\June\development\backend\Lib\site-packages\channels\utils.py", line 50, in await_many_dispatch await dispatch(result) File "D:\Najam\June\development\backend\Lib\site-packages\channels\consumer.py", line 73, in dispatch await handler(message) File "D:\Najam\June\development\backend\Lib\site-packages\channels\generic\websocket.py", line 194, in websocket_receive await self.receive(text_data=message["text"]) File "D:\Najam\June\development\baba_drive_project_backend\app\rides\consumers.py", line 399, in receive await self.send_amount(text_data) File "D:\Najam\June\development\baba_drive_project_backend\app\rides\consumers.py", line 443, in send_amount 'data': f"{driver,location} {data['amount']}", ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "D:\Najam\June\development\backend\Lib\site-packages\django\db\models\base.py", line 588, in __repr__ return "<%s: %s>" % (self.__class__.__name__, self) ^^^^ File "D:\Najam\June\development\baba_drive_project_backend\app\location\models.py", line 29, in __str__ return f'{self.driver_id.name}' ^^^^^^^^^^^^^^ File "D:\Najam\June\development\backend\Lib\site-packages\django\db\models\fields\related_descriptors.py", line 236, in __get__ rel_obj = self.get_object(instance) ^^^^^^^^^^^^^^^^^^^^^^^^^ File "D:\Najam\June\development\backend\Lib\site-packages\django\db\models\fields\related_descriptors.py", line 366, in get_object return super().get_object(instance) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "D:\Najam\June\development\backend\Lib\site-packages\django\db\models\fields\related_descriptors.py", line 199, in get_object return qs.get(self.field.get_reverse_related_filter(instance)) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "D:\Najam\June\development\backend\Lib\site-packages\django\db\models\query.py", line 633, in get num = len(clone) ^^^^^^^^^^ File "D:\Najam\June\development\backend\Lib\site-packages\django\db\models\query.py", line 380, in __len__ self._fetch_all() File "D:\Najam\June\development\backend\Lib\site-packages\django\db\models\query.py", line 1881, in _fetch_all self._result_cache = list(self._iterable_class(self)) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "D:\Najam\June\development\backend\Lib\site-packages\django\db\models\query.py", line 91, in __iter__ results = compiler.execute_sql( ^^^^^^^^^^^^^^^^^^^^^ File "D:\Najam\June\development\backend\Lib\site-packages\django\db\models\sql\compiler.py", line 1560, in execute_sql cursor = self.connection.cursor() ^^^^^^^^^^^^^^^^^^^^^^^^ File "D:\Najam\June\development\backend\Lib\site-packages\django\utils\asyncio.py", line 24, in inner raise SynchronousOnlyOperation(message) django.core.exceptions.SynchronousOnlyOperation: You cannot call this from an async context - use a thread or sync_to_async.
原因分析
从报错栈可以定位到问题根源:
在group_send的data字段格式化时,f"{driver,location} {data['amount']}"会调用location实例的__str__方法。而CabLocation模型的__str__方法里访问了self.driver_id.name,这会触发Django的懒加载机制——也就是在异步上下文里执行了同步的数据库查询,直接违反了Django的异步操作规则。
添加print(location)时,print会在@database_sync_to_async装饰的同步线程中调用location的__str__,此时懒加载的数据库查询是合法的,并且会把driver_id的数据缓存到实例中。后续在异步上下文里格式化字符串时,直接使用缓存值,不会触发新的查询,因此不会报错。
解决方案
1. 预加载关联对象(推荐)
修改get_driver_location函数,用select_related提前加载关联的driver_id数据,避免后续懒加载:
@database_sync_to_async def get_driver_location(self, data): # 预加载关联的driver_id数据,避免后续异步环境中触发查询 location = CabLocation.objects.select_related('driver_id').get(driver_id=data) return location
2. 在同步线程中处理字符串格式化
提前在同步函数中生成需要的字符串,避免在异步上下文里调用模型的__str__:
@database_sync_to_async def get_driver_location(self, data): location = CabLocation.objects.get(driver_id=data) # 提前生成格式化后的字符串 return (location, f"{location}") async def send_amount(self, text_data): # ... 其他代码 ... try: location, location_str = await self.get_driver_location(driver) except Exception as e: print(f'error is {e}') await self.channel_layer.group_send( group=data['rider'], message={ 'type': 'echo.message', 'message_type': 'send_amount', 'data': f"{driver} {location_str} {data['amount']}", } )
3. 修改模型__str__方法(临时方案)
修改CabLocation的__str__,避免触发懒加载,但会丢失部分信息,仅作临时应急:
class CabLocation(models.Model): # ... 其他字段定义 ... def __str__(self): try: return f'{self.driver_id.name}' except: # 无法获取关联数据时返回基础标识 return f'CabLocation {self.pk}'
内容的提问来源于stack exchange,提问作者najam iqbal

