formularios - modelform django
Opciones de conjunto de consultas de caché para ModelChoiceField o ModelMultipleChoiceField en forma de Django (5)
Al usar ModelChoiceField o ModelMultipleChoiceField en forma de Django, ¿hay alguna forma de pasar un conjunto de opciones en caché? Actualmente, si especifico las opciones a través del parámetro queryset , resulta en un hit de la base de datos.
Me gustaría guardar estas opciones en caché utilizando memcached y evitar visitas innecesarias a la base de datos cuando se muestra un formulario con dicho campo.
@jnns noté que en tu código el conjunto de preguntas se evalúa dos veces (al menos en mi contexto de Administración en línea), que parece ser una sobrecarga de django admin de todos modos, incluso sin esta mezcla (más una vez por línea cuando no tienes esta mezcla).
En el caso de este mixin, esto se debe al hecho de que formfield.choices tiene un setter que (para simplificar) desencadena la reevaluación del queryset.all () del objeto
Propongo una mejora que consiste en tratar directamente con formfield.cache_choices y formfield.choice_cache
Aquí está:
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
Aquí hay un pequeño truco que uso con Django 1.10 para almacenar en caché un queryset en un 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
La razón por la que ModelChoiceField
en particular crea un acierto al generar elecciones, independientemente de si el QuerySet se ha llenado previamente, se encuentra en esta línea
for obj in self.queryset.all():
en django.forms.models.ModelChoiceIterator
. Como se destaca la documentación de Django sobre el almacenamiento en caché de QuerySets ,
Los atributos invocables causan búsquedas de DB todo el tiempo.
Así que preferiría simplemente usar
for obj in self.queryset:
a pesar de que no estoy 100% seguro de todas las implicaciones de esto (sí sé que no tengo grandes planes con el queryset luego, así que creo que estoy bien sin la copia .all()
crea). Estoy tentado de cambiar esto en el código fuente, pero como voy a olvidarlo en la próxima instalación (y es un estilo malo para empezar) terminé escribiendo mi ModelChoiceField
personalizado:
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)
Esto no resuelve el problema general del almacenamiento en caché de la base de datos, pero dado que usted está preguntando sobre ModelChoiceField
en particular y eso es exactamente lo que me hizo pensar en el almacenamiento en caché, pensó que esto podría ayudar.
Puede anular el método "todo" en QuerySet algo así como
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()
Y luego cambie el conjunto de preguntas del campo antes de usar el formulario (por ejemplo, cuando use formularios)
form_class.base_fields[''foo''].queryset = YourModel.cache_all_method.all()
También tropecé con este problema al usar un InlineFormset en el administrador de Django que hacía referencia a otros dos modelos. Se generan muchas consultas innecesarias porque, como explicó ModelChoiceIterator
, ModelChoiceIterator
el conjunto de consulta siempre desde cero.
El siguiente Mixin se puede agregar a admin.ModelAdmin
, admin.TabularInline
o admin.StackedInline
para reducir el número de consultas solo a las necesarias para completar el caché. La memoria caché está vinculada al objeto Request
, por lo que se invalida en una nueva solicitud.
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