widgets formularios form fields example combofield django django-forms django-admin django-queryset django-cache

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