L’infrastructure de cache dans Django

Un site Web crée avec Django est dynamique. Chaque fois qu’un utilisateur demande l’affichage d’une page, le serveur Web effectue toutes sortes d’opérations – des requêtes de bases de données au rendu des gabarits, en plus de la logique métier de l’application – afin de créer les pages que vos visiteurs verront. Ces opérations sont bien plus chères en temps de calcul qu’un site Web statique, ou des pages seraient directement lues depuis le système de fichier.

Pour la plupart des applications Web, cette charge supplémentaire n’est pas un problème. En effet, la plupart des sites Web n’ont pas la popularité de washingtonpost.com ou slashdot.org, mais sont des sites petits ou moyens, avec un trafic bien plus faible. En revanche, pour les sites à trafic moyen ou lourd, il est essentiel de réduire la charge de travail du serveur au maximum.

C’est là que le cache intervient.

Mettre quelque chose «en cache» est le fait de sauvegarder le résultat d’une opération coûteuse en temps de calcul, afin de ne pas avoir à ré-effectuer cette opération la fois suivante. Voici du pseudocode expliquant comment cela fonctionnerait pour une page web générée dynamiquement :

given a URL, try finding that page in the cache
if the page is in the cache:
    return the cached page
else:
    generate the page
    save the generated page in the cache (for next time)
    return the generated page

Django mets un système de cache robuste à disposition, qui permet de sauvegarder les pages dynamiques afin d’éviter leur calcul pour chaque requête. Pour plus de souplesse, Django propose différents niveaux de granularité de cache: il est possible de mettre en cache le résultat de vues spécifiques, seulement les parties difficiles à produire, ou l’intégralité de votre site.

Django fonctionne aussi très bien avec des caches externes tels que `Squid `_, ainsi que les caches des navigateurs. Ces types de caches ne sont pas contrôlés directement par l’application. Il est en revanche possible de leur fournir des indices (via les en-têtes HTTP) sur quelles parties de votre site mettre en cache, et comment.

Voir aussi

La philosophie de conception du système de cache explique quelques-unes des décisions de conception de ce système.

Mise en place du cache

Le système de cache nécessite un peu de mise en place. En particulier, il faut indiquer où les données mises en cache doivent être stockées – dans une base de données, dans le système de fichiers ou directement en mémoire. C’est une décision importante qui influe sur les performances de votre cache: oui, certains systèmes sont plus rapides que d’autres.

Le choix de votre lieu de stockage de données de cache s’effectue à l’aide de l’entrée CACHES de votre fichier de réglages. Voici les valeurs possibles de CACHES.

Memcached

Memcached est un serveur de cache entièrement basé en mémoire, développé à l’origine pour soutenir la charge de travail élevée de LiveJournal.com, et dont la source a été par la suite ouverte par Danga Interactive. De gros sites tels que Facebook et Wikipedia l’utilisent pour réduire le nombre d’accès à leurs bases de données et accroître les performances de leurs sites en général.

Memcached fonctionne comme un service et reçoit une quantité de mémoire vive définie. Son unique rôle est de fournir une interface rapide pour ajouter, récupérer et supprimer des données dans le cache. Toutes les données sont stockées directement en mémoire, il n’y a donc aucune charge induite par une base de données ou par le système de fichiers.

Après avoir installé Memcached, il s’agit d’installer le code de liaison Memcached. Il existe plusieurs codes de liaison Memcached disponibles en Python ; les deux pris en charge par Django sont pylibmc et pymemcache.

Pour utiliser Memcached avec Django :

  • Définissez BACKEND à django.core.cache.backends.memcached.PyMemcacheCache ou à django.core.cache.backends.memcached.PyLibMCCache (selon votre choix de code de liaison memcached)

  • Définissez LOCATION aux valeurs ip:port, où ip est l’adresse IP du service Memcached et port est le port de fonctionnement de Memcached, ou à une valeur unix:path, où path est le chemin vers le fichier connecteur Unix de Memcached.

Dans cet exemple, Memcached tourne sur localhost (127.0.0.1) port 11211, utilisant la liaison pymemcache:

CACHES = {
    "default": {
        "BACKEND": "django.core.cache.backends.memcached.PyMemcacheCache",
        "LOCATION": "127.0.0.1:11211",
    }
}

Dans cet exemple, Memcached est disponible par un connecteur Unix local /tmp/memcached.sock en utilisant la liaison pymemcache:

CACHES = {
    "default": {
        "BACKEND": "django.core.cache.backends.memcached.PyMemcacheCache",
        "LOCATION": "unix:/tmp/memcached.sock",
    }
}

L’une des fonctionnalités très intéressantes de Memcached est de pouvoir répartir un cache sur plusieurs serveurs. Cela signifie que vous pouvez faire fonctionner des services Memcached sur plusieurs machines et le programme va considérer le groupe de machines comme un seul cache, sans devoir dupliquer les valeurs du cache sur chaque machine. Pour tirer profit de cette fonctionnalité, ajoutez toutes les adresses des serveurs impliqués dans LOCATION, soit en les séparant par des virgules ou points-virgules, soit sous forme de liste.

Dans cet exemple, le cache est distribué sur des instances Memcached fonctionnant aux adresses IP 172.19.26.240 et 172.19.26.242, les deux sur le port 11211 :

CACHES = {
    "default": {
        "BACKEND": "django.core.cache.backends.memcached.PyMemcacheCache",
        "LOCATION": [
            "172.19.26.240:11211",
            "172.19.26.242:11211",
        ],
    }
}

Dans l’exemple suivant, le cache est distribué sur des instances Memcached fonctionnant aux adresses IP 172.19.26.240 (port 11211), 172.19.26.242 (port 11212), and 172.19.26.244 (port 11213) :

CACHES = {
    "default": {
        "BACKEND": "django.core.cache.backends.memcached.PyMemcacheCache",
        "LOCATION": [
            "172.19.26.240:11211",
            "172.19.26.242:11212",
            "172.19.26.244:11213",
        ],
    }
}

Par défaut, le moteur PyMemcacheCache définit les options suivantes (vous pouvez les surcharger dans vos OPTIONS):

"OPTIONS": {
    "allow_unicode_keys": True,
    "default_noreply": False,
    "serde": pymemcache.serde.pickle_serde,
}

Un dernier point au sujet de Memcached est que le cache basé sur la mémoire présente un désavantage : en raison du stockage en mémoire, les données du cache sont perdues si le serveur s’arrête. Il est clair que la mémoire n’est pas prévue pour contenir des données de manière permanente, il ne faut donc pas se baser sur du cache en mémoire comme seul stockage de données. Sans aucun doute, aucun des moteurs de cache de Django ne devrait être utilisé pour du stockage permanent, ils sont tous prévus pour être des solutions de mise en cache et non pour du stockage ; mais nous signalons cela ici car le cache basé sur la mémoire est particulièrement volatile.

Redis

Redis est une base de données en mémoire pouvant être utilisée pour le cache. Pour commencer, il faut un serveur Redis fonctionnant soit localement, soit sur une machine distante.

