You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用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
问题根源
  1. 查询参数错误:你的.only()调用中,['id' 'audio_metadata']缺少逗号,Python会将其解析为字符串拼接,实际传递的是idaudio_metadata这个不存在的字段,导致真正需要的audio_metadata和published都没被加载,处于延迟加载状态。
  2. __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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.12 00:24:50