web-dev-qa-db-fra.com

Comment forcer Django pour ignorer les caches et recharger les données?

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?

73
scippy

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.

93
Nick Craig-Wood

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:

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

8
hwjp

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

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.

6
Chris Clark

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

3
Sarah Messer