Après avoir configuré le serveur Redis, il est nécessaire d’installer les liaisons Python pour Redis. redis-py est celle qui est prise en charge nativement par Django. L’installation du paquet supplémentaire hiredis-py est aussi recommandée.

Pour utiliser Redis comme votre moteur de cache avec Django :

Par exemple, si Redis tourne sur localhost (127.0.0.1) port 6379

CACHES = {
    "default": {
        "BACKEND": "django.core.cache.backends.redis.RedisCache",
        "LOCATION": "redis://127.0.0.1:6379",
    }
}

Les serveurs Redis sont souvent protégés par une authentification. Pour indiquer un nom d’utilisateur et un mot de passe, ajoutez-les dans LOCATION en compagnie de l’URL

CACHES = {
    "default": {
        "BACKEND": "django.core.cache.backends.redis.RedisCache",
        "LOCATION": "redis://username:password@127.0.0.1:6379",
    }
}

Si vous disposez de plusieurs serveurs Redis configurés en mode réplication, vous pouvez définir les serveurs soit sous forme de chaîne délimitée par virgule ou point-virgule, soit sous forme de liste. Lorsque plusieurs serveurs sont définis, les opérations d’écriture sont appliquées au premier serveur (leader). Les opérations de lecture sont effectuées sur les autres serveurs (réplicas) choisis au hasard

CACHES = {
    "default": {
        "BACKEND": "django.core.cache.backends.redis.RedisCache",
        "LOCATION": [
            "redis://127.0.0.1:6379",  # leader
            "redis://127.0.0.1:6378",  # read-replica 1
            "redis://127.0.0.1:6377",  # read-replica 2
        ],
    }
}

Cache en base de données

Django peut stocker ses données mises en cache dans votre base de données. Cela fonctionne mieux si vous avez un serveur de base de données rapide et bien indexé.

Pour utiliser une table de base de données comme cache :

  • Définissez BACKEND à django.core.cache.backends.db.DatabaseCache

  • Pour LOCATION, indiquez tablename, le nom de la table de base de données. Ce nom peut être librement choisi, pour autant qu’il corresponde à un nom de table valide qui n’est pas déjà utilisé dans votre base de données.

Dans cet exemple, le nom de la table du cache est my_cache_table:

CACHES = {
    "default": {
        "BACKEND": "django.core.cache.backends.db.DatabaseCache",
        "LOCATION": "my_cache_table",
    }
}

Au contraire des autres moteurs de cache, le cache de base de données ne prend pas en charge la purge automatique des entrées expirées au niveau de la base de données. Ces entrées de cache expirées sont toutefois purgées lors de chaque appel à add(), set() ou touch().

Création de la table du cache

Avant d’utiliser le cache en base de données, vous devez créer la table du cache avec cette commande :

python manage.py createcachetable

Cela crée une table dans votre base de données qui est dans le format attendu par le système de cache de Django. Le nom de la table est extrait de LOCATION.

Si vous utilisez plusieurs caches en base de données, createcachetable crée une table pour chaque cache.

Si vous utilisez plusieurs bases de données, createcachetable respecte la méthode allow_migrate () de vos routeurs de base de données (voir ci-dessous).

Comme migrate, createcachetable ne touchera pas à une table existante. Il ne fera que créer les tables manquantes.

Pour afficher le code SQL qui serait exécuté plutôt que de l’exécuter réellement, utilisez l’option createcachetable --dry-run.

Bases de données multiples

Si vous utilisez le cache en base de données avec plusieurs bases de données, vous devrez également indiquer des instructions de routage pour la table de base de données du cache. Dans l’optique du routage, la table de base de données du cache apparaît en tant que modèle nommé CacheEntry dans une application nommée django_cache. Ce modèle n’est pas présent dans le cache des modèles, mais il est possible d’utiliser les détails du modèle dans un contexte de routage.

Par exemple, le routeur suivant redirige toutes les opérations de lecture de cache vers cache_replica et toutes les opérations d’écriture en cache vers cache_primary. La table de cache ne sera synchronisée que vers cache_primary:

class CacheRouter:
    """A router to control all database cache operations"""

    def db_for_read(self, model, **hints):
        "All cache read operations go to the replica"
        if model._meta.app_label == "django_cache":
            return "cache_replica"
        return None

    def db_for_write(self, model, **hints):
        "All cache write operations go to primary"
        if model._meta.app_label == "django_cache":
            return "cache_primary"
        return None

    def allow_migrate(self, db, app_label, model_name=None, **hints):
        "Only install the cache model on primary"
        if app_label == "django_cache":
            return db == "cache_primary"
        return None

Si vous n’indiquez aucune information de routage pour le modèle de cache en base de données, le moteur de cache utilise la base de données default.

Et si vous n’utilisez pas le moteur de cache en base de données, il n’est pas nécessaire de fournir des instructions de routage pour le modèle de cache en base de données.

Cache sur système de fichiers

Le moteur de cache basé sur des fichiers sérialise et stocke chaque valeur de cache dans un fichier séparé. Pour utiliser ce moteur, définissez BACKEND à "django.core.cache.backends.filebased.FileBasedCache" et renseignez LOCATION avec un répertoire approprié. Par exemple, pour stocker vos données en mémoire cache dans /var/tmp/django_cache, utilisez ce paramètre :

CACHES = {
    "default": {
        "BACKEND": "django.core.cache.backends.filebased.FileBasedCache",
        "LOCATION": "/var/tmp/django_cache",
    }
}

Si vous êtes sur Windows, indiquez la lettre de lecteur au début du chemin, comme ceci :

CACHES = {
    "default": {
        "BACKEND": "django.core.cache.backends.filebased.FileBasedCache",
        "LOCATION": "c:/foo/bar",
    }
}

Le chemin de répertoire doit être absolu, c’est-à-dire qu’il doit commencer à la racine du système de fichiers. La barre oblique en fin de chemin est facultative.

Assurez-vous que le répertoire indiqué par ce réglage existe bel et bien et qu’il est accessible en lecture et écriture, ou qu’il puisse être créé par l’utilisateur système avec lequel tourne votre serveur Web. Poursuivant l’exemple ci-dessus, si votre serveur tourne avec l’utilisateur apache, vérifiez que le répertoire /var/tmp/django_cache existe et qu’il est accessible en lecture et écriture par l’utilisateur apache ou qu’il puisse être créé par l’utilisateur apache.

Avertissement

Lorsque l’emplacement LOCATION du cache se trouve à l’intérieur de MEDIA_ROOT, STATIC_ROOT ou STATICFILES_FINDERS, des données sensibles pourraient se trouver exposées.

Un attaquant qui obtient l’accès au fichier de cache peut non seulement altérer du contenu HTML auquel votre site fait confiance, mais aussi exécuter du code arbitraire à distance, dans la mesure où les données sont sérialisées par pickle.

Avertissement

La mise en cache basée sur le système de fichiers peut devenir lente lors du stockage d’un grand nombre de fichiers. Si vous rencontrez ce problème, envisagez l’utilisation d’un autre mécanisme de mise en cache. Vous pouvez également créer une sous-classe de FileBasedCache et améliorer vous-même la stratégie d’expiration du contenu en cache.

Cache en mémoire locale

