【问题标题】:Django model_to_dict stack overflow crashDjango model_to_dict 堆栈溢出崩溃
【发布时间】:2020-06-04 15:36:31
【问题描述】:

在我的应用程序中,我为所有模型使用了一个通用的 Base 模型。我遇到了 CampusHall 模型无法解释的行为:

class Base(models.Model):
    id: int = models.AutoField(primary_key=True, editable=False)

    class Meta:
        abstract = True

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self._initial = self.as_dict

    ...
    @property
    def as_dict(self) -> dict:
        fields = [field.name for field in self._meta.fields]
        res = model_to_dict(self, fields=fields)
        return res
    ...

class Campus(Base):
    name: str = models.CharField(max_length=100)

    class Meta:
        verbose_name_plural = "campuses"

    def __str__(self):
        return self.name

class Hall(Base):
    name: str = models.CharField(max_length=100)
    campus: Campus = models.ForeignKey(Campus, on_delete=models.SET_NULL, null=True)

    def save(self, *args, **kwargs):
        if self.campus is not None and isinstance(self.campus, str):
            self.campus = Campus.objects.get_or_create(name=self.campus)[0]
        super().save(*args, **kwargs)

    def __str__(self):
        return f"{self.name}{f' ({self.campus})' if self.campus else ''}"

在管理控制台上,当我尝试删除某个 Campus 实例时,服务器崩溃了。这是(非常长的)跟踪的一小部分:

Fatal Python error: Cannot recover from stack overflow.
...
Current thread 0x00000bdc (most recent call first):
...
  File "P:\Python\Coursist\academic_helper\models\base.py", line 142 in as_dict
  File "P:\Python\Coursist\academic_helper\models\base.py", line 98 in __init__
  File "C:\Venvs\Coursist\lib\site-packages\django\db\models\base.py", line 512 in from_db
  File "C:\Venvs\Coursist\lib\site-packages\django\db\models\query.py", line 75 in __iter__
  File "C:\Venvs\Coursist\lib\site-packages\django\db\models\query.py", line 1261 in _fetch_all
  File "C:\Venvs\Coursist\lib\site-packages\django\db\models\query.py", line 258 in __len__
  File "C:\Venvs\Coursist\lib\site-packages\django\db\models\query.py", line 411 in get
  File "C:\Venvs\Coursist\lib\site-packages\django\db\models\base.py", line 627 in refresh_from_db
  File "C:\Venvs\Coursist\lib\site-packages\django\db\models\query_utils.py", line 139 in __get__
  File "C:\Venvs\Coursist\lib\site-packages\django\db\models\fields\__init__.py", line 931 in value_from_object
  File "C:\Venvs\Coursist\lib\site-packages\django\forms\models.py", line 93 in model_to_dict
  File "P:\Python\Coursist\academic_helper\models\base.py", line 142 in as_dict
  File "P:\Python\Coursist\academic_helper\models\base.py", line 98 in __init__
  File "C:\Venvs\Coursist\lib\site-packages\django\db\models\base.py", line 512 in from_db
  File "C:\Venvs\Coursist\lib\site-packages\django\db\models\query.py", line 75 in __iter__
  File "C:\Venvs\Coursist\lib\site-packages\django\db\models\query.py", line 1261 in _fetch_all
  File "C:\Venvs\Coursist\lib\site-packages\django\db\models\query.py", line 258 in __len__
  File "C:\Venvs\Coursist\lib\site-packages\django\db\models\query.py", line 411 in get
  File "C:\Venvs\Coursist\lib\site-packages\django\db\models\base.py", line 627 in refresh_from_db
  File "C:\Venvs\Coursist\lib\site-packages\django\db\models\query_utils.py", line 139 in __get__
  File "C:\Venvs\Coursist\lib\site-packages\django\db\models\fields\__init__.py", line 931 in value_from_object
  File "C:\Venvs\Coursist\lib\site-packages\django\forms\models.py", line 93 in model_to_dict
  File "P:\Python\Coursist\academic_helper\models\base.py", line 142 in as_dict
  File "P:\Python\Coursist\academic_helper\models\base.py", line 98 in __init__
  File "C:\Venvs\Coursist\lib\site-packages\django\db\models\base.py", line 512 in from_db
  File "C:\Venvs\Coursist\lib\site-packages\django\db\models\query.py", line 75 in __iter__
...

起初我认为这是HallCampus 之间的某种循环指向问题,但似乎并非如此(调试时,我看不到Campus 指向的任何指针Hall)。奇怪的是,如果我先删除所有Hall 实例,Campus 的删除会顺利进行。
我正在使用python 3.7Django 3.0.6

【问题讨论】:

    标签: python django python-3.x django-models stack-overflow


    【解决方案1】:

    经过一些调试,我想出了这个补丁:

    class Base(models.Model):
        id: int = models.AutoField(primary_key=True, editable=False)
    ...
        def __init__(self, *args, **kwargs):
            super().__init__(*args, **kwargs)
            if models.DEFERRED in args:
                # This means we delete it now, not interested in dict logic
                self._initial = dict()
            else:
                self._initial = self.as_dict
    ...
    

    不确定这是一个优雅/正确的解决方案,但它解决了这个错误,其他一切似乎都正常。
    如果其他人有更好的想法,我很乐意听到。

    【讨论】:

      【解决方案2】:

      您的 BaseModel 设计导致了这个问题,因为model_to_dict 将解决 *ToMany 关系并创建可能巨大的列表。管理员创建了自己的内存列表以向您显示您将要删除的所有相关模型,从而放大了这一点。

      您基本上是在模型的 init 方法中放置一个序列化的表示形式,您已经将其作为属性。如果这是您缓存值的方式,那么我建议您使用django.utils.functions.cached_property 并将机制更改为“首次访问时缓存”而不是“初始化时缓存”:

      from django.utils.functional import cached_property
      
      
      class Base(models.Model):
          id: int = models.AutoField(primary_key=True, editable=False)
      
          class Meta:
              abstract = True
      
          ...
          @cached_property
          def as_dict(self) -> dict:
              fields = [field.name for field in self._meta.fields]
              res = model_to_dict(self, fields=fields)
              return res
      
          @property
          def _initial(self) -> dict:
              # if you're set on the _initial name
              return self.as_dict
      

      【讨论】:

      • 你的推理是有道理的,但问题是我使用save 中的as_dict 方法来确定对象自创建以来是否已更改,方法是将其与@987654327 进行比较@ 状态。由于我并不真正关心性能问题,我想知道如何优雅地做到这一点。
      • 这取决于原因:如果您想在调用 save 时阻止保存,这是一个不好的模式。如果您想要回滚/历史/差异信息,那么也许temporal tables 可能是需要研究的东西。我已经研究了替代方案,但我也看不到方法,如果您需要它在模型级别工作(在表单级别,可以使用 has_changed() )。您可以做的一种优化是不遍历 *ToMany,但这可能会违反应用程序要求。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2011-06-24
      • 2019-05-18
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-11-28
      • 1970-01-01
      相关资源
      最近更新 更多