Выбор кеширования запросов для ModelChoiceField или ModelMultipleChoiceField в форме Django
При использовании ModelChoiceField или ModelMultipleChoiceField в форме Django существует ли способ передать в кешированном наборе вариантов? В настоящее время, если я укажу выбор через параметр queryset, это приведет к удалению базы данных.
Я хотел бы кэшировать эти варианты с помощью memcached и предотвращать ненужные обращения к базе данных при отображении формы с таким полем.
Ответы
Ответ 1
Вы можете переопределить метод "all" в 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()
Ответ 2
Причина, по которой 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
Вот небольшой хак, который я использую с Django 1.10 для кэширования набора запросов в наборе форм:
qs = my_queryset
# cache the queryset results
cache = [p for p in qs]
# build an iterable class to override the queryset 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
Ответ 4
Я также наткнулся на эту проблему, используя 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
Ответ 5
@jnns Я заметил, что в вашем коде набор запросов оценивается дважды (по крайней мере, в моем встроенном контексте Admin), который, как представляется, является накладными расходами администратора django, даже без этого 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