Il s’agit du cache par défaut s’il n’y en a pas un autre défini dans votre fichier de réglages. Si vous souhaitez obtenir les avantages de rapidité du cache en mémoire mais que vous n’avez pas la possibilité de faire fonctionner Memcached, considérez la possibilité d’utiliser le moteur de cache en mémoire locale. Ce cache est par processus (voir ci-dessous) et gère la concurrence entre fils d’exécution (thread-safe). Pour l’utiliser, définissez BACKEND à "django.core.cache.backends.locmem.LocMemCache". Par exemple :

CACHES = {
    "default": {
        "BACKEND": "django.core.cache.backends.locmem.LocMemCache",
        "LOCATION": "unique-snowflake",
    }
}

Le réglage de cache LOCATION est utilisé pour identifier les stockages individuels en mémoire. Si vous n’avez qu’un seul cache locmem`, vous pouvez omettre de configurer LOCATION; Cependant, si vous avez plus d’un stockage en mémoire locale, vous devrez attribuer un nom à au moins l’un d’entre eux afin de pouvoir les distinguer.

Le cache utilise une stratégie de nettoyage de type LRU (en fonction de la date d’accès).

Notez que chaque processus aura sa propre instance de cache privée, ce qui signifie qu’il n’est pas possible de faire du cache inter-processus. Cela signifie aussi que le cache en mémoire locale n’est pas particulièrement efficace en mémoire, ce qui n’en fait pas un très bon choix en environnement de production. Mais il est pratique pour le développement.

Pseudo-cache (pour le développement)

Pour terminer, Django fournit un « pseudo-cache » qui ne fait pas réellement de cache, mais qui ne fait qu’implémenter l’interface de cache sans action réelle.

Il est utile dans le cas où un site utilise intensivement le cache en production dans différents endroits, mais que l’environnement de test ou de développement n’est pas censé exploiter le cache et que vous ne souhaitez pas changer le code pour distinguer les types d’environnement au cas par cas. Pour activer le pseudo-cache, définissez BACKEND comme ceci :

CACHES = {
    "default": {
        "BACKEND": "django.core.cache.backends.dummy.DummyCache",
    }
}

Utilisation d’un moteur de cache personnalisé

Bien que Django soit livré avec un certain nombre de moteurs de cache, il peut parfois être nécessaire de créer son propre moteur de cache. Pour utiliser un moteur de cache personnalisé avec Django, il s’agit d’indiquer le chemin d’importation Python dans la clé BACKEND du réglage CACHES, comme ceci :

CACHES = {
    "default": {
        "BACKEND": "path.to.backend",
    }
}

Si vous créez votre propre moteur, vous pouvez considérer les moteurs de cache standards comme les implémentations de référence. Le code se trouve dans le répertoire django/core/cache/backends/ du code source de Django.

Note : sans raison impérieuse, comme par exemple un hôte qui ne les prend pas en charge, il est recommandé de se restreindre aux moteurs de cache inclus dans Django. Ils sont intensivement testés et bien documentés.

Paramètres de cache

Chaque moteur de cache accepte des paramètres supplémentaires pour contrôler le comportement du cache. Ces paramètres sont transmis sous la forme de clés supplémentaires dans le réglage CACHES. Les paramètres valides sont les suivants :

  • TIMEOUT: le délai d’expiration du cache par défaut, en secondes. Ce paramètre contient 300 secondes (5 minutes) par défaut. Vous pouvez définir TIMEOUT à la valeur None de sorte que, par défaut, les clés de cache n’expirent jamais. Une valeur de 0 implique que les clés expirent immédiatement (dans les faits on « ne met pas en cache »).

  • OPTIONS: toute option devant être transmise au moteur de cache. La liste des options valides dépend de chaque moteur et les moteurs s’appuyant sur une bibliothèque tierce transmettent directement ces options à la bibliothèque de cache sous-jacente.

    Les moteurs de cache implémentant leur propre stratégie de purge (« culling »), c’est-à-dire les moteurs locmem, filesystem et database, acceptent les options suivantes :

    • MAX_ENTRIES: le nombre maximum d’éléments autorisés dans le cache avant que des valeurs plus anciennes ne soient supprimées. Ce paramètre vaut 300 par défaut.

    • CULL_FREQUENCY: la quantité d’éléments effacés lorsque le nombre MAX_ENTRIES est atteint. Le taux effectif est 1 / CULL_FREQUENCY, il faut donc indiquer 2 dans CULL_FREQUENCY pour purger la moitié des éléments lorsque MAX_ENTRIES est atteint. Ce paramètre doit être un nombre entier, et sa valeur par défaut est 3.

      Une valeur de CULL_FREQUENCY à 0 signifie que tout le cache est effacé lorsque MAX_ENTRIES est atteint. Sur certains moteurs (database en particulier), cela accélère nettement la purge du cache au dépend de davantage de défauts de cache.

    Les moteurs Memcached et Redis transmettent les contenus de OPTIONS comme paramètres nommés aux constructeurs clients, permettant un contrôle plus fin du comportement du client. Voir ci-dessous pour un exemple d’utilisation.

  • KEY_PREFIX: une chaîne étant systématiquement incluse (en préfixe par défaut) dans toutes les clés de cache utilisées par le serveur Django.

    Consultez la documentation du cache pour plus d’informations.

  • VERSION: le numéro de version par défaut des clés de cache générées par le serveur Django.

    Consultez la documentation du cache pour plus d’informations.

  • KEY_FUNCTION: une chaîne contenant un chemin pointé vers une fonction qui définit la manière de composer un préfixe, une version et une clé pour former une clé de cache finale.

    Consultez la documentation du cache pour plus d’informations.

Dans cet exemple, un moteur basé sur le système de fichiers est configuré avec un délai d’expiration de 60 secondes et une capacité maximale de 1000 éléments :

CACHES = {
    "default": {
        "BACKEND": "django.core.cache.backends.filebased.FileBasedCache",
        "LOCATION": "/var/tmp/django_cache",
        "TIMEOUT": 60,
        "OPTIONS": {"MAX_ENTRIES": 1000},
    }
}

Voici un exemple de configuration pour un moteur basé sur pylibmc et qui active le protocole binaire, l’authentification SASL et le mode de comportement ketama:

CACHES = {
    "default": {
        "BACKEND": "django.core.cache.backends.memcached.PyLibMCCache",
        "LOCATION": "127.0.0.1:11211",
        "OPTIONS": {
            "binary": True,
            "username": "user",
            "password": "pass",
            "behaviors": {
                "ketama": True,
            },
        },
    }
}

Voici un exemple de configuration pour un moteur basé sur pymemcache qui active le pool de clients (ce qui peut améliorer les performances en gardant les clients connectés), qui traite les erreurs réseau et memcache comme des défauts de cache et qui définit l’option TCP_NODELAY sur l’interface de connexion

CACHES = {
    "default": {
        "BACKEND": "django.core.cache.backends.memcached.PyMemcacheCache",
        "LOCATION": "127.0.0.1:11211",
        "OPTIONS": {
            "no_delay": True,
            "ignore_exc": True,
            "max_pool_size": 4,
            "use_pooling": True,
        },
    }
}

Voici une configuration d’exemple pour un moteur basé sur redis qui sélectionne la base de données 10 (par défaut, Redis contient 16 bases de données logiques) et définit une classe de connexions partagées (redis.ConnectionPool est utilisée par défaut) :

CACHES = {
    "default": {
        "BACKEND": "django.core.cache.backends.redis.RedisCache",
        "LOCATION": "redis://127.0.0.1:6379",
        "OPTIONS": {
            "db": "10",
            "pool_class": "redis.BlockingConnectionPool",
        },
    }
}

Le cache « par site »

Une fois le cache configuré, la manière la plus simple d’utiliser le cache est de mettre en cache tout le site. Il s’agit alors d’ajouter 'django.middleware.cache.UpdateCacheMiddleware' et 'django.middleware.cache.FetchFromCacheMiddleware' au réglage MIDDLEWARE, comme dans cet exemple :

MIDDLEWARE = [
    "django.middleware.cache.UpdateCacheMiddleware",
    "django.middleware.common.CommonMiddleware",
    "django.middleware.cache.FetchFromCacheMiddleware",
]

Note

Non, il ne s’agit pas d’une erreur : l’intergiciel « update » doit bien figurer en premier dans la liste, et l’intergiciel « fetch » en dernier. Les détails sont quelque peu obscurs, mais vous pouvez consulter Ordre dans MIDDLEWARE ci-dessous pour connaître toute l’histoire.

Puis, ajoutez les réglages obligatoires suivants dans votre fichier de réglages Django :

  • CACHE_MIDDLEWARE_ALIAS – L’alias de cache à utiliser pour le stockage.

  • CACHE_MIDDLEWARE_SECONDS – le nombre entier de secondes durant lequel chaque page doit être conservée en cache.

  • CACHE_MIDDLEWARE_KEY_PREFIX – Si le cache est partagé par plusieurs sites utilisant la même installation de Django, définissez cette valeur au nom du site ou à un nom unique de cette instance de Django, afin d’éviter des collisions de clés. Indiquez une chaîne vide si cela ne vous concerne pas.

L’intergiciel FetchFromCacheMiddleware met en mémoire cache les réponses GET et HEAD ayant le statut 200, pour autant que les en-têtes de requête et de réponse le permettent. Les réponses aux requêtes à une même URL ayant des paramètres de requête différents sont considérées comme des pages distinctes et sont mises en cache séparément. Cet intergiciel s’attend à ce que la réponse à une requête HEAD possède les mêmes en-têtes de réponse que la requête GET équivalente ; et dans ce cas, il peut renvoyer une réponse GET à partir du cache lors d’une requête HEAD.

De plus, l’intergiciel UpdateCacheMiddleware définit automatiquement quelques en-têtes dans chaque réponse HttpResponse, lesquels affectent les caches amonts:

Voir Intergiciels (« Middleware ») pour plus d’informations sur les intergiciels.

Si une vue définit son propre délai d’expiration de cache (par ex. avec une section max-age dans son en-tête Cache-Control), la page sera mise en cache jusqu’à la fin de ce délai d’expiration, sans tenir compte du temps défini dans CACHE_MIDDLEWARE_SECONDS. À l’aide des décorateurs de django.views.decorators.cache, vous pouvez facilement définir le délai d’expiration d’une vue (par le décorateur cache_control()) ou désactiver la mise en cache d’une vue (par le décorateur never_cache()). Consultez la section utilisation d’autres en-têtes pour en savoir plus sur ces décorateurs.

Si USE_I18N est défini à True, la clé de cache générée inclut le nom de la langue active, voir aussi Processus de découverte de la préférence de langue par Django). Cela permet de facilement mettre en cache des sites multilingues sans devoir créer soi-même les clés de cache.

Les clés de cache incluent également le fuseau horaire actuel lorsque USE_TZ est défini à True.

Le cache par vue

django.views.decorators.cache.cache_page(timeout, *, cache=None, key_prefix=None)

Une manière plus fine d’utiliser l’infrastructure de cache est de mettre en cache le résultat de vues individuelles. django.views.decorators.cache définit un décorateur cache_page qui se charge de mettre en cache la réponse de la vue

from django.views.decorators.cache import cache_page


@cache_page(60 * 15)
def my_view(request): ...

cache_page accepte un seul paramètre : le délai d’expiration du cache en secondes. Dans l’exemple ci-dessus, le résultat de la vue my_view() reste en cache pendant 15 minutes (remarquez que nous avons écrit 60 * 15 par souci de lisibilité, le résultat donne 900, c’est-à-dire 15 minutes multipliées par 60 secondes).

L’expiration du cache définie par cache_page a la priorité sur la directive max-age de l’en-tête Cache-Control.

Le cache par vue, comme le cache par site, construit ses clés d’après les URL. Si des URL différentes pointent vers la même vue, chaque URL aura son propre cache. En reprenant l’exemple my_view, si votre configuration d’URL ressemble à ceci :

urlpatterns = [
    path("foo/<int:code>/", my_view),
]

alors les requêtes vers /foo/1/ et /foo/23/ seront mises en cache séparément, comme prévu. Dès qu’une des URL a été accédée (par ex. /foo/23/), les requêtes suivantes à cette même URL utiliseront le cache.

cache_page accepte également un paramètre nommé facultatif, cache, qui indique au décorateur d’utiliser un cache spécifique (parmi ceux du réglage CACHES) lors de la mise en cache des résultats de la vue. Par défaut, c’est le cache default qui est utilisé, mais vous pouvez indiquer le cache que vous souhaitez :

@cache_page(60 * 15, cache="special_cache")
def my_view(request): ...

Il est aussi possible de surcharger le préfixe de cache par vue. cache_page accepte un paramètre nommé facultatif, key_prefix, qui joue le même rôle que le réglage CACHE_MIDDLEWARE_KEY_PREFIX pour l’intergiciel. Il peut être utilisé comme suit :

@cache_page(60 * 15, key_prefix="site1")
def my_view(request): ...

Les paramètres key_prefix et cache peuvent être tous deux spécifiés. Le paramètre key_prefix et la clé KEY_PREFIX du réglage CACHES sont combinés.

De plus, cache_page définit automatiquement les en-têtes de réponse Cache-Control et Expires qui affectent les caches amonts.

Utilisation du cache par vue dans la configuration d’URL

Les exemples de la section précédente ont figé dans le code le fait que la vue est mise en cache, car cache_page modifie la fonction my_view sur place. Cette approche lie la vue au système de cache, ce qui n’est pas idéal pour plusieurs raisons. Par exemple, il est possible que les vues puissent être réutilisées pour un autre site sans cache, ou peut-être qu’à un moment donné les vues seront mises à disposition d’autres personnes qui voudront les utiliser sans cache. La solution à ces problèmes est de définir le cache par vue dans la configuration d’URL plutôt qu’autour des fonctions de vue elles-mêmes.

Vous pouvez faire cela en enveloppant la fonction de vue dans cache_page lorsque vous la mentionnez dans la configuration d’URL. Voici la configuration que nous avons déjà vue précédemment :

urlpatterns = [
    path("foo/<int:code>/", my_view),
]

Et la même chose, avec my_view entourée par cache_page:

from django.views.decorators.cache import cache_page

urlpatterns = [
    path("foo/<int:code>/", cache_page(60 * 15)(my_view)),
]

Cache de gabarits partiels

Si vous recherchez encore plus de contrôle, il est encore possible de mettre en cache des bouts de gabarits avec la balise de gabarit cache. Pour que vos gabarits aient accès à cette balise, placez {% load cache %} au début du gabarit.

La balise de gabarit {% cache %} met en cache le contenu du bloc pour un temps donné. Il accepte au moins deux paramètres : le délai d’expiration de cache en secondes et le nom à donner au fragment de cache. Le fragment est mis en cache indéfiniment si le délai d’expiration vaut None. Le nom est accepté tel quel, n’utilisez pas de variable. Par exemple :

{% load cache %}
{% cache 500 sidebar %}
    .. sidebar ..
{% endcache %}

Il peut arriver que vous souhaitiez mettre en cache plusieurs copies d’un fragment en fonction de certaines données dynamiques apparaissant dans le fragment. Par exemple, la barre latérale de l’exemple précédent pourrait être mise en cache séparément pour chaque utilisateur du site. Cela se fait en passant un ou plusieurs paramètres supplémentaires, qui peuvent être des variables avec ou sans filtre, à la balise de gabarit {% cache %} afin d’identifier le fragment de cache de manière unique :

{% load cache %}
{% cache 500 sidebar request.user.username %}
    .. sidebar for logged in user ..
{% endcache %}

Quand USE_I18N est défini à True, le cache intergiciel par site respecte la langue active. Pour la balise de gabarit cache, il est possible d’utiliser l’une des variables spécifiques aux traductions disponibles dans les gabarits pour obtenir le même résultat :

{% load i18n %}
{% load cache %}

{% get_current_language as LANGUAGE_CODE %}

{% cache 600 welcome LANGUAGE_CODE %}
    {% translate "Welcome to example.com" %}
{% endcache %}

Le délai d’expiration de cache peut être une variable de gabarit, pour autant que la variable de gabarit corresponde à un nombre entier. Par exemple, si la variable de gabarit my_timeout contient la valeur 600, les deux exemples suivants sont alors équivalents :

{% cache 600 sidebar %} ... {% endcache %}
{% cache my_timeout sidebar %} ... {% endcache %}

Cette fonctionnalité est utile pour éviter les répétitions dans les gabarits. Vous pouvez définir le délai d’expiration dans une variable à un seul endroit, puis réutiliser cette valeur.

Par défaut, la balise cache va essayer d’utiliser le cache appelé « template_fragments ». Si aucun cache de ce type n’existe, il se replie sur l’utilisation du cache par défaut. Vous pouvez définir un moteur de cache alternatif à utiliser au moyen du paramètre nommé using, qui doit être le dernier paramètre de la balise.

{% cache 300 local-thing ...  using="localcache" %}

L’indication d’un nom de cache qui n’est pas configuré est considéré comme une erreur.

django.core.cache.utils.make_template_fragment_key(fragment_name, vary_on=None)

Si vous souhaitez obtenir la clé de cache utilisée pour un fragment en cache, vous pouvez utiliser make_template_fragment_key. fragment_name est comme le second paramètre de la balise de gabarit cache. vary_on est une liste de tous les paramètres supplémentaires transmis à la balise. Cette fonction peut être utile pour invalider ou écraser un élément du cache, par exemple :

>>> from django.core.cache import cache
>>> from django.core.cache.utils import make_template_fragment_key
# cache key for {% cache 500 sidebar username %}
>>> key = make_template_fragment_key("sidebar", [username])
>>> cache.delete(key)  # invalidates cached template fragment
True

L’API de cache de bas niveau

Dans certains cas, la mise en cache d’une page entière ne donne pas d’avantage significatif et peut même se révéler totalement inutile.

Imaginons par exemple que votre site contienne une vue dont les résultats dépendent de plusieurs requêtes coûteuses dont les résultats changent à des moments différents. Dans ce cas, il ne serait pas idéal d’utiliser la mise en cache de pages entières telles que l’offrent les stratégies de cache par site ou par vue, parce qu’il n’est pas souhaitable de mettre en cache la totalité du résultat (étant donné que certaines données changent souvent), mais il reste intéressant de mettre en cache les résultats qui changent rarement.

Dans de telles situations, Django expose une API de cache de bas niveau. Vous pouvez utiliser cette API pour stocker des objets dans le cache avec la granularité de votre choix. Vous pouvez placer dans ce cache tout objet Python pouvant être sérialisée par pickle: chaînes, dictionnaires, listes d’objets de modèle, etc. (la plupart des objets Python courants peuvent être ainsi sérialisés, référez-vous à la documentation de Python pour plus d’informations sur la sérialisation pickle).

Accès au cache

django.core.cache.caches

Vous pouvez accéder aux caches configurés dans le réglage CACHES par un objet de type dictionnaire : django.core.cache.caches. Les requêtes successives du même alias dans le même fil d’exécution renvoient le même objet.

>>> from django.core.cache import caches
>>> cache1 = caches["myalias"]
>>> cache2 = caches["myalias"]
>>> cache1 is cache2
True

Si l’alias indiqué n’existe pas, une exception InvalidCacheBackendError est générée.

Pour respecter la concurrence des fils d’exécution, une instance différente du moteur de cache est renvoyée pour chaque fil d’exécution.

django.core.cache.cache

En raccourci, le cache par défaut est disponible dans django.core.cache.cache:

>>> from django.core.cache import cache

Cet objet est équivalent à caches['default'].

Utilisation de base

L’interface de base est :

cache.set(key, value, timeout=DEFAULT_TIMEOUT, version=None)
>>> cache.set("my_key", "hello, world!", 30)
cache.get(key, default=None, version=None)
>>> cache.get("my_key")
'hello, world!'

key doit être de type str et value peut être n’importe quel objet Python sérialisé par « pickle ».

Le paramètre timeout est optionnel et est par défaut à la même valeur que le paramètre timeout du backend approprié défini dans le réglage de CACHES (expliqué ci-dessus). Il correspond au nombre de secondes pendant lesquelles la valeur doit être stockée dans le cache. Si timeout a la valeur None la valeur reste en cache pour toujours. Un timeout de 0 ne met pas en cache la valeur.

Si l’objet n’existe pas dans le cache, cache.get() renvoie None:

>>> # Wait 30 seconds for 'my_key' to expire...
>>> cache.get("my_key")
None

Si vous avez besoin de savoir si l’objet existe dans le cache avec une valeur littérale None stockée, utilisez un objet sentinelle comme valeur par défaut :

>>> sentinel = object()
>>> cache.get("my_key", sentinel) is sentinel
False
>>> # Wait 30 seconds for 'my_key' to expire...
>>> cache.get("my_key", sentinel) is sentinel
True

cache.get() accepte un paramètre default. Ceci permet d’indiquer la valeur à renvoyer si l’objet n’existe pas dans le cache :

>>> cache.get("my_key", "has expired")
'has expired'
cache.add(key, value, timeout=DEFAULT_TIMEOUT, version=None)

Pour ajouter une clé dans le seul cas où elle n’existe pas encore, utilisez la méthode add(). Elle accepte les mêmes paramètres que set(), mais elle n’essaie pas de mettre à jour le cache si la clé indiquée est déjà présente :

>>> cache.set("add_key", "Initial value")
>>> cache.add("add_key", "New value")
>>> cache.get("add_key")
'Initial value'

Si vous avez besoin de savoir si add() a bien stocké une valeur dans le cache, vous pouvez consulter la valeur renvoyée : True si la valeur a été stockée et False dans le cas contraire.

cache.get_or_set(key, default, timeout=DEFAULT_TIMEOUT, version=None)

Si vous souhaitez obtenir la valeur d’une clé ou définir cette valeur si la clé ne se trouve pas dans le cache, il existe une méthode get_or_set(). Elle accepte les mêmes paramètres que get() mais la valeur par défaut est définie comme nouvelle valeur de cette clé dans le cache, plutôt que d’être renvoyée :

>>> cache.get("my_new_key")  # returns None
>>> cache.get_or_set("my_new_key", "my new value", 100)
'my new value'

Vous pouvez également passer n’importe quel objet exécutable comme valeur par défaut :

>>> import datetime
>>> cache.get_or_set("some-timestamp-key", datetime.datetime.now)
datetime.datetime(2014, 12, 11, 0, 15, 49, 457920)
cache.get_many(keys, version=None)

Il existe aussi une interface get_many() qui n’interroge le cache qu’une seule fois. get_many() renvoie un dictionnaire contenant toutes les clés demandées qui existent réellement dans le cache (et qui n’ont pas expiré) :

>>> cache.set("a", 1)
>>> cache.set("b", 2)
>>> cache.set("c", 3)
>>> cache.get_many(["a", "b", "c"])
{'a': 1, 'b': 2, 'c': 3}
cache.set_many(dict, timeout)

Pour définir plusieurs valeurs de manière plus efficace, utilisez set_many() pour transmettre un dictionnaire de paires clé-valeur :

>>> cache.set_many({"a": 1, "b": 2, "c": 3})
>>> cache.get_many(["a", "b", "c"])
{'a': 1, 'b': 2, 'c': 3}

Tout comme cache.set(), set_many() accepte un paramètre facultatif timeout (délai d’expiration).

Pour les moteurs le prenant en charge (memcached), set_many() renvoie une liste de clés dont l’insertion a échoué.

cache.delete(key, version=None)

Vous pouvez supprimer explicitement des clés avec delete() pour enlever du cache un objet particulier :

>>> cache.delete("a")
True

delete() renvoie True si la clé à été supprimée avec succès, sinon False.

cache.delete_many(keys, version=None)

Si vous voulez effacer plusieurs clés en une seule opération, delete_many() accepte une liste de clés à effacer :

>>> cache.delete_many(["a", "b", "c"])
cache.clear()

Pour finir, si vous voulez effacer toutes les clés du cache, utilisez cache.clear(). Mais soyez prudent ; clear() efface tout ce qui se trouve dans le cache, et pas seulement les clés définies par votre application :

>>> cache.clear()
cache.touch(key, timeout=DEFAULT_TIMEOUT, version=None)

cache.touch() définit une nouvelle date d’expiration d’une clé. Par exemple, pour mettre à jour une clé pour qu’elle expire dans 10 secondes :

>>> cache.touch("a", 10)
True

Comme pour les autres méthodes, le paramètre timeout est facultatif et contient par défaut l’option TIMEOUT du moteur approprié dans le réglage CACHES.

touch() renvoie True si la clé à été touchée avec succès, sinon False.

cache.incr(key, delta=1, version=None)
cache.decr(key, delta=1, version=None)

Vous pouvez aussi incrémenter ou décrémenter une clé existante au moyen des méthodes incr() et decr(), respectivement. Par défaut, l’incrément des valeurs de cache existantes est de 1. D’autre valeurs d’incrément peuvent être fournies en indiquant un paramètre lors de l’appel aux méthodes correspondantes. Une exception ValueError est générée si vous essayez d’incrémenter ou de décrémenter une clé de cache non existante :

>>> cache.set("num", 1)
>>> cache.incr("num")
2
>>> cache.incr("num", 10)
12
>>> cache.decr("num")
11
>>> cache.decr("num", 5)
6

Note

L’atomicité des méthodes incr()/decr() n’est pas garantie. Pour les moteurs qui peuvent garantir l’atomicité de l’incrémentation ou de la décrémentation (en particulier le moteur memcached), les opérations seront effectivement atomiques. Cependant, si le moteur ne contient pas d’opération d’incrémentation ou de décrémentation native, la fonctionnalité sera implémentée en deux temps par une opération de lecture suivie d’une opération de mise à jour.

cache.close()

Vous pouvez fermer la connexion au cache avec close() si le moteur de cache prend en charge cette opération.

>>> cache.close()

Note

Pour les caches qui n’implémentent pas la méthode close, il s’agit d’une opération blanche.

Note

Les variantes asynchrones des méthodes de base sont préfixées par a, par ex. cache.aadd() ou cache.adelete_many(). Lisez Prise en charge asynchrone pour plus de détails.

Préfixe de clés de cache

Si une instance de cache est partagée entre plusieurs serveurs ou entre l’environnement de production et de développement, il est possible que des données mises en cache par un serveur soient lues par un autre serveur. Si le format des données mises en cache est différent d’un serveur à l’autre, cela peut conduire à des problèmes très difficiles à diagnostiquer.

Pour éviter cela, Django offre la possibilité de préfixer toutes les clés de cache utilisées par un serveur. Pour chaque clé de cache enregistrée ou lue, Django préfixe automatiquement la clé de cache par la valeur du réglage de cache KEY_PREFIX.

En prenant soin de définir des valeurs KEY_PREFIX différentes pour chaque instance de Django, vous pouvez vous assurer qu’il n’y aura pas de collisions de valeurs de cache.

Versions dans le cache

Lorsque vous modifiez du code utilisant des valeurs en cache, il peut être nécessaire de purger toutes les valeurs en cache. La manière la plus simple de le faire est d’effacer tout le cache, mais cela peut aussi effacer des valeurs en cache qui resteraient valides et utiles.

Django met à disposition une façon plus subtile de cibler certaines valeurs de cache. Le système de cache de Django dispose d’un identifiant de version global défini dans le réglage de cache VERSION. La valeur de ce réglage est automatiquement combinée au préfixe de cache et à la clé de cache fournie par l’utilisateur pour obtenir la clé de cache finale.

Par défaut, toute lecture de clé inclut automatiquement la version de clé de cache par défaut du site. Cependant, les fonctions de cache primitives proposent toutes un paramètre version, afin de pouvoir indiquer une version de clé de cache particulière, que ce soit en lecture ou en écriture. Par exemple :

>>> # Set version 2 of a cache key
>>> cache.set("my_key", "hello world!", version=2)
>>> # Get the default version (assuming version=1)
>>> cache.get("my_key")
None
>>> # Get version 2 of the same key
>>> cache.get("my_key", version=2)
'hello world!'

La version d’une clé particulière peut être incrémentée ou décrémentée par les méthodes incr_version() et decr_version(). Cela permet de mettre à jour des clés spécifiques à une nouvelle version sans toucher aux autres clés. En poursuivant l’exemple précédent :

>>> # Increment the version of 'my_key'
>>> cache.incr_version("my_key")
>>> # The default version still isn't available
>>> cache.get("my_key")
None
# Version 2 isn't available, either
>>> cache.get("my_key", version=2)
None
>>> # But version 3 *is* available
>>> cache.get("my_key", version=3)
'hello world!'

Transformation de clé de cache

Comme expliqué dans les deux précédentes sections, la clé de cache indiquée par un utilisateur n’est pas employée telle quelle – elle est combinée au préfixe de cache et à la version de clé pour composer une clé de cache finale. Par défaut, les trois parties sont combinées par des deux-points pour produire la clé finale :

def make_key(key, key_prefix, version):
    return "%s:%s:%s" % (key_prefix, version, key)

Si vous souhaitez combiner les parties d’une autre manière ou appliquer un autre traitement à la clé finale (par ex. en produisant une empreinte numérique des parties de la clé), vous pouvez définir une fonction de clé personnalisée.

Le réglage de cache KEY_FUNCTION indique un chemin en syntaxe pointée vers une fonction correspondant au prototype de make_key() (voir ci-dessus). Lorsqu’elle est définie, cette fonction de clé personnalisée sera utilisée en lieu et place de la fonction par défaut de combinaison de clé.

Avertissements de clé de cache

Memcached, le moteur de cache le plus souvent utilisé en production, n’autorise pas de clés de cache plus longues que 250 caractères ou qui contient des espaces ou des caractères de contrôle. De telles clés génèrent des exceptions. Pour promouvoir du code compatible entre caches et minimiser les mauvaises surprises, les autres moteurs de cache intégrés génèrent un avertissement (django.core.cache.backends.base.CacheKeyWarning) si un nom de clé produirait une erreur avec memcached.

Si vous utilisez un moteur de cache en production qui accepte un plus large éventail de clés (moteur personnalisé ou l’un des moteurs intégrés autres que memcached) et que vous ne voulez par être dérangé par les avertissements CacheKeyWarning, vous pouvez les masquer par ce code placé dans le module management de l’une des applications dans INSTALLED_APPS:

import warnings

from django.core.cache import CacheKeyWarning

warnings.simplefilter("ignore", CacheKeyWarning)

Si vous préférez plutôt définir une logique de validation de clé personnalisée pour l’un des moteurs intégrés, vous pouvez créer une sous-classe qui ne surcharge que la méthode validate_key et suivre les instructions données dans utilisation d’un moteur de cache personnalisé. Par exemple, pour faire cela avec le moteur locmem, placez ce code dans un module :

from django.core.cache.backends.locmem import LocMemCache


class CustomLocMemCache(LocMemCache):
    def validate_key(self, key):
        """Custom validation, raising exceptions or warnings as needed."""
        ...

…et utilisez le chemin Python pointé vers cette classe dans la partie BACKEND de votre réglage CACHES.

Gestion du code asynchrone

Django a développé une prise en charge des moteurs de cache asynchrones, mais ne propose pas encore de cache asynchrone. Cela devrait arriver dans les prochaines versions.

django.core.cache.backends.base.BaseCache possède des variantes asynchrones de toutes les méthodes de base. Par convention, les versions asynchrones de toutes les méthodes sont préfixées par a. Par défaut, les arguments des deux variantes sont identiques :

>>> await cache.aset("num", 1)
>>> await cache.ahas_key("num")
True

Caches amonts

Jusqu’ici, ce document s’est concentré sur le cache de vos propres données. Mais dans le cadre du développement Web, un autre type de cache doit être considéré : le cache effectué par des caches « amonts ». Ce sont des systèmes qui mettent des pages en cache pour les utilisateurs avant même que les requêtes n’atteignent votre serveur Web.

Voici quelques exemples de caches amonts :

  • En utilisant HTTP, votre fournisseur de services Internet (ISP) peut mettre certaines pages en cache, donc en demandant une page à l’adresse http://example.com/, vous recevez la page de la part de votre fournisseur sans que la requête n’ait atteint directement example.com. Les mainteneurs de example.com n’ont pas conscience de ce processus de cache. Le fournisseur se place entre votre navigateur Web et le serveur example.com et se charge du cache de manière transparente. Un tel cache n’est pas possible avec HTTPS car cela constituerait une attaque « de l’homme du milieu » (MITM).

  • Votre site Web Django peut se trouver derrière un cache mandataire tel que Squid (http://www.squid-cache.org/), qui met des pages en cache pour améliorer les performances du réseau. Dans ce cas, chaque requête est d’abord traitée par le serveur mandataire et n’est transmise à votre application que s’il le juge nécessaire.

  • Votre navigateur Web met aussi des pages en cache. Si une page Web envoie des en-têtes appropriés, le navigateur utilise la copie d’une page dans le cache local pour toute nouvelle requête vers cette même page, sans même redemander la page Web au serveur pour savoir si elle a changé.

Les caches amonts améliorent bien les performances, mais il y a un risque : de nombreuses pages Web produisent du contenu différent selon la personne authentifiée ou selon d’autres paramètres, et donc des systèmes qui mettent aveuglément des pages en cache en ne se basant que sur l’URL pourraient exposer des données incorrectes ou sensibles à tout visiteur consultant ces pages.

Par exemple, si vous gérez un système de messagerie Web, le contenu de la page « boîte de réception » dépend de l’utilisateur connecté. Si un fournisseur d’accès se met à mettre en cache votre site sans précaution, le premier utilisateur se connectant par l’intermédiaire de ce fournisseur verrait sa propre page de la boîte de réception placée dans le cache et à disposition des visiteurs suivants du site. Ce n’est pas sympa du tout.

Heureusement, HTTP fournit une solution à ce problème. Il existe un certain nombre d’en-têtes HTTP indiquant aux caches amonts de différencier les contenus en cache selon certaines variables précises et d’indiquer aux mécanismes de cache de ne pas mettre en cache certaines pages. Nous allons examiner certains de ces en-têtes dans les sections qui suivent.

Utilisation des en-têtes Vary

L’en-tête Vary définit quels en-têtes de requête un cache doit prendre en considération pour générer sa clé de cache. Par exemple, si le contenu d’une page Web dépend de la préférence de langue d’un utilisateur, on dit que la page « varie en fonction de la langue » (vary on language).

Par défaut, le système de cache de Django crée ses clés de cache en utilisant l’URL complète de la requête, par exemple "https://www.example.com/stories/2005/?order_by=author". Cela signifie que chaque requête vers cette URL utilisera la même version en cache, quels que soient les différences du client (« user-agent ») tels que ses cookies ou préférences de langue. Cependant, si cette page produit du contenu différent en fonction de certaines différences dans les en-têtes de la requête, tels qu’un cookie, une langue, une signature de client, etc. il est alors nécessaire d’utiliser l’en-tête Vary pour signifier aux mécanismes de cache que le contenu de la page dépend de ces éléments.

Pour faire cela avec Django, utilisez le décorateur de vue bien pratique django.views.decorators.vary.vary_on_headers(), comme ceci :

from django.views.decorators.vary import vary_on_headers


@vary_on_headers("User-Agent")
def my_view(request): ...

Dans ce cas, un mécanisme de cache (tel que le propre intergiciel de cache de Django) mettra en cache des versions séparées de la page pour chaque type de client (« user-agent ») différent.

L’avantage d’utiliser le décorateur vary_on_headers plutôt que de définir manuellement l’en-tête Vary (en écrivant quelque chose comme response.headers['Vary'] = 'user-agent') est que le décorateur complète l’en-tête Vary (qui peut déjà exister) au lieu de le redéfinir complètement ce qui pourrait écraser un éventuel contenu d’en-tête existant.

Il est possible de transmettre plusieurs en-têtes à vary_on_headers():

@vary_on_headers("User-Agent", "Cookie")
def my_view(request): ...

Cela indique aux caches amonts de différencier les deux, ce qui signifie qu’à chaque combinaison de type de client et de cookie correspondra une valeur de cache propre. Par exemple, une requête avec le type de client Mozilla et la valeur de cookie foo=bar sera considérée différente d’une requête avec le type de client Mozilla et la valeur de cookie foo=ham.

Comme la variation selon les cookies est très courante, il existe un décorateur django.views.decorators.vary.vary_on_cookie(). Ces deux vues sont équivalentes :

@vary_on_cookie
def my_view(request): ...


@vary_on_headers("Cookie")
def my_view(request): ...

Les en-têtes transmis à vary_on_headers ne sont pas sensibles à la casse ; "User-Agent" est équivalent à "user-agent".

Vous pouvez aussi utiliser directement une fonction utilitaire django.utils.cache.patch_vary_headers(). Celle-ci définit ou complète l’en-tête Vary. Par exemple :

from django.shortcuts import render
from django.utils.cache import patch_vary_headers


def my_view(request):
    ...
    response = render(request, "template_name", context)
    patch_vary_headers(response, ["Cookie"])
    return response

patch_vary_headers accepte une instance HttpResponse comme premier paramètre et une liste de noms d’en-têtes insensibles à la casse comme second paramètre.

Pour plus de détails sur les en-têtes Vary, consultez la spécification officielle de Vary.

Contrôle du cache : utilisation d’autres en-têtes

D’autres problèmes de cache incluent la confidentialité des données et la problématique de l’emplacement de stockage des données en cas de caches en cascade.

Un utilisateur est généralement confronté à deux sortes de caches : son propre cache de navigateur (cache privé) et le cache de son fournisseur d’accès (cache public). Un cache public est partagé par de nombreux utilisateurs et contrôlé de manière externe. Cela pose des problèmes avec les données sensibles ; vous ne souhaitez probablement pas que votre numéro de compte bancaire soit stocké dans un cache public. Les applications Web doivent donc pouvoir indiquer aux caches quelles sont les données privées et celles qui sont publiques.

La solution est d’indiquer que le contenu d’une page en cache devrait être « privé ». Pour faire cela avec Django, utilisez le décorateur de vue cache_control(). Exemple :

from django.views.decorators.cache import cache_control


@cache_control(private=True)
def my_view(request): ...

Ce décorateur se charge d’envoyer les en-têtes HTTP appropriés en arrière-plan.

Signalons que les réglages de contrôle de cache « private » et « public » sont mutuellement exclusifs. Le décorateur s’assure que la directive « public » est enlevée si « private » doit être défini (et vice versa). Un exemple d’utilisation de ces deux directives pourrait être un site de blog qui propose à la fois des articles privés et publics. Les articles publics peuvent être mis en cache dans n’importe quel cache partagé. Le code suivant utilise django.utils.cache.patch_cache_control(), la façon manuelle de modifier l’en-tête de contrôle de cache (elle est appelée en interne par le décorateur cache_control()) :

from django.views.decorators.cache import patch_cache_control
from django.views.decorators.vary import vary_on_cookie


@vary_on_cookie
def list_blog_entries_view(request):
    if request.user.is_anonymous:
        response = render_only_public_entries()
        patch_cache_control(response, public=True)
    else:
        response = render_private_and_public_entries(request.user)
        patch_cache_control(response, private=True)

    return response

Il est aussi possible de contrôler les caches amonts par d’autres moyens (voir RFC 9111 pour des détails sur le cache HTTP). Par exemple, même si vous n’exploitez pas l’infrastructure de cache du côté serveur Django, il est tout de même possible de demander aux clients de mettre en cache une vue pour un certain temps à l’aide de la directive max-age:

from django.views.decorators.cache import cache_control


@cache_control(max_age=3600)
def my_view(request): ...

Si vous utilisez l’intergiciel de cache, celui-ci définit déjà l’en-tête max-age avec la valeur du réglage CACHE_MIDDLEWARE_SECONDS. Dans ce cas, la valeur max_age personnalisée par le décorateur cache_control() est prioritaire et les valeurs d’en-têtes seront fusionnées correctement.

Toute directive de réponse Cache-Control valide est acceptée dans cache_control(). Voici quelques exemples supplémentaires :

  • no_transform=True

  • must_revalidate=True

  • stale_while_revalidate=nombre_secondes

  • no_cache=True

La liste complète des directives connues se trouve dans le registre IANA (notez que toutes ne s’appliquent pas aux réponses).

Si vous souhaitez utiliser des en-têtes pour désactiver totalement le cache, never_cache() est un décorateur de vue qui ajoute les en-têtes nécessaires pour s’assurer que la réponse ne sera pas mise en cache par les navigateurs ou d’autres caches. Exemple :

from django.views.decorators.cache import never_cache


@never_cache
def myview(request): ...

Ordre dans MIDDLEWARE

Si vous utilisez l’intergiciel de cache, il est important de placer chacun des deux éléments au bon endroit dans le réglage MIDDLEWARE. Ceci parce que l’intergiciel de cache a besoin de savoir à partir de quels en-têtes le stockage de cache doit varier. L’intergiciel ajoute toujours quelque chose à l’en-tête de réponse Vary autant qu’il le peut.

UpdateCacheMiddleware s’exécute durant la phase de réponse, lorsque les intergiciels sont exécutés en ordre inverse, et qu’un élément en haut de liste s’exécute en dernier pendant la phase de réponse. Ainsi donc, vous devez prendre soin de placer UpdateCacheMiddleware avant tout autre intergiciel susceptible d’ajouter quelque chose à l’en-tête Vary. Les modules d’intergiciel suivants sont concernés :

  • SessionMiddleware ajoute Cookie

  • GZipMiddleware ajoute Accept-Encoding

  • LocaleMiddleware ajoute Accept-Language

FetchFromCacheMiddleware, de son côté, s’exécute durant la phase de requête, lorsque les intergiciels sont exécutés de haut en bas, et qu’un élément en haut de liste s’exécute en premier pendant la phase de requête. L’intergiciel FetchFromCacheMiddleware doit également être exécuté après que tout autre intergiciel ne modifie l’en-tête Vary, c’est pourquoi il doit figurer dans la liste après ceux qui effectuent une telle modification.

Back to Top