J'utilise les modèles de base de données Django à partir d'un processus qui n'est pas appelé à partir d'une requête HTTP. Le processus est censé rechercher de nouvelles données toutes les quelques secondes et effectuer un certain traitement dessus. J'ai un boucle qui dort pendant quelques secondes, puis récupère toutes les données non gérées de la base de données.
Ce que je vois, c'est qu'après la première extraction, le processus ne voit jamais de nouvelles données. J'ai exécuté quelques tests et il semble que Django met en cache les résultats, même si je construis de nouveaux QuerySets à chaque fois. Pour le vérifier, je l'ai fait à partir d'un Python Shell:
>>> 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
Comme vous pouvez le voir, l'ajout de nouvelles données ne modifie pas le nombre de résultats. Cependant, appeler la méthode update () du gestionnaire semble résoudre le problème.
Je ne trouve aucune documentation sur cette méthode update () et je n'ai aucune idée de ce que cela pourrait faire de mal.
Ma question est, pourquoi est-ce que je vois ce comportement de mise en cache, qui contredit ce que Django docs dit? Et comment puis-je l'empêcher de se produire?
Ayant eu ce problème et trouvé deux solutions définitives, j'ai pensé qu'il valait la peine de publier une autre réponse.
C'est un problème avec le mode de transaction par défaut de MySQL. Django ouvre une transaction au début, ce qui signifie que par défaut, vous ne verrez pas les modifications apportées dans la base de données.
Démontrer comme ça
Exécutez un Django Shell dans le terminal 1
>>> MyModel.objects.get(id=1).my_field
u'old'
Et un autre dans le 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'
>>>
Retour au terminal 1 pour illustrer le problème - nous lisons toujours l'ancienne valeur de la base de données.
>>> MyModel.objects.get(id=1).my_field
u'old'
Maintenant, dans le terminal 1, montrez la solution
>>> 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'
>>>
Les nouvelles données sont maintenant lues
Voici ce code dans un bloc facile à coller avec 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 solution alternative est de changer my.cnf pour MySQL pour changer le mode de transaction par défaut
transaction-isolation = READ-COMMITTED
Notez que c'est une fonctionnalité relativement nouvelle pour Mysql et a quelques conséquences pour la journalisation/asservissement binaire . Vous pouvez également le mettre dans le préambule de connexion Django si vous le souhaitez.
Mise à jour 3 ans plus tard
Maintenant que Django 1.6 a activé l'autocommit dans MySQL ce n'est plus un problème. L'exemple ci-dessus fonctionne désormais correctement sans le code flush_transaction()
que ce soit votre MySQL est en mode d'isolation de transaction REPEATABLE-READ
(par défaut) ou READ-COMMITTED
.
Ce qui se passait dans les versions précédentes de Django qui s'exécutait en mode non autocommit était que la première instruction select
ouvrait une transaction. Comme le mode par défaut de MySQL est REPEATABLE-READ
, Ce signifie qu'aucune mise à jour de la base de données ne sera lue par les instructions select
suivantes - d'où la nécessité du code flush_transaction()
ci-dessus qui arrête la transaction et en démarre une nouvelle.
Il existe néanmoins des raisons pour lesquelles vous souhaiterez peut-être utiliser l'isolation des transactions READ-COMMITTED
. Si vous deviez mettre le terminal 1 dans une transaction et que vous vouliez voir les écritures du terminal 2, vous auriez besoin de READ-COMMITTED
.
Le code flush_transaction()
produit maintenant un avertissement de dépréciation dans Django 1.6 donc je vous recommande de le supprimer.
Nous avons eu beaucoup de mal à forcer Django pour actualiser le "cache" - ce qui s'est avéré ne pas être du tout un cache mais un artefact dû aux transactions. Cela pourrait ne pas s'appliquer à votre exemple, mais certainement dans Django vues, par défaut, il y a un appel implicite à une transaction, que mysql isole ensuite de tout changement qui se produit à partir d'autres processus après que vous démarrez.
nous avons utilisé le décorateur @transaction.commit_manually
et appelé à transaction.commit()
juste avant chaque occasion où vous avez besoin d'informations à jour.
Comme je l'ai dit, cela s'applique certainement aux vues, je ne sais pas si cela s'appliquerait au code Django non exécuté dans une vue.
informations détaillées ici:
Il semble que la count()
soit mise en cache après la première fois. Voici la source Django pour 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
semble faire un peu de travail supplémentaire, en plus de ce dont vous avez besoin.
Mais je ne peux pas penser à une meilleure façon de faire cela, à moins d'écrire votre propre SQL pour le compte.
Si les performances ne sont pas super importantes, je ferais juste ce que vous faites, en appelant update
avant 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
Je ne suis pas sûr que je le recommanderais ... mais vous pouvez simplement tuer le cache vous-même:
>>> qs = MyModel.objects.all()
>>> qs.count()
1
>>> MyModel().save()
>>> qs.count() # cached!
1
>>> qs._result_cache = None
>>> qs.count()
2
Et voici une meilleure technique qui ne repose pas sur la manipulation des entrailles du QuerySet: N'oubliez pas que la mise en cache se produit dans un QuerySet, mais l'actualisation des données nécessite simplement le sous-jacent Query à réexécuter. Le QuerySet est vraiment juste une API de haut niveau enveloppant un objet Query, plus un conteneur (avec mise en cache!) Pour les résultats de la requête. Ainsi, étant donné un ensemble de requêtes, voici une manière générale de forcer un rafraîchissement:
>>> 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()
Plutôt facile! Vous pouvez bien sûr l'implémenter en tant que fonction d'aide et l'utiliser selon vos besoins.
Si vous ajoutez .all()
à un ensemble de requêtes, cela forcera une relecture à partir de la base de données. Essayez MyModel.objects.all().count()
au lieu de MyModel.objects.count()
.