在 Django 表单中使用ModelChoiceField或ModelMultipleChoiceField时,有没有办法传入一组缓存的选项?目前,如果我通过queryset参数指定选项,则会导致数据库命中。
我想使用 memcached 缓存这些选择,并防止在显示具有此类字段的表单时对数据库进行不必要的访问。
在 Django 表单中使用ModelChoiceField或ModelMultipleChoiceField时,有没有办法传入一组缓存的选项?目前,如果我通过queryset参数指定选项,则会导致数据库命中。
我想使用 memcached 缓存这些选择,并防止在显示具有此类字段的表单时对数据库进行不必要的访问。
特别是在生成选择时产生命中的原因ModelChoiceField
- 无论之前是否填充了 QuerySet - 在于这一行
for obj in self.queryset.all():
在django.forms.models.ModelChoiceIterator
. 正如关于 QuerySets 缓存的 Django 文档强调的那样,
可调用属性每次都会导致数据库查找。
所以我宁愿只使用
for obj in self.queryset:
即使我不是 100% 确定这一切的影响(我知道我之后对查询集没有大计划,所以我认为没有副本.all()
创建我很好)。我很想在源代码中更改它,但是由于我将在下一次安装时忘记它(而且一开始的风格很糟糕),我最终编写了我的自定义ModelChoiceField
:
class MyModelChoiceIterator(forms.models.ModelChoiceIterator):
"""note that only line with # *** in it is actually changed"""
def __init__(self, field):
forms.models.ModelChoiceIterator.__init__(self, field)
def __iter__(self):
if self.field.empty_label is not None:
yield (u"", self.field.empty_label)
if self.field.cache_choices:
if self.field.choice_cache is None:
self.field.choice_cache = [
self.choice(obj) for obj in self.queryset.all()
]
for choice in self.field.choice_cache:
yield choice
else:
for obj in self.queryset: # ***
yield self.choice(obj)
class MyModelChoiceField(forms.ModelChoiceField):
"""only purpose of this class is to call another ModelChoiceIterator"""
def __init__(*args, **kwargs):
forms.ModelChoiceField.__init__(*args, **kwargs)
def _get_choices(self):
if hasattr(self, '_choices'):
return self._choices
return MyModelChoiceIterator(self)
choices = property(_get_choices, forms.ModelChoiceField._set_choices)
这并不能解决数据库缓存的一般问题,但是由于您ModelChoiceField
特别询问,而这正是让我首先想到缓存的原因,因此认为这可能会有所帮助。
您可以覆盖 QuerySet 中的“all”方法,例如
from django.db import models
class AllMethodCachingQueryset(models.query.QuerySet):
def all(self, get_from_cache=True):
if get_from_cache:
return self
else:
return self._clone()
class AllMethodCachingManager(models.Manager):
def get_query_set(self):
return AllMethodCachingQueryset(self.model, using=self._db)
class YourModel(models.Model):
foo = models.ForeignKey(AnotherModel)
cache_all_method = AllMethodCachingManager()
然后在使用表单之前更改字段的查询集(例如,当您使用表单集时)
form_class.base_fields['foo'].queryset = YourModel.cache_all_method.all()
这是我在 Django 1.10 中使用的一个小技巧,用于在表单集中缓存查询集:
qs = my_queryset
# cache the queryset results
cache = [p for p in qs]
# build an iterable class to override the queryset's all() method
class CacheQuerysetAll(object):
def __iter__(self):
return iter(cache)
def _prefetch_related_lookups(self):
return False
qs.all = CacheQuerysetAll
# update the forms field in the formset
for form in formset.forms:
form.fields['my_field'].queryset = qs
我在 Django Admin 中使用 InlineFormset 时也偶然发现了这个问题,它本身引用了另外两个模型。生成了许多不必要的查询,因为正如Nicolas87解释ModelChoiceIterator
的那样,每次都从头开始获取查询集。
可以将以下 Mixin 添加到admin.ModelAdmin
,admin.TabularInline
或admin.StackedInline
将查询数量减少到填充缓存所需的数量。缓存与Request
对象绑定,因此它在新请求时失效。
class ForeignKeyCacheMixin(object):
def formfield_for_foreignkey(self, db_field, request, **kwargs):
formfield = super(ForeignKeyCacheMixin, self).formfield_for_foreignkey(db_field, **kwargs)
cache = getattr(request, 'db_field_cache', {})
if cache.get(db_field.name):
formfield.choices = cache[db_field.name]
else:
formfield.choices.field.cache_choices = True
formfield.choices.field.choice_cache = [
formfield.choices.choice(obj) for obj in formfield.choices.queryset.all()
]
request.db_field_cache = cache
request.db_field_cache[db_field.name] = formfield.choices
return formfield
@jnns 我注意到在您的代码中,查询集被评估了两次(至少在我的 Admin 内联上下文中),这似乎是 django admin 的开销,即使没有这个 mixin(当你没有时,每个内联加上一次)这种混合)。
在这个 mixin 的情况下,这是因为 formfield.choices 有一个设置器(为了简化)触发对象的 queryset.all() 的重新评估
我提出了一项改进,其中包括直接处理 formfield.cache_choices 和 formfield.choice_cache
这里是:
class ForeignKeyCacheMixin(object):
def formfield_for_foreignkey(self, db_field, request, **kwargs):
formfield = super(ForeignKeyCacheMixin, self).formfield_for_foreignkey(db_field, **kwargs)
cache = getattr(request, 'db_field_cache', {})
formfield.cache_choices = True
if db_field.name in cache:
formfield.choice_cache = cache[db_field.name]
else:
formfield.choice_cache = [
formfield.choices.choice(obj) for obj in formfield.choices.queryset.all()
]
request.db_field_cache = cache
request.db_field_cache[db_field.name] = formfield.choices
return formfield
这是防止ModelMultipleChoiceField
从数据库中重新获取其查询集的另一种解决方案。当您有相同表单的多个实例并且不希望每个表单重新获取相同的查询集时,这很有帮助。此外,查询集是表单初始化的一个参数,例如允许您在视图中定义它。
请注意,这些类的代码同时发生了变化。此解决方案使用 Django 3.1 的版本。
此示例使用与 Django 的多对多关系Group
from django.contrib.auth.models import Group
from django.db import models
class Example(models.Model):
name = models.CharField(max_length=100, default="")
groups = models.ManyToManyField(Group)
...
from django.contrib.auth.models import Group
from django import forms
class MyModelChoiceIterator(forms.models.ModelChoiceIterator):
"""Variant of Django's ModelChoiceIterator to prevent it from always re-fetching the
given queryset from database.
"""
def __iter__(self):
if self.field.empty_label is not None:
yield ("", self.field.empty_label)
queryset = self.queryset
for obj in queryset:
yield self.choice(obj)
class MyModelMultipleChoiceField(forms.ModelMultipleChoiceField):
"""Variant of Django's ModelMultipleChoiceField to prevent it from always
re-fetching the given queryset from database.
"""
iterator = MyModelChoiceIterator
def _get_queryset(self):
return self._queryset
def _set_queryset(self, queryset):
self._queryset = queryset
self.widget.choices = self.choices
queryset = property(_get_queryset, _set_queryset)
class ExampleForm(ModelForm):
name = forms.CharField(required=True, label="Name", max_length=100)
groups = MyModelMultipleChoiceField(required=False, queryset=Group.objects.none())
def __init__(self, *args, **kwargs):
groups_queryset = kwargs.pop("groups_queryset", None)
super().__init__(*args, **kwargs)
if groups_queryset:
self.fields["groups"].queryset = groups_queryset
class Meta:
model = Example
fields = ["name", "groups"]
from django.contrib.auth.models import Group
from .forms import ExampleForm
def my_view(request):
...
groups_queryset = Group.objects.order_by("name")
form_1 = ExampleForm(groups_queryset=groups_queryset)
form_2 = ExampleForm(groups_queryset=groups_queryset)
form_3 = ExampleForm(groups_queryset=groups_queryset)
```
@lai 对于 Django 2.1.2,我必须将第一个 if 语句中的代码从jnns的答案中formfield.choice_cache = cache[db_field.name]
更改为 to 。在 Django 版本 2.1.2 中,如果您从那里继承,则可以直接覆盖该方法而无需使用 mixin。所以代码可能如下所示:formfield.choices = cache[db_field.name]
admin.TabularInline
formfield_for_foreignkey(self, db_field, request, **kwargs)
class MyInline(admin.TabularInline):
model = MyModel
formset = MyModelInlineFormset
extra = 3
def formfield_for_foreignkey(self, db_field, request, **kwargs):
formfield = super().formfield_for_foreignkey(db_field, request, **kwargs)
cache = getattr(request, 'db_field_cache', {})
formfield.cache_choices = True
if db_field.name in cache:
formfield.choices = cache[db_field.name]
else:
formfield.choice_cache = [
formfield.choices.choice(obj) for obj in formfield.choices.queryset.all()
]
request.db_field_cache = cache
request.db_field_cache[db_field.name] = formfield.choices
return formfield
就我而言,我还必须重写get_queryset
才能从中受益select_related
:
class MyInline(admin.TabularInline):
model = MyModel
formset = MyModelInlineFormset
extra = 3
def formfield_for_foreignkey(self, db_field, request, **kwargs):
formfield = super().formfield_for_foreignkey(db_field, request, **kwargs)
cache = getattr(request, 'db_field_cache', {})
formfield.cache_choices = True
if db_field.name in cache:
formfield.choices = cache[db_field.name]
else:
formfield.choice_cache = [
formfield.choices.choice(obj) for obj in formfield.choices.queryset.all()
]
request.db_field_cache = cache
request.db_field_cache[db_field.name] = formfield.choices
return formfield
def get_queryset(self, request):
return super().get_queryset(request).select_related('my_field')