【问题标题】:Django multiple forms with modelchoicefield -> too many queries带有modelchoicefield的Django多个表单->查询太多
【发布时间】:2015-11-12 00:17:07
【问题描述】:

我有一个包含 ModelChoiceField 的同一类的表格。并且一行中的每个表单都对该字段具有相同的查询集。问题是每次呈现表单时,都是一个新的查询,查询的数量增加得难以忍受。

我想出的唯一解决方案是使用 js 随时随地构建表单,而不是让 django 自己渲染。有没有办法缓存这些查询集或一次预加载它?

views.py:
shift_table=[]
for project in calendar_projects:
    shift_table.append([])
    project_branches = project.branches.all()
    for i, week in enumerate(month):
        for day in week:
            shift_table[-1].append(
                CreateShiftCalendarForm(initial={'date': day}, branch_choices=project_branches))


forms.py:
CreateShiftCalendarForm(EditShiftCalendarForm):

    class Meta(ShiftForm.Meta):
        fields = ('project_branch', 'date') + ShiftForm.Meta.fields
        widgets = {'date': forms.HiddenInput(), 'length': forms.NumberInput(attrs={'step': 'any'}), 'project_branch': forms.Select()}

    def __init__(self, *args, **kwargs):
        branch_choices = kwargs.pop('branch_choices', ProjectBranch.objects.none())
        super(CreateShiftCalendarForm, self).__init__(*args, **kwargs)

        self.fields['project_branch'].queryset = branch_choices
        self.fields['project_branch'].empty_label = None

【问题讨论】:

  • 您能提供您的表单代码吗?
  • 你去@GwynBleidD

标签: django django-forms django-queryset overhead


【解决方案1】:

ModelChoiceFieldChoiceField 的子类,其中“正常”选择被替换为迭代器,该迭代器将遍历提供的查询集。还有自定义的“to_python”方法将返回实际对象而不是 pk。不幸的是,即使它们共享查询集,迭代器也会为每个选择字段重置查询集并再次命中数据库

您需要做的是子类 ChoiceField 并模仿 ModelChoiceField 的行为,但有一个区别:它将采用静态选择列表而不是查询集。您将在视图中为所有字段(或表单)构建一次选择列表。

【讨论】:

    【解决方案2】:

    我按照 GwynBleidD 的建议对 ChoiceField 进行了子类化,现在它已经足够工作了。

    class ListModelChoiceField(forms.ChoiceField):
        """
        special field using list instead of queryset as choices
        """
        def __init__(self, model, *args, **kwargs):
            self.model = model
            super(ListModelChoiceField, self).__init__(*args, **kwargs)
    
        def to_python(self, value):
    
            if value in self.empty_values:
                return None
            try:
                value = self.model.objects.get(id=value)
            except self.model.DoesNotExist:
                raise ValidationError(self.error_messages['invalid_choice'], code='invalid_choice')
            return value
    
    
        def valid_value(self, value):
            "Check to see if the provided value is a valid choice"
    
            if any(value.id == int(choice[0]) for choice in self.choices):
                return True
            return False
    

    【讨论】:

    • 这不会为value = self.model.objects.get(id=value) 的每次迭代执行查询/命中数据库吗?
    • 老实说,我已经不记得了……但我认为它有所帮助。关键是表格中的值已经存在。无论如何,这在当时是不好的方法,我今天永远不会构建整个表,而只是用 js 获取关注的数据。
    【解决方案3】:

    使用 Django 的 FormSets 的重载并保持基本表单不受影响(即保持 ModelChoiceFields 与其动态查询集):

    from django import forms
    
    class OptimFormSet( forms.BaseFormSet ):
        """
        FormSet with minimized number of SQL queries for ModelChoiceFields
        """
    
        def __init__( self, *args, modelchoicefields_qs=None, **kwargs ):
            """
            Overload the ModelChoiceField querysets by a common queryset per
            field, with dummy .all() and .iterator() methods to avoid multiple
            queries when filling the (repeated) choices fields.
    
            Parameters
            ----------
    
            modelchoicefields_qs :          dict
                Dictionary of modelchoicefield querysets. If ``None``, the
                modelchoicefields are identified internally
    
            """
    
            # Init the formset
            super( OptimFormSet, self ).__init__( *args, **kwargs )
    
            if modelchoicefields_qs is None and len( self.forms ) > 0:
                # Store querysets of modelchoicefields
                modelchoicefields_qs = {}
                first_form = self.forms[0]
                for key in first_form.fields:
                    if isinstance( first_form.fields[key], forms.ModelChoiceField ):
                        modelchoicefields_qs[key] = first_form.fields[key].queryset
    
            # Django calls .queryset.all() before iterating over the queried objects
            # to render the select boxes. This clones the querysets and multiplies
            # the queries for nothing.
            # Hence, overload the querysets' .all() method to avoid cloning querysets
            # in ModelChoiceField. Simply return the queryset itself with a lambda function.
            # Django also calls .queryset.iterator() as an optimization which
            # doesn't make sense for formsets. Hence, overload .iterator as well.
            if modelchoicefields_qs:
                for qs in modelchoicefields_qs.values():
                    qs.all = lambda local_qs=qs: local_qs  # use a default value of qs to pass from late to immediate binding (so that the last qs is not used for all lambda's)
                    qs.iterator = qs.all
    
                # Apply the common (non-cloning) querysets to all the forms
                for form in self.forms:
                    for key in modelchoicefields_qs:
                        form.fields[key].queryset = modelchoicefields_qs[key]
    

    在您看来,然后您调用:

    formset_class = forms.formset_factory( form=MyBaseForm, formset=OptimFormSet )
    formset = formset_class()
    

    然后使用Django's doc 中描述的表单集呈现您的模板。

    请注意,在表单验证中,每个 ModelChoiceField 实例仍然有 1 个查询,但每次仅限于一个主键值。接受的答案也是如此。为避免这种情况,to_python 方法应使用现有的查询集,这会使 hack 更加骇人。

    这至少适用于 Django 1.11。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2013-02-05
      • 2019-08-15
      • 1970-01-01
      • 1970-01-01
      • 2014-11-15
      • 1970-01-01
      相关资源
      最近更新 更多