2010-07-27 7 views
67

Sto utilizzando i modelli di database Django da un processo che non viene chiamato da una richiesta HTTP. Si suppone che il processo esegua il polling dei nuovi dati ogni pochi secondi e ne elabori l'elaborazione. Ho un ciclo che dorme per alcuni secondi e quindi recupera tutti i dati non gestiti dal database.Come impongo a Django di ignorare eventuali cache e ricaricare i dati?

Quello che sto vedendo è che dopo il primo recupero, il processo non vede mai nuovi dati. Ho eseguito alcuni test e sembra che Django stia memorizzando i risultati nella cache, anche se sto creando nuovi QuerySets ogni volta. Per verificare questo, ho fatto questo da una shell Python:

>>> MyModel.objects.count() 
885 
# (Here I added some more data from another process.) 
>>> MyModel.objects.count() 
885 
>>> MyModel.objects.update() 
0 
>>> MyModel.objects.count() 
1025 

Come si può vedere, l'aggiunta di nuovi dati non cambia il conteggio risultato. Tuttavia, chiamare il metodo update() del gestore sembra risolvere il problema.

Non riesco a trovare alcuna documentazione sul metodo update() e non ho idea di quali altre cose brutte potrebbero fare.

La mia domanda è: perché vedo questo comportamento di memorizzazione nella cache, che contraddice ciò che dice Django docs? E come posso evitare che ciò accada?

risposta

6

Sembra che lo count() passi alla cache dopo la prima volta. Questa è la fonte django per QuerySet.count:

def count(self): 
    """ 
    Performs a SELECT COUNT() and returns the number of records as an 
    integer. 

    If the QuerySet is already fully cached this simply returns the length 
    of the cached results set to avoid multiple SELECT COUNT(*) calls. 
    """ 
    if self._result_cache is not None and not self._iter: 
     return len(self._result_cache) 

    return self.query.get_count(using=self.db) 

update non sembrano fare un po 'di lavoro extra, oltre a quello che ti serve.
Ma non riesco a pensare a un modo migliore per farlo, a meno di scrivere il proprio SQL per il conteggio.
Se le prestazioni non sono molto importanti, farei semplicemente ciò che state facendo, chiamando update prima del count.

QuerySet.update:

def update(self, **kwargs): 
    """ 
    Updates all elements in the current QuerySet, setting all the given 
    fields to the appropriate values. 
    """ 
    assert self.query.can_filter(), \ 
      "Cannot update a query once a slice has been taken." 
    self._for_write = True 
    query = self.query.clone(sql.UpdateQuery) 
    query.add_update_values(kwargs) 
    if not transaction.is_managed(using=self.db): 
     transaction.enter_transaction_management(using=self.db) 
     forced_managed = True 
    else: 
     forced_managed = False 
    try: 
     rows = query.get_compiler(self.db).execute_sql(None) 
     if forced_managed: 
      transaction.commit(using=self.db) 
     else: 
      transaction.commit_unless_managed(using=self.db) 
    finally: 
     if forced_managed: 
      transaction.leave_transaction_management(using=self.db) 
    self._result_cache = None 
    return rows 
update.alters_data = True 
-1

È anche possibile utilizzare MyModel.objects._clone().count(). Tutti i metodi della della QuerySet chiamata _clone() prima di fare qualsiasi lavoro - per garantire che ogni cache interne vengono invalidate.

La causa principale è che MyModel.objects è la stessa istanza ogni volta. Con la clonazione stai creando una nuova istanza senza il valore memorizzato nella cache. Naturalmente, puoi sempre raggiungere e invalidare la cache se preferisci utilizzare la stessa istanza.

+0

Sembra una soluzione fantastica e facile, ma almeno nella mia versione di Django non funziona. Chiamando MyModel.objects._clone() si ottiene un errore "AttributeError: 'Manager' non ha attributo '_clone'". Posso fare MyModel.objects.all() ._ clone(), ma funziona come prima - non cambia finché non chiamo update(). Sto usando Django 1.2.1. – scippy

+0

Il mio male - dovrebbe essere 'MyModel.objects.all() ._ clone()'. Nel pensarci, si può fare a meno di fare un 'MyModel.objects.all(). Count()' senza '_clone()'. Questo crea una nuova versione dell'oggetto base e dovrebbe darti una nuova versione senza il valore memorizzato nella cache. Cioè, a meno che Django non stia facendo qualcosa di subdolo e portando lo stato con il clone. –

+3

Questa risposta è sbagliata. Chiamare qualsiasi metodo (come 'count()') su un manager clona implicitamente un nuovo queryset, non c'è alcun comportamento di caching implicito dovuto all'identità di Manager e non è necessario inserire una chiamata esterna a '_clone()' o 'all() '. L'intera catena di pensiero è una falsa pista, il vero problema dell'OP è l'isolamento delle transazioni a livello di database, non ha nulla a che fare con i querysets o il caching di livello Django. –

8

Abbiamo faticato un bel po 'a forzare il django per aggiornare la "cache", che in realtà non era una cache, ma un artefatto a causa delle transazioni. Questo potrebbe non essere applicabile al tuo esempio, ma certamente nelle viste di Django, per impostazione predefinita, c'è una chiamata implicita a una transazione, che mysql quindi isola da qualsiasi modifica che si verifica da altri processi prima che inizi.

abbiamo utilizzato il decoratore @transaction.commit_manually e le chiamate a transaction.commit() appena prima di ogni occasione in cui sono necessarie informazioni aggiornate.

Come ho detto, questo vale sicuramente per le visualizzazioni, non so se si applicherebbe al codice django non eseguito all'interno di una vista.

informazioni dettagliate qui:

http://devblog.resolversystems.com/?p=439

+0

