11

Quando si utilizza ModelChoiceField o ModelMultipleChoiceField in una forma Django, c'è un modo di passare in un insieme cache di scelte? Attualmente, se si specificano le scelte tramite il parametro queryset, si ottiene un hit del database.caching queryset alberghi per ModelChoiceField o ModelMultipleChoiceField in una forma Django

Vorrei mettere in cache queste scelte utilizzando memcached ed evitare accessi non necessari al database quando si visualizza un modulo con tale campo.

risposta

9

La ragione per cui ModelChoiceField, in particolare, crea un colpo durante la generazione delle scelte - a prescindere dal fatto che il QuerySet è stata popolata in precedenza - si trova in questa linea

for obj in self.queryset.all(): 

in django.forms.models.ModelChoiceIterator. Come i punti salienti Django documentation on caching of QuerySets,

callable attributes cause DB lookups every time.

Quindi preferisco utilizzare solo

for obj in self.queryset: 

anche se io non sono sicuro al 100% su tutte le implicazioni di questo (io so che non ho grande pianifica con il queryset in seguito, quindi penso di star bene senza la copia .all() creata). Sono tentato di cambiare questo nel codice sorgente, ma dal momento che ho intenzione di non pensarci più alla prossima installare (ed è un cattivo stile per cominciare) ho finito per scrivere la mia personalizzato 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) 

Questo non risolve il problema generale della memorizzazione nella cache del database, ma dal momento che stai chiedendo informazioni su ModelChoiceField in particolare e questo è esattamente ciò che mi ha fatto pensare a quel caching in primo luogo, pensavo che questo potesse essere d'aiuto.

+1

Questa è un'ottima soluzione e funziona perfettamente in Django 1.8. Due suggerimenti minori che potrebbero rendere il codice leggermente più pulito: 1) È possibile rimuovere il '__init __()' da entrambe le classi, poiché sono non-ops. 2) 'cache_choices' è stato rimosso in Django 1.9, quindi è possibile rimuovere l'intero pezzo del codice. – Chad

+0

Ciao, al momento non sto usando Django, quindi non ho impostato Django e quindi modo per verificarlo. Riluttante a cambiare questo codice al codice che non posso testare: per quanto riguarda la creazione di una risposta con il codice e modificare questo post in basso per collegarlo alla risposta? – Nicolas78

12

È possibile ignorare "tutti" metodo QuerySet qualcosa come

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() 

E poi cambiare set di query di campo prima di modulo utilizzando (per exmple quando si utilizza formsets)

form_class.base_fields['foo'].queryset = YourModel.cache_all_method.all() 
+0

eccellenti grazie! Questa è la soluzione più pulita (al meglio delle mie conoscenze). Molto utile quando si lavora con i modelli in linea di amministrazione che dispongono di ForeignKeys per i modelli terzi. – ppetrid

+1

Questo non sembra funzionare in Django 1.8. Qualcuno può aiutare? – johnny

+0

@johnny, vedere la risposta di Nicolas78, che funziona per me in Django 1.8. – Chad

2

ho anche imbattuto su questo problema durante l'utilizzo di InlineFormset nell'Amministratore Django a cui fanno riferimento altri due Modelli. Vengono generate molte query non necessarie perché, come spiegato in Nicolas87, il numero ModelChoiceIterator recupera il queryset ogni volta da zero.

Il seguente Mixin può essere aggiunto a admin.ModelAdmin, admin.TabularInline o admin.StackedInline per ridurre il numero di query al solo quelli necessari per riempire la cache. La cache è legata all'oggetto Request, quindi invalida su una nuova richiesta.

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 
2

@jnns ho notato che nel codice del set di query viene valutata due volte (almeno nel mio contesto in linea Admin), che sembra essere un sovraccarico di Django di amministrazione in ogni caso, anche senza questo mixin (più una volta al in linea quando non si ha questo mixaggio).

Nel caso di questo mixin, questo è dovuto al fatto che formfield.choices ha un setter che (per semplificare) attiva la rivalutazione del queryset dell'oggetto.tutto()

propongo un miglioramento che si compone di trattare direttamente con formfield.cache_choices e formfield.choice_cache

Eccolo:

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 
3

Ecco un piccolo hack che uso con Django 1.10 cache un queryset in 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 
+0

Questo ha smesso di funzionare con Django 1.11.4 lamentando "AttributeError: 'CacheQuerysetAll' oggetto non ha attributo 'all'". Hai qualche idea su come risolvere questo problema? Grazie! – hetsch