Кэширование вариантов набора запросов для ModelChoiceField или ModelMultipleChoiceField в форме Django - PullRequest
12 голосов
/ 18 ноября 2011

При использовании ModelChoiceField или ModelMultipleChoiceField в форме Django, есть ли способ передать в кэшированный набор вариантов? В настоящее время, если я укажу выбор с помощью параметра queryset , это приведет к попаданию в базу данных.

Я бы хотел кэшировать эти варианты с помощью memcached и предотвратить ненужные попадания в базу данных при отображении формы с таким полем.

Ответы [ 6 ]

13 голосов
/ 01 ноября 2012

Вы можете переопределить метод "все" в 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()
12 голосов
/ 21 ноября 2011

Причина, по которой ModelChoiceField, в частности, создает попадание при создании вариантов - независимо от того, был ли QuerySet заполнен ранее, - заключается в этой строке

for obj in self.queryset.all(): 

в django.forms.models.ModelChoiceIterator.Как подчеркивает документация Django по кэшированию QuerySets ,

вызываемые атрибуты вызывают поиск БД каждый раз.

Поэтому я бы предпочел просто использовать

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 в частности, и именно это заставило меня задуматься об этом кэшировании, я подумал, что это может помочь.

3 голосов
/ 30 марта 2017

Вот небольшой хак, который я использую с 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
2 голосов
/ 08 сентября 2013

@ jnns Я заметил, что в вашем коде набор запросов оценивается дважды (по крайней мере, в моем внутреннем контексте Admin), что в любом случае кажется излишним для администратора django, даже без этого миксина (плюс один раз на каждый встроенный, когда вы это смешивание).

В случае этого миксина, это связано с тем, что 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
2 голосов
/ 22 августа 2013

Я также наткнулся на эту проблему, когда использовал InlineFormset в Django Admin, который сам ссылался на две другие модели.Генерируется множество ненужных запросов, потому что, как объяснил 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
0 голосов
/ 14 февраля 2019

@ 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) напрямую без mixin.Поэтому код может выглядеть так:

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')
...