在Django表单中缓存ModelChoiceField或ModelMultipleChoiceField的查询集选项

时间:2011-11-18 00:12:49

标签: django django-forms django-admin django-queryset django-cache

在Django表单中使用 ModelChoiceField ModelMultipleChoiceField 时,有没有办法传入一组缓存的选项?目前,如果我通过 queryset 参数指定选项,则会导致数据库命中。

我想使用memcached缓存这些选项,并在显示包含此类字段的表单时防止对数据库的不必要的命中。

7 个答案:

答案 0 :(得分:13)

你可以覆盖"所有" QuerySet中的方法

之类的东西
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()

答案 1 :(得分:12)

ModelChoiceField特别是在生成选择时创建命中的原因 - 无论先前是否填充了QuerySet - 都在此行中

for obj in self.queryset.all(): 
django.forms.models.ModelChoiceIterator中的

。由于Django documentation on caching of QuerySets突出显示,

  

可调用属性每次都会导致数据库查找。

所以我更愿意使用

for obj in self.queryset:

即使我不是百分之百地确定这一点的所有含义(我知道之后我对查询集没有大的计划,所以我认为没有副本.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,而这正是让我首先考虑缓存的原因,认为这可能会有所帮助。

答案 2 :(得分:3)

这是我用Django 1.10缓存查询集在formset中的一个小黑客:

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

答案 3 :(得分:2)

我在Django Admin中使用InlineFormset时偶然发现了这个问题,而Django Admin本身也引用了其他两个模型。生成了许多不必要的查询,因为正如Nicolas87所解释的那样,ModelChoiceIterator每次都从头开始提取查询集。

可以将以下Mixin添加到admin.ModelAdminadmin.TabularInlineadmin.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

答案 4 :(得分:2)

@jnns我注意到在您的代码中,查询集被评估两次(至少在我的Admin内联上下文中),这似乎是django admin的开销,即使没有这个mixin(当你不穿时,每个内联加一次)没有这种混合。

对于这个mixin,这是因为formfield.choices有一个setter(为了简化)触发了对象的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

答案 5 :(得分:0)

@lai在Django 2.1.2中,我不得不将第一个if语句中的代码从formfield.choice_cache = cache[db_field.name]更改为formfield.choices = cache[db_field.name],如jnns的答案一样。在Django版本2.1.2中,如果您继承自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')

答案 6 :(得分:0)

这是防止 ModelMultipleChoiceField 从数据库重新获取查询集的另一种解决方案。当您有多个相同表单的实例并且不希望每个表单重新获取相同的查询集时,这会很有帮助。此外,查询集是表单初始化的一个参数,允许您例如在您的视图中定义它。

请注意,这些类的代码在此期间已更改。此解决方案使用 Django 3.1 中的版本。

这个例子使用了多对多关系和 Django 的 Group

models.py
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)
    ...

forms.py
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"]

views.py
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)
    ```