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 select
Anweisung eine Transaktion geöffnet. Da der Standardmodus von MySQL REPEATABLE-READ
dies ist, bedeutet dies, dass keine Aktualisierungen der Datenbank von nachfolgenden select
Anweisungen 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-COMMITTED
Transaktionsisolation 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.
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_manually
Dekorateur 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
quelle
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.
quelle
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)
update
scheint 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
update
vorher anrufencount
.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
quelle
Wenn Sie
.all()
an ein Abfrageset anhängen , wird ein erneutes Lesen aus der Datenbank erzwungen. Versuchen SieMyModel.objects.all().count()
stattMyModel.objects.count()
.quelle
Sie können auch
MyModel.objects._clone().count().
alle Methoden imQuerySet
Aufruf verwenden,_clone()
bevor Sie Arbeiten ausführen. Dadurch wird sichergestellt, dass alle internen Caches ungültig werden.Die Hauptursache ist, dass
MyModel.objects
jedes 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.quelle
MyModel.objects.all()._clone()
. Wenn Sie darüber nachdenken, könnten Sie damit durchkommen, einMyModel.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.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ügenall()
. 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.count()
eine Art Caching gab, ansonsten ist Carl richtig und diese Antwort ist weit entfernt.