Django与modelchoicefield的多种形式 - >查询太多了

时间:2015-08-18 21:32:54

标签: django django-forms django-queryset overhead

我有一个包含ModelChoiceField的同一个类的表格。并且一行中的每个表单对此字段具有相同的查询集。问题是,每次渲染表单时,都会出现一个新的查询,无法忍受查询的数量。

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

SELECT users.* from users
left join  
(
    SELECT 
        users_follows.instagram_id_2 as 'instagram_id'
    from users
    inner join users_follows on users.instagram_id=users_follows.instagram_id_1
    where users.instagram_id = 'insta123'
) as followers on users.instagram_id = followers.instagram_id
where followers.instagram_id is null
and users.instagram_id != 'insta123' # you can't follow yourself

3 个答案:

答案 0 :(得分:2)

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

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

答案 1 :(得分:1)

我按照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

答案 2 :(得分:-1)

使用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中描述的formset渲染模板。

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

这至少适用于Django 1.11。