【问题标题】:Django ModelFormSet executes a single-object SELECT query per formset instance when validating (ORM inefficiency)Django ModelFormSet 在验证时对每个 formset 实例执行一个单对象 SELECT 查询(ORM 效率低下)
【发布时间】:2021-03-16 21:37:12
【问题描述】:

问题的概念总结:

  • 假设我们有一个带有AuthorBook 模型的Django 应用,并使用BookFormSet 添加/修改/删除属于给定Author 的书籍。
  • 问题是当BookFormSet 被验证时,ModelChoiceField.to_python() 最终会调用self.queryset.get(id=123),这会导致表单集中每本书的单对象SELECT查询。
  • 这意味着如果我想更新 15 本书,Django 会执行 15 次单独的 SELECT 查询,这似乎非常低效。 (我们的实际应用是一个编辑器,可以在单个表单集中更新任意数量的对象,例如 50 多个)。

我尝试了以下几点:

  • 首先我尝试将查询集传递给BookFormSet,即formset = BookFormSet(data=request.POST, queryset=Book.objects.filter(author=1)),但ModelChoiceField 仍然执行其单对象SELECT 查询。
  • 然后我试图查看ModelChoiceField 定义其查询集的位置,它似乎在BaseModelFormSet.add_fields() 中。我尝试使用传递给表单集的相同查询集启动ModelChoiceField,例如Book.objects.filter(author=1) 而不是原来的代码 Book._default_manager.get_queryset()。但这无济于事,因为我定义的新查询集实际上并没有链接到传递给表单集和之前评估的内容。所以仍然会发生多个 SELECT 查询。 (注意:我意识到model._default_manager.get_queryset() 可能在表单集可用于将一个模型实例切换到另一个可能不在传递给BaseModelFormset 的原始查询集中的实例的情况下是必要的,但这不是我们的用例)
  • 我注意到BaseModelFormSet._existing_object() 实际上提供了一种方法来检查对象是否存在于提供给表单集构造函数的查询集中,这意味着查询集最多被评估一次,结果存储在BaseModelFormSet._object_dict 中。我认为可能有一些方法可以让ModelChoiceField.to_python() 在调用self.queryset.get(id=123) 之前进行类似的检查,但我认为ModelChoiceField 不知道BaseModelFormSet,这似乎是一种反模式像这样的层次结构。

在我看来,最简单的解决方案是以某种方式将 BaseModelFormSet._object_dict 传递给创建的每个 ModelForm,然后允许 ModelChoiceField 在进行另一个 SELECT 查询之前检查此 _object_dict

附录:

这是来自 Django-Debug-Toolbar 的屏幕截图,显示了第一个 SELECT 查询(这是传递给 BaseModelFormSet 的查询集),随后是 4 个单独的 SELECT 查询(每个 form.instance 一个)

注意:我还在此处将其作为 Django 票发布: https://code.djangoproject.com/ticket/32244#comment:1

即使可以通过某种形式的缓存来解决这个问题,它仍然看起来仍然是低效的应用层逻辑。

【问题讨论】:

  • 编辑:我认为这可能是一个更深层次的 Django ORM 问题,如果你有qs = Book.objects.filter(author=1)(假设这会返回 3 本书,id = 1,2,3),然后评估它-- 即len(list(qs)) -- 然后尝试book2 = qs.get(id=2) 这仍然会运行 SELECT 查询,即使在评估原始查询集时已经获取了 book2

标签: django orm django-forms formset


【解决方案1】:

我想出了一个非常老套的解决方法,它可以保留我们需要的表单集功能。基本上在BookForm 你可以覆盖ModelForm._clean_fields()

def _clean_fields(self):
        # remove 'id' field so it's not cleaned 
        # (this is the ModelChoiceField that's generating an extra SELECT query per Book object in the formset during clean)
        id_field = self.fields.pop('id')

        # run normal cleaning, with the form now unaware of its own 'id' field
        super(BookForm, self)._clean_fields()

        # add 'id' field back and insert it into clean_data
        id_value = id_field.widget.value_from_datadict(self.data, self.files, self.add_prefix('id'))
        self.cleaned_data['id'] = id_value

        # add the 'id' field back into the form
        self.fields['id'] = id_field

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2021-10-10
    • 1970-01-01
    • 2016-03-24
    • 1970-01-01
    • 1970-01-01
    • 2018-07-23
    • 2012-06-18
    相关资源
    最近更新 更多