Wie zwinge ich Django, Caches zu ignorieren und Daten neu zu laden?

77

Ich verwende die Django-Datenbankmodelle aus einem Prozess, der nicht aus einer HTTP-Anforderung aufgerufen wird. Der Prozess soll alle paar Sekunden nach neuen Daten abfragen und diese verarbeiten. Ich habe eine Schleife, die einige Sekunden lang schläft und dann alle nicht behandelten Daten aus der Datenbank abruft.

Ich sehe, dass der Prozess nach dem ersten Abruf keine neuen Daten mehr sieht. Ich habe einige Tests durchgeführt und es sieht so aus, als würde Django die Ergebnisse zwischenspeichern, obwohl ich jedes Mal neue QuerySets erstelle. Um dies zu überprüfen, habe ich dies über eine Python-Shell getan:

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

Wie Sie sehen, ändert das Hinzufügen neuer Daten nichts an der Ergebnisanzahl. Das Aufrufen der update () -Methode des Managers scheint das Problem jedoch zu beheben.

Ich kann keine Dokumentation zu dieser update () -Methode finden und habe keine Ahnung, welche anderen schlechten Dinge sie möglicherweise tun könnte.

Meine Frage ist, warum ich dieses Caching-Verhalten sehe, das dem widerspricht, was Django-Dokumente sagen. Und wie verhindere ich das?

scippy
quelle

Antworten:

95

Nachdem ich dieses Problem hatte und zwei endgültige Lösungen dafür gefunden hatte, hielt ich es für sinnvoll, eine weitere Antwort zu veröffentlichen.

Dies ist ein Problem mit dem Standardtransaktionsmodus von MySQL. Django öffnet zu Beginn eine Transaktion, was bedeutet, dass standardmäßig keine Änderungen in der Datenbank angezeigt werden.

Demonstrieren Sie so

Führen Sie eine Django-Shell in Terminal 1 aus

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

Und noch eine in Terminal 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'
>>> 

Zurück zu Terminal 1, um das Problem zu demonstrieren - wir lesen immer noch den alten Wert aus der Datenbank.

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

Zeigen Sie nun in Terminal 1 die Lösung

>>> 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'
>>> 

Die neuen Daten werden jetzt gelesen

Hier ist dieser Code in einem einfach einzufügenden Block mit 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()

Die alternative Lösung besteht darin, my.cnf für MySQL zu ändern, um den Standardtransaktionsmodus zu ändern

transaction-isolation = READ-COMMITTED

Beachten Sie, dass dies eine relativ neue Funktion für MySQL ist und einige Konsequenzen für die binäre Protokollierung / Slave hat . Sie können dies auch in die Präambel der Django-Verbindung einfügen, wenn Sie möchten.

Update 3 Jahre später

Nachdem Django 1.6 Autocommit in MySQL aktiviert hat, ist dies kein Problem mehr. Das obige Beispiel funktioniert jetzt ohne den flush_transaction()Code, unabhängig davon, ob sich MySQL in REPEATABLE-READ(Standard) oder befindetREAD-COMMITTED Transaktionsisolationsmodus befindet.

In früheren Versionen von Django, die im Nicht-Autocommit-Modus ausgeführt wurden, wurde in der ersten selectAnweisung eine Transaktion geöffnet. Da der Standardmodus von MySQL REPEATABLE-READdies ist, bedeutet dies, dass keine Aktualisierungen der Datenbank von nachfolgenden selectAnweisungen gelesen werden - daher ist dieflush_transaction() Code , über dem die Transaktion gestoppt und eine neue gestartet wird.

Es gibt immer noch Gründe, warum Sie die READ-COMMITTEDTransaktionsisolation verwenden möchten . Wenn Sie Terminal 1 in eine Transaktion einfügen und die Schreibvorgänge von Terminal 2 anzeigen möchten, die Sie benötigen würdenREAD-COMMITTED .

Der flush_transaction()Code erzeugt jetzt in Django 1.6 eine Warnung vor Verfall. Ich empfehle daher, diese zu entfernen.

Nick Craig-Wood
quelle
7
DATABASE_OPTIONS = {"init_command": "SET storage_engine = INNODB, SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED",}
Guy Gavriely
Ab django 1.2 hat sich die Syntax der Einstellungen geändert. Add "Optionen" , um Ihre Datenbanken Einstellung (wahrscheinlich der 'default' one) 'OPTIONEN': { "init_command": "SET storage_engine = INNODB, SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED",}
ryanbraganza
2
Nun, ich bin auf Django 1.8 und das Problem besteht immer noch, ob ich MySQL oder SQLite verwende
tgdn
Ich habe dies auf Django 1.10 versucht und es funktioniert nicht mit SQLite oder pg.
Jared
8

Wir haben einiges damit zu kämpfen, Django zu zwingen, den "Cache" zu aktualisieren - was sich herausstellte, war kein wirklicher Cache, sondern ein Artefakt aufgrund von Transaktionen. Dies trifft möglicherweise nicht auf Ihr Beispiel zu, aber in Django-Ansichten gibt es standardmäßig einen impliziten Aufruf einer Transaktion, die mysql dann von allen Änderungen isoliert, die nach dem Start von anderen Prozessen auftreten.

