使用Django .only()查询模型时触发无限循环的问题排查
问题描述
使用Django的.only()方法查询PodcastEpisode模型时,模型__init__方法触发无限递归循环。注释掉初始化_original_audios和_published的代码后循环消失,即便在.only()中尝试包含audio_metadata字段也无法解决问题。
模型代码
class PodcastEpisode(models.Model): audio_metadata = models.JSONField(null=True) featured_artists = models.ManyToManyField(to=User, related_name='featured_artists') podcast_series = models.ForeignKey(to=PodcastSeries, on_delete=models.CASCADE, null=False) published = models.BooleanField(default=False) published_at = models.DateTimeField(blank=True, null=True) _original_audios = None # 存储初始数据 _published = None def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) # self._original_audios = self.audio_metadata # self._published = self.published
查询语句
PodcastEpisode.objects\ .filter(id__in=id_list).prefetch_related(*prefetches).only(*['id' 'audio_metadata'])
错误堆栈
File "/Users/dev/Desktop/dev/Podsack_backend/mediacontent/models.py", line 152, in __init__ self._original_audios = self.audio_metadata ^^^^^^^^^^^^^^^^^^^ File "/Users/dev/Library/Caches/pypoetry/virtualenvs/narratave-9vDc9ea1-py3.11/lib/python3.11/site-packages/django/db/models/query_utils.py", line 182, in __get__ instance.refresh_from_db(fields=[field_name]) File "/Users/dev/Library/Caches/pypoetry/virtualenvs/narratave-9vDc9ea1-py3.11/lib/python3.11/site-packages/django/db/models/base.py", line 707, in refresh_from_db ).filter(pk=self.pk) ^^^^^^^^^^^^^^^^^^ File "/Users/dev/Library/Caches/pypoetry/virtualenvs/narratave-9vDc9ea1-py3.11/lib/python3.11/site-packages/django/db/models/manager.py", line 85, in manager_method return getattr(self.get_queryset(), name)(*args, **kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/Users/dev/Library/Caches/pypoetry/virtualenvs/narratave-9vDc9ea1-py3.11/lib/python3.11/site-packages/django/db/models/query.py", line 1420, in filter return self._filter_or_exclude(False, args, kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/Users/dev/Library/Caches/pypoetry/virtualenvs/narratave-9vDc9ea1-py3.11/lib/python3.11/site-packages/django/db/models/query.py", line 1438, in _filter_or_exclude clone._filter_or_exclude_inplace(negate, args, kwargs) File "/Users/dev/Library/Caches/pypoetry/virtualenvs/narratave-9vDc9ea1-py3.11/lib/python3.11/site-packages/django/db/models/query.py", line 1445, in _filter_or_exclude_inplace self._query.add_q(Q(*args, **kwargs)) File "/Users/dev/Library/Caches/pypoetry/virtualenvs/narratave-9vDc9ea1-py3.11/lib/python3.11/site-packages/django/db/models/sql/query.py", line 1532, in add_q clause, _ = self._add_q(q_object, self.used_aliases) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/Users/dev/Library/Caches/pypoetry/virtualenvs/narratave-9vDc9ea1-py3.11/lib/python3.11/site-packages/django/db/models/sql/query.py", line 1562, in _add_q child_clause, needed_inner = self.build_filter( ^^^^^^^^^^^^^^^^^^ File "/Users/dev/Library/Caches/pypoetry/virtualenvs/narratave-9vDc9ea1-py3.11/lib/python3.11/site-packages/django/db/models/sql/query.py", line 1478, in build_filter condition = self.build_lookup(lookups, col, value) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/Users/dev/Library/Caches/pypoetry/virtualenvs/narratave-9vDc9ea1-py3.11/lib/python3.11/site-packages/django/db/models/sql/query.py", line 1289, in build_lookup lookup_class = lhs.get_lookup(lookup_name) ^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/Users/dev/Library/Caches/pypoetry/virtualenvs/narratave-9vDc9ea1-py3.11/lib/python3.11/site-packages/django/db/models/expressions.py", line 377, in get_lookup return self.output_field.get_lookup(lookup) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/Users/dev/Library/Caches/pypoetry/virtualenvs/narratave-9vDc9ea1-py3.11/lib/python3.11/site-packages/django/db/models/query_utils.py", line 216, in get_lookup found = self._get_lookup(lookup_name) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/Users/dev/Library/Caches/pypoetry/virtualenvs/narratave-9vDc9ea1-py3.11/lib/python3.11/site-packages/django/db/models/query_utils.py", line 203, in _get_lookup return cls.get_lookups().get(lookup_name, None) ^^^^^^^^^^^^^^^^^ RecursionError: maximum recursion depth exceeded in comparison
问题根源
- 查询参数错误:你的
.only()调用中,['id' 'audio_metadata']缺少逗号,Python会将其解析为字符串拼接,实际传递的是idaudio_metadata这个不存在的字段,导致真正需要的audio_metadata和published都没被加载,处于延迟加载状态。 __init__中访问延迟加载字段:当在__init__中访问未加载的字段时,Django会自动调用refresh_from_db重新查询数据库,而这个查询会创建新的模型实例,再次触发__init__,进而再次访问字段、触发查询,形成无限循环。
解决方案
1. 修复.only()参数错误
确保字段列表用逗号分隔,或者直接传递字段名作为参数:
# 正确写法1:直接传递字段名 PodcastEpisode.objects\ .filter(id__in=id_list).prefetch_related(*prefetches).only('id', 'audio_metadata', 'published') # 正确写法2:列表内加逗号 PodcastEpisode.objects\ .filter(id__in=id_list).prefetch_related(*prefetches).only(*['id', 'audio_metadata', 'published'])
2. 安全初始化原始值的正确方式
避免在__init__中直接访问模型字段,推荐以下几种方案:
方案一:使用post_init信号
实例初始化完成后再设置原始值,避免触发延迟加载:
from django.db.models.signals import post_init from django.dispatch import receiver class PodcastEpisode(models.Model): # ... 模型字段 ... _original_audios = None _published = None @receiver(post_init, sender=PodcastEpisode) def set_original_values(sender, instance, **kwargs): instance._original_audios = instance.audio_metadata instance._published = instance.published
方案二:懒加载原始值
第一次使用时才初始化,避免不必要的字段访问:
class PodcastEpisode(models.Model): # ... 模型字段 ... _original_audios = None _published = None @property def original_audios(self): if self._original_audios is None: self._original_audios = self.audio_metadata return self._original_audios @property def original_published(self): if self._published is None: self._published = self.published return self._published
方案三:重写refresh_from_db
如果需要在实例从数据库刷新后重新同步原始值:
class PodcastEpisode(models.Model): # ... 模型字段 ... _original_audios = None _published = None def refresh_from_db(self, *args, **kwargs): super().refresh_from_db(*args, **kwargs) self._original_audios = self.audio_metadata self._published = self.published
3. 注意事项
- 永远不要在模型的
__init__方法中访问数据库字段,除非你能确保该字段已经被加载。 - 使用
.only()/.defer()时,明确列出所有需要访问的字段,避免触发意外的延迟加载。
内容的提问来源于stack exchange,提问作者suvodipMondal
相关产品推荐
相关产品推荐

