在Django表单中缓存ModelChoiceField或ModelMultipleChiiceField的queryset选项

在Django表单中缓存ModelChoiceField或ModelMultipleChiiceField的queryset选项,django,django-forms,django-admin,django-queryset,django-cache,Django,Django Forms,Django Admin,Django Queryset,Django Cache,在Django表单中使用ModelChoiceField或ModelMultipleEchoiceField时,是否有方法传入一组缓存的选项?目前,如果我通过queryset参数指定选项,就会导致数据库命中 我希望使用memcached缓存这些选项,并防止在显示带有此类字段的表单时对数据库进行不必要的点击。在生成选项时,modelcoocefield特别是在生成选项时创建点击的原因(无论之前是否填充了查询集)位于这一行 for obj in self.queryset.all(): 在dja

在Django表单中使用ModelChoiceField或ModelMultipleEchoiceField时,是否有方法传入一组缓存的选项?目前,如果我通过queryset参数指定选项,就会导致数据库命中


我希望使用memcached缓存这些选项,并防止在显示带有此类字段的表单时对数据库进行不必要的点击。

在生成选项时,
modelcoocefield
特别是在生成选项时创建点击的原因(无论之前是否填充了查询集)位于这一行

for obj in self.queryset.all(): 
django.forms.models.modelcooiceiterator
中。作为亮点,

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

所以我宁愿只使用

for obj in self.queryset:
尽管我不能百分之百地确定这一点的所有含义(我知道我对之后的queryset没有什么大的计划,所以我认为没有拷贝
.all()
creates我很好)。我很想在源代码中对此进行更改,但由于我将在下次安装时忘记它(这是一个糟糕的开始),因此我最终编写了我的自定义
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()
然后在使用表单之前更改字段的queryset(例如,在使用表单集时)


我在Django管理中使用InlineFormset时也遇到了这个问题,它本身引用了另外两个模型。生成了大量不必要的查询,因为正如所解释的,
modelcooiceiterator
每次都从头开始获取查询集

可以将以下Mixin添加到
admin.ModelAdmin
admin.tablerinline
admin.StackedInline
,以将查询数量减少到只填充缓存所需的数量。缓存绑定到
请求
对象,因此它在新请求时无效

类ForeignKeyCacheMixin(对象):
def formfield_for_foreignkey(self、db_字段、request、**kwargs):
formfield=super(ForeignKeyCacheMixin,self)。formfield\u表示foreignkey(db\u字段,**kwargs)
cache=getattr(请求'db_field_cache',{})
if cache.get(db_field.name):
formfield.choices=缓存[db_field.name]
其他:
formfield.choices.field.cache_choices=True
formfield.choices.field.choice_缓存=[
formfield.choices.choice(obj)用于formfield.choices.queryset.all()中的obj
]
request.db_field_cache=cache
request.db\u field\u cache[db\u field.name]=formfield.choices
返回表单字段

@jnns我注意到,在您的代码中,queryset被计算了两次(至少在我的管理内联上下文中),这似乎是django Admin的开销,即使没有此混合(在没有此混合时,每个内联加一次)

在这个mixin的例子中,这是因为formfield.choices有一个setter,它(为了简化)触发了对对象的queryset.all()的重新计算

我提出了一个改进,包括直接处理formfield.cache\u选项和formfield.choice\u缓存

这是:

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

以下是我在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 2.1.2时,我必须将第一个if语句中的代码从
formfield.choice\u cache=cache[db\u field.name]
更改为
formfield.choices=cache[db\u field.name]
,与来自的答案相同。在Django版本2.1.2中,如果您继承自
admin.tablarinline
,则可以直接覆盖foreignkey(self、db\u字段、request、**kwargs)的方法
formfield\u
,而无需混入。因此,代码可以如下所示:

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
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')
在我的例子中,我还必须覆盖
get\u queryset
以从
select\u 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
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')

非常感谢!这是最干净的解决方案(据我所知)。在使用管理内联模型时非常有用,该模型具有指向第三个模型的外键。这在Django 1.8中似乎不起作用。有人能帮忙吗?@johnny,请看Nicolas78的答案,它在Django 1.8中对我很有效。@johnny my litlle hack在Django 1.10中工作得很好,应该在Django 1.8中工作。这是一个很好的解决方案,在Django 1.8中非常有效。有两个小建议可能会使代码稍微干净一些:1)您可以从这两个类中删除
\uuu init\uuu()
,因为它们不是ops。2) Django 1.9中删除了cache_choices,因此您可以删除整个代码。您好,我目前没有使用Django,因此我没有当前的Django设置,因此无法验证这一点。不愿意将此更改为代码“我无法测试”-你用代码创建一个答案,然后我在底部编辑此帖子以链接到你的答案如何?Django 1.11.4抱怨“AttributeError:“CacheQuerysetAll”对象没有属性“all”,这停止了工作。你知道怎么解决这个问题吗?谢谢你!不幸的是,这破坏了
get\u formset