Wir haben den @transaction.commit_manuallyDekorateur benutzt und rufen antransaction.commit() kurz vor jeder Gelegenheit angerufen, wo Sie aktuelle Informationen benötigen.

Wie gesagt, dies gilt definitiv für Ansichten, nicht sicher, ob dies für Django-Code gilt, der nicht in einer Ansicht ausgeführt wird.

Detaillierte Infos hier:

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

hwjp
quelle
Der größte Teil dieser Antwort ist relativ genau, aber der erste Satz ist sehr irreführend. Dieses Problem hat nichts mit "Cache" in Django oder "Erzwingen einer Aktualisierung von Django" zu tun. Es geht ausschließlich um die Transaktionsisolation auf Datenbankebene.
Carl Meyer
7

Ich bin mir nicht sicher, ob ich es empfehlen würde ... aber Sie können den Cache einfach selbst beenden:

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

Und hier ist eine bessere Technik, bei der nicht mit den Innereien des QuerySet herumgespielt werden muss: Denken Sie daran, dass das Caching in einem QuerySet stattfindet . Für das Aktualisieren der Daten muss die zugrunde liegende Abfrage jedoch erneut ausgeführt werden. Das QuerySet ist eigentlich nur eine übergeordnete API, die ein Abfrageobjekt umschließt, sowie einen Container (mit Caching!) Für Abfrageergebnisse. In Anbetracht eines Abfragesatzes gibt es hier eine allgemeine Möglichkeit, eine Aktualisierung zu erzwingen:

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

Ziemlich einfach! Sie können dies natürlich als Hilfsfunktion implementieren und bei Bedarf verwenden.

Chris Clark
quelle
6

Scheint, als würde das count()nach dem ersten Mal zwischengespeichert. Dies ist die Django-Quelle für 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)

updatescheint ziemlich viel zusätzliche Arbeit zu leisten, abgesehen von dem, was Sie brauchen.
Aber ich kann mir keinen besseren Weg vorstellen, dies zu tun, als Ihr eigenes SQL für die Zählung zu schreiben.
Wenn Leistung nicht besonders wichtig ist, würde ich einfach das tun, was Sie tun, und updatevorher anrufen 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
adamJLev
quelle
4

Wenn Sie .all()an ein Abfrageset anhängen , wird ein erneutes Lesen aus der Datenbank erzwungen. Versuchen Sie MyModel.objects.all().count()statt MyModel.objects.count().

Sarah Messer
quelle
das ist eigentlich sauberer
AK Tolentino
1
all () erstellt ein neues Objekt von QuerySet, dessen _result_cache leer ist.
Alexey Ruzin
-1

Sie können auch MyModel.objects._clone().count(). alle Methoden im QuerySetAufruf verwenden, _clone()bevor Sie Arbeiten ausführen. Dadurch wird sichergestellt, dass alle internen Caches ungültig werden.

Die Hauptursache ist, dass MyModel.objectsjedes Mal dieselbe Instanz vorhanden ist. Durch das Klonen erstellen Sie eine neue Instanz ohne den zwischengespeicherten Wert. Natürlich können Sie jederzeit in den Cache greifen und ihn ungültig machen, wenn Sie dieselbe Instanz verwenden möchten.

Travis Swicegood
quelle
Das sieht nach einer großartigen und einfachen Lösung aus, aber zumindest bei meiner Django-Version funktioniert es nicht. Das Aufrufen von MyModel.objects._clone () führt zu einem "AttributeError: 'Manager'-Objekt hat kein Attribut' _clone '". Ich kann MyModel.objects.all () ._ clone () ausführen, aber das funktioniert genauso wie zuvor - ändert sich erst, wenn ich update () aufrufe. Ich benutze Django 1.2.1.
Scippy
Mein schlechtes - es sollte sein MyModel.objects.all()._clone(). Wenn Sie darüber nachdenken, könnten Sie damit durchkommen, ein MyModel.objects.all().count()ohne das zu tun _clone(). Dadurch wird eine neue Version des Basisobjekts erstellt, und Sie sollten eine neue Version ohne den zwischengespeicherten Wert erhalten. Das heißt, es sei denn, Django tut dort etwas Falsches und trägt den Staat mit dem Klon.
Travis Swicegood
3
Diese Antwort ist falsch. count()Wenn Sie eine Methode (wie ) auf einem Manager aufrufen, wird ein neues Abfrageset implizit geklont. Aufgrund der Manageridentität gibt es kein implizites Caching-Verhalten und es ist nicht erforderlich, einen externen Aufruf an _clone()oder einzufügen all(). Dieser gesamte Gedankengang ist ein roter Hering. Das eigentliche Problem des OP ist die Transaktionsisolation auf Datenbankebene. Es hat überhaupt nichts mit Abfragesätzen oder Caching auf Django-Ebene zu tun.
Carl Meyer
Es ist schon lange her, dass ich mit diesem speziellen Problem gespielt habe, aber ich stelle mir vor, dass es zu dem Zeitpunkt, als es erstellt wurde, count()eine Art Caching gab, ansonsten ist Carl richtig und diese Antwort ist weit entfernt.
Travis Swicegood