La maggior parte di questa risposta è relativamente accurata, ma la prima frase è altamente fuorviante. Questo problema non ha nulla a che fare con alcuna "cache" in Django o "forzare django ad aggiornare", riguarda interamente l'isolamento della transazione a livello di database. –

+0

grazie Carl, ho aggiunto un chiarimento – hwjp

91

Avendo avuto questo problema e ha trovato due soluzioni definitive per questo ho pensato che vale la pena inviare un'altra risposta.

Questo è un problema con la modalità di transazione predefinita di MySQL. Django apre una transazione all'inizio, il che significa che per impostazione predefinita non verranno visualizzate le modifiche apportate al database.

Dimostrare come questo

eseguire una shell Django nel terminal 1

>>> MyModel.objects.get(id=1).my_field 
u'old' 

e un altro nel terminale 2

>>> MyModel.objects.get(id=1).my_field 
u'old' 
>>> a = MyModel.objects.get(id=1) 
>>> a.my_field = "NEW" 
>>> a.save() 
>>> MyModel.objects.get(id=1).my_field 
u'NEW' 
>>> 

Torna al morsetto 1 per dimostrare il problema - abbiamo ancora letto la vecchio valore dal database.

>>> MyModel.objects.get(id=1).my_field 
u'old' 

Ora nel terminal 1 dimostrano la soluzione

>>> from django.db import transaction 
>>> 
>>> @transaction.commit_manually 
... def flush_transaction(): 
...  transaction.commit() 
... 
>>> MyModel.objects.get(id=1).my_field 
u'old' 
>>> flush_transaction() 
>>> MyModel.objects.get(id=1).my_field 
u'NEW' 
>>> 

I nuovi dati ora vengono letti

qui è che il codice in un facile da incollare blocco con docstring

from django.db import transaction 

@transaction.commit_manually 
def flush_transaction(): 
    """ 
    Flush the current transaction so we don't read stale data 

    Use in long running processes to make sure fresh data is read from 
    the database. This is a problem with MySQL and the default 
    transaction mode. You can fix it by setting 
    "transaction-isolation = READ-COMMITTED" in my.cnf or by calling 
    this function at the appropriate moment 
    """ 
    transaction.commit() 

La soluzione alternativa è cambiare my.cnf per MySQL per cambiare la modalità di transazione predefinita

transaction-isolation = READ-COMMITTED 

Si noti che questa è una funzionalità relativamente nuova per Mysql e ha some consequences for binary logging/slaving. Puoi anche inserire questo nel preambolo della connessione django se lo desideri.

Update 3 anni dopo

Ora che Django 1.6 ha turned on autocommit in MySQL questo non è più un problema. L'esempio sopra ora funziona bene senza il codice flush_transaction() se il tuo MySQL è in REPEATABLE-READ (impostazione predefinita) o nella modalità di isolamento della transazione READ-COMMITTED.

Quello che stava succedendo nelle versioni precedenti di Django che veniva eseguito in modalità non autocommit era che la prima istruzione select apriva una transazione. Poiché la modalità predefinita di MySQL è REPEATABLE-READ, ciò significa che nessun aggiornamento al database verrà letto dalle successive istruzioni select: da qui la necessità del codice flush_transaction() sopra il quale si interrompe la transazione e ne inizia una nuova.

Ci sono ancora dei motivi per cui si potrebbe voler usare l'isolamento della transazione READ-COMMITTED. Se si dovesse inserire il terminale 1 in una transazione e si desidera visualizzare le scritture dal terminale 2, sarà necessario READ-COMMITTED.

Il codice flush_transaction() produce ora un avviso di ritiro in Django 1.6, quindi consiglio di rimuoverlo.

+7

DATABASE_OPTIONS = {"init_command": "SET storage_engine = INNODB, SESSIONE TRANSAZIONE ISOLAMENTO LIVELLO LETTO IMPOSTATO",} –

+0

A partire da django 1.2, la sintassi delle impostazioni è cambiata. Aggiungere "Opzioni" per l'impostazione del Database (probabilmente il 'default' uno) 'Opzioni': { "init_command": "SET storage_engine = InnoDB, sessione di transazione ISOLATION LEVEL READ COMMITTED", } – ryanbraganza

+1

Beh, io' su Django 1.8 e il problema persiste, sia che io usi MySQL o SQLite – tgdn

5

Non sono sicuro di raccomandarlo ...ma si può semplicemente uccidere la cache da soli:

>>> qs = MyModel.objects.all() 
>>> qs.count() 
1 
>>> MyModel().save() 
>>> qs.count() # cached! 
1 
>>> qs._result_cache = None 
>>> qs.count() 
2 

Ed ecco una tecnica migliore che non si basa su giocherellare con le interiora del QuerySet: Ricordate che il caching sta accadendo all'interno di una QuerySet, ma l'aggiornamento della i dati richiedono semplicemente la sottostima della query da rieseguire. QuerySet è in realtà solo un'API di alto livello che avvolge un oggetto Query, oltre a un contenitore (con memorizzazione nella cache!) Per i risultati della query. Così, dato un set di query, ecco un modo generico di costringere un aggiornamento:

>>> MyModel().save() 
>>> qs = MyModel.objects.all() 
>>> qs.count() 
1 
>>> MyModel().save() 
>>> qs.count() # cached! 
1 
>>> from django.db.models import QuerySet 
>>> qs = QuerySet(model=MyModel, query=qs.query) 
>>> qs.count() # refreshed! 
2 
>>> party_time() 

Abbastanza facile! Ovviamente puoi implementarlo come una funzione di supporto e utilizzarlo quando necessario.

0

Se si aggiunge .all() a un set di query, si impone una rilettura dal DB. Prova MyModel.objects.all().count() anziché MyModel.objects.count().

Problemi correlati