带有 modelchoicefield 的 Django 多种形式 - >查询太多
Django multiple forms with modelchoicefield -> too many queries
我有 table 个相同 class 的表单,其中包含 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
ModelChoiceField
是 ChoiceField
的子类,其中 "normal" 选择被迭代器替换,迭代器将迭代提供的查询集。还有自定义的 'to_python' 方法将 return 实际对象而不是它的 pk。不幸的是,迭代器将为每个选择字段重置查询集并再次访问数据库,即使它们共享查询集
您需要做的是子类化 ChoiceField 并模仿 ModelChoiceField
的行为,但有一个区别:它将采用静态选择列表而不是查询集。您将为所有字段(或表单)在视图中构建一次该选择列表。
我按照 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
一个侵入性较小的 hack,使用 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
方法应使用现有的查询集,这会使黑客攻击更加黑客化。
这至少适用于 Django 1.11。
我有 table 个相同 class 的表单,其中包含 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
ModelChoiceField
是 ChoiceField
的子类,其中 "normal" 选择被迭代器替换,迭代器将迭代提供的查询集。还有自定义的 'to_python' 方法将 return 实际对象而不是它的 pk。不幸的是,迭代器将为每个选择字段重置查询集并再次访问数据库,即使它们共享查询集
您需要做的是子类化 ChoiceField 并模仿 ModelChoiceField
的行为,但有一个区别:它将采用静态选择列表而不是查询集。您将为所有字段(或表单)在视图中构建一次该选择列表。
我按照 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
一个侵入性较小的 hack,使用 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
方法应使用现有的查询集,这会使黑客攻击更加黑客化。
这至少适用于 Django 1.11。