web-dev-qa-db-fra.com

Expirer un cache de visualisation dans Django?

Le @cache_page decorator est génial. Mais pour mon blog, j'aimerais garder une page en cache jusqu'à ce que quelqu'un commente un article. Cela semble être une bonne idée car les gens font rarement des commentaires. Garder les pages en mémoire sans que personne ne les commente serait bien. Je pense que quelqu'un doit avoir eu ce problème avant? Et cela diffère de la mise en cache par URL.

Donc, une solution à laquelle je pense est la suivante:

@cache_page( 60 * 15, "blog" );
def blog( request ) ...

Et puis je garderais une liste de toutes les clés de cache utilisées pour la vue de blog et puis aurais le moyen d’expirer l’espace de cache "blog". Mais je ne suis pas très expérimenté avec Django, alors je me demande si quelqu'un connaît une meilleure façon de le faire?

40
Nixarn

Cette solution fonctionne pour les versions de Django antérieures à la version 1.7

Voici une solution que j'ai écrite pour faire exactement ce dont vous parlez sur certains de mes propres projets:

def expire_view_cache(view_name, args=[], namespace=None, key_prefix=None):
    """
    This function allows you to invalidate any view-level cache. 
        view_name: view function you wish to invalidate or it's named url pattern
        args: any arguments passed to the view function
        namepace: optioal, if an application namespace is needed
        key prefix: for the @cache_page decorator for the function (if any)
    """
    from Django.core.urlresolvers import reverse
    from Django.http import HttpRequest
    from Django.utils.cache import get_cache_key
    from Django.core.cache import cache
    # create a fake request object
    request = HttpRequest()
    # Loookup the request path:
    if namespace:
        view_name = namespace + ":" + view_name
    request.path = reverse(view_name, args=args)
    # get cache key, expire if the cached item exists:
    key = get_cache_key(request, key_prefix=key_prefix)
    if key:
        if cache.get(key):
            # Delete the cache entry.  
            #
            # Note that there is a possible race condition here, as another 
            # process / thread may have refreshed the cache between
            # the call to cache.get() above, and the cache.set(key, None) 
            # below.  This may lead to unexpected performance problems under 
            # severe load.
            cache.set(key, None, 0)
        return True
    return False

Django utilise ces caches de la demande de vue pour créer un objet de demande fictif pour la vue mise en cache, l'utiliser pour récupérer la clé de cache, puis l'exclure. 

Pour l'utiliser de la manière dont vous parlez, essayez quelque chose comme:

from Django.db.models.signals import post_save
from blog.models import Entry

def invalidate_blog_index(sender, **kwargs):
    expire_view_cache("blog")

post_save.connect(invalidate_portfolio_index, sender=Entry)

En résumé, chaque fois qu'un objet Entry de blog est enregistré, invalidate_blog_index est appelé et la vue en cache expire. NB: je n'ai pas testé cela de manière approfondie, mais cela a bien fonctionné pour moi jusqu'à présent. 

43
mazelife

J'ai écrit Django-groupcache pour ce genre de situation (vous pouvez téléchargez le code ici ). Dans votre cas, vous pourriez écrire:

from groupcache.decorators import cache_tagged_page

@cache_tagged_page("blog", 60 * 15)
def blog(request):
    ...

A partir de là, vous pourrez simplement faire plus tard:

from groupcache.utils import uncache_from_tag

# Uncache all view responses tagged as "blog"
uncache_from_tag("blog") 

Jetez aussi un coup d'oeil à cache_page_against_model (): c'est un peu plus compliqué, mais cela vous permettra de mettre automatiquement en cache les réponses en fonction des modifications apportées à l'entité de modèle.

11
Syfou

Avec la dernière version de Django (> = 2.0), ce que vous recherchez est très facile à implémenter:

from Django.utils.cache import learn_cache_key
from Django.core.cache import cache
from Django.views.decorators.cache import cache_page

keys = set()

@cache_page( 60 * 15, "blog" );
def blog( request ):
    response = render(request, 'template')
    keys.add(learn_cache_key(request, response)
    return response

def invalidate_cache()
    cache.delete_many(keys)

Vous pouvez enregistrer invalidate_cache en tant que rappel lorsque quelqu'un met à jour une publication dans le blog via un signal de pré-enregistrement.

8
nesdis

Le décorateur cache_page utilisera finalement CacheMiddleware, ce qui générera une clé de cache basée sur la demande (regardez Django.utils.cache.get_cache_key) et le préfixe-clé ("blog" dans votre cas). Notez que "blog" est uniquement un préfixe, pas la clé de cache complète.

Vous pouvez être averti via le signal post_save de Django lorsqu'un commentaire est enregistré, vous pouvez alors créer la clé de cache pour la ou les page (s) appropriée (s) et enfin dire cache.delete(key).

Toutefois, cela nécessite la clé cache_key, qui est construite avec la demande de la vue précédemment mise en cache. Cet objet de demande n'est pas disponible lorsqu'un commentaire est enregistré. Vous pouvez construire la clé de cache sans l'objet de requête approprié, mais cette construction se produit dans une fonction marquée comme privée (_generate_cache_header_key), de sorte que vous n'êtes pas censé utiliser cette fonction directement. Cependant, vous pouvez créer un objet avec un attribut path identique à celui de la vue en cache originale et Django ne le remarquera pas, mais je ne le recommande pas.

Le décorateur de cache_page résume un peu la mise en cache pour vous et rend difficile la suppression directe d'un certain objet de cache. Vous pouvez créer vos propres clés et les manipuler de la même manière, mais cela nécessite davantage de programmation et n’est pas aussi abstrait que le décorateur cache_page.

Vous devrez également supprimer plusieurs objets de cache lorsque vos commentaires sont affichés dans plusieurs vues (c'est-à-dire une page d'index avec le nombre de commentaires et des pages d'entrée de blog individuelles).

Pour résumer: Django effectue l'expiration des clés de cache en fonction du temps, mais la suppression personnalisée des clés de cache au bon moment est plus délicate.

6
stefanw

Cela ne fonctionnera pas sur Django 1.7; comme vous pouvez le voir ici https://docs.djangoproject.com/fr/dev/releases/1.7/#cache-keys-are-now-generated-from-the-request-s-absolute-url the les nouvelles clés de cache sont générées avec l'URL complète, de sorte qu'une fausse demande d'accès ne fonctionnera pas. Vous devez configurer correctement la demande de valeur d'hôte. 

fake_meta = {'HTTP_Host':'myhost',}
request.META = fake_meta

Si vous avez plusieurs domaines travaillant avec les mêmes vues, vous devez les alterner dans HTTP_Host, obtenir la clé appropriée et effectuer le nettoyage pour chacun.

5
maikelpac

Invalidation du cache d'affichage de Django pour les versions 1.7 et supérieures. Testé sur Django 1.9.

def invalidate_cache(path=''):
    ''' this function uses Django's caching function get_cache_key(). Since 1.7, 
        Django has used more variables from the request object (scheme, Host, 
        path, and query string) in order to create the MD5 hashed part of the
        cache_key. Additionally, Django will use your server's timezone and 
        language as properties as well. If internationalization is important to
        your application, you will most likely need to adapt this function to
        handle that appropriately.
    '''
    from Django.core.cache import cache
    from Django.http import HttpRequest
    from Django.utils.cache import get_cache_key

    # Bootstrap request:
    #   request.path should point to the view endpoint you want to invalidate
    #   request.META must include the correct SERVER_NAME and SERVER_PORT as Django uses these in order
    #   to build a MD5 hashed value for the cache_key. Similarly, we need to artificially set the 
    #   language code on the request to 'en-us' to match the initial creation of the cache_key. 
    #   YMMV regarding the language code.        
    request = HttpRequest()
    request.META = {'SERVER_NAME':'localhost','SERVER_PORT':8000}
    request.LANGUAGE_CODE = 'en-us'
    request.path = path

    try:
        cache_key = get_cache_key(request)
        if cache_key :
            if cache.has_key(cache_key):
                cache.delete(cache_key)
                return (True, 'successfully invalidated')
            else:
                return (False, 'cache_key does not exist in cache')
        else:
            raise ValueError('failed to create cache_key')
    except (ValueError, Exception) as e:            
        return (False, e)

Usage:

status, message = invalidate_cache(path='/api/v1/blog/')

5
Duncan

J'ai eu le même problème et je ne voulais pas jouer avec HTTP_Host, alors j'ai créé mon propre décorateur cache_page:

from Django.core.cache import cache


def simple_cache_page(cache_timeout):
    """
    Decorator for views that tries getting the page from the cache and
    populates the cache if the page isn't in the cache yet.

    The cache is keyed by view name and arguments.
    """
    def _dec(func):
        def _new_func(*args, **kwargs):
            key = func.__name__
            if kwargs:
                key += ':' + ':'.join([kwargs[key] for key in kwargs])

            response = cache.get(key)
            if not response:
                response = func(*args, **kwargs)
                cache.set(key, response, cache_timeout)
            return response
        return _new_func
    return _dec

Pour expirer le cache de page suffit d'appeler:

cache.set('map_view:' + self.slug, None, 0)

where self.slug - param de urls.py

url(r'^map/(?P<slug>.+)$', simple_cache_page(60 * 60 * 24)(map_view), name='map'), 

Django 1.11, Python 3.4.3 

3
Mykhailo

FWIW J'ai dû modifier la solution de mazelife pour la faire fonctionner:

def expire_view_cache(view_name, args=[], namespace=None, key_prefix=None, method="GET"):
    """
    This function allows you to invalidate any view-level cache. 
        view_name: view function you wish to invalidate or it's named url pattern
        args: any arguments passed to the view function
        namepace: optioal, if an application namespace is needed
        key prefix: for the @cache_page decorator for the function (if any)

        from: http://stackoverflow.com/questions/2268417/expire-a-view-cache-in-Django
        added: method to request to get the key generating properly
    """
    from Django.core.urlresolvers import reverse
    from Django.http import HttpRequest
    from Django.utils.cache import get_cache_key
    from Django.core.cache import cache
    # create a fake request object
    request = HttpRequest()
    request.method = method
    # Loookup the request path:
    if namespace:
        view_name = namespace + ":" + view_name
    request.path = reverse(view_name, args=args)
    # get cache key, expire if the cached item exists:
    key = get_cache_key(request, key_prefix=key_prefix)
    if key:
        if cache.get(key):
            cache.set(key, None, 0)
        return True
    return False
3
dpn

Au lieu d'utiliser le décorateur de pages en cache, vous pouvez mettre en cache manuellement l'objet de publication de blog (ou similaire) s'il n'y a pas de commentaire, puis lorsqu'il existe un premier commentaire, re-cache l'objet de publication de blog afin qu'il soit à la hauteur. date (en supposant que l'objet possède des attributs qui référencent tous les commentaires), mais laissez simplement les données mises en cache pour la publication de blog commentée expirer et ne vous souciez plus de les remettre en cache ...

3
Steve Jalim

Une dernière version mise à jour de la réponse de Duncan: il fallait trouver les bons champs méta: (testé sur Django 1.9.8)

def invalidate_cache(path=''):
    import socket
    from Django.core.cache import cache
    from Django.http import HttpRequest
    from Django.utils.cache import get_cache_key

    request = HttpRequest()
    domain = 'www.yourdomain.com'
    request.META = {'SERVER_NAME': socket.gethostname(), 'SERVER_PORT':8000, "HTTP_Host": domain, 'HTTP_ACCEPT_ENCODING': 'gzip, deflate, br'}
    request.LANGUAGE_CODE = 'en-us'
    request.path = path

    try:
        cache_key = get_cache_key(request)
        if cache_key :
            if cache.has_key(cache_key):
                cache.delete(cache_key)
                return (True, 'successfully invalidated')
            else:
                return (False, 'cache_key does not exist in cache')
        else:
            raise ValueError('failed to create cache_key')
    except (ValueError, Exception) as e:            
        return (False, e)
0
scythargon

J'ai eu des difficultés avec une situation similaire et voici la solution que j'ai proposée, je l'ai démarrée sur une version antérieure de Django, mais elle est actuellement utilisée sur la version 2.0.3.

Premier problème: lorsque vous définissez des éléments à mettre en cache dans Django, les en-têtes sont placés de manière à ce que les caches en aval, y compris le cache du navigateur, mettent votre page en cache.

Pour remplacer cela, vous devez définir un middleware. J'ai écrit cela ailleurs sur StackOverflow, mais je ne le trouve pas pour le moment. Dans appname/middleware.py:

from Django.utils.cache import add_never_cache_headers


class Disable(object):

    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request):
        response = self.get_response(request)
        add_never_cache_headers(response)
        return response

Puis dans settings.py, à MIDDLEWARE, ajoutez:

'appname.middleware.downstream_caching.Disable',

Gardez à l'esprit que cette approche désactive complètement la mise en cache en aval, ce qui peut ne pas être ce que vous voulez.

Enfin, j'ai ajouté à mon views.py:

def expire_page(request, path=None, query_string=None, method='GET'):
    """
    :param request: "real" request, or at least one providing the same scheme, Host, and port as what you want to expire
    :param path: The path you want to expire, if not the path on the request
    :param query_string: The query string you want to expire, as opposed to the path on the request
    :param method: the HTTP method for the page, if not GET
    :return: None
    """
    if query_string is not None:
        request.META['QUERY_STRING'] = query_string
    if path is not None:
        request.path = path
    request.method = method

    # get_raw_uri and method show, as of this writing, everything used in the cache key
    # print('req uri: {} method: {}'.format(request.get_raw_uri(), request.method))
    key = get_cache_key(request)
    if key in cache:
        cache.delete(key)

Je n’aimais pas avoir à passer un objet request, mais au moment de l’écriture, il fournit le schéma/protocole, l’hôte et le port de la requête, quasiment tous les objets de requête de votre site/application suffiront, tant que vous transmettez le chemin et la chaîne de requête.

0
Paul Krohn

La réponse de Duncan fonctionne bien avec Django 1.9. Mais si nous avons besoin d'invalider l'URL avec le paramètre GET, nous devons apporter quelques modifications à la requête. Par exemple, pour ... /? Mykey = myvalue

request.META = {'SERVER_NAME':'127.0.0.1','SERVER_PORT':8000, 'REQUEST_METHOD':'GET', 'QUERY_STRING': 'mykey=myvalue'}
request.GET.__setitem__(key='mykey', value='myvalue')
0

Au lieu de l'expiration explicite du cache, vous pouvez probablement utiliser le nouveau "préfixe_clé" chaque fois que quelqu'un commente la publication. Par exemple. il peut s'agir de la date/heure du commentaire de la dernière publication (vous pouvez même combiner cette valeur avec l'en-tête Last-Modified).

Malheureusement, Django (y compris cache_page()) ne prend pas en charge les "clés_prefix" dynamiques (vérifié sur Django 1.9 ), mais il existe une solution de contournement. Vous pouvez implémenter votre propre cache_page() qui peut utiliser une CacheMiddleware étendue avec la prise en charge dynamique "préfixe_clé" incluse. Par exemple:

from Django.middleware.cache import CacheMiddleware
from Django.utils.decorators import decorator_from_middleware_with_args

def extended_cache_page(cache_timeout, key_prefix=None, cache=None):
    return decorator_from_middleware_with_args(ExtendedCacheMiddleware)(
        cache_timeout=cache_timeout,
        cache_alias=cache,
        key_prefix=key_prefix,
    )

class ExtendedCacheMiddleware(CacheMiddleware):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        if callable(self.key_prefix):
            self.key_function = self.key_prefix

    def key_function(self, request, *args, **kwargs):
        return self.key_prefix

    def get_key_prefix(self, request):
        return self.key_function(
            request,
            *request.resolver_match.args,
            **request.resolver_match.kwargs
        )

    def process_request(self, request):
        self.key_prefix = self.get_key_prefix(request)
        return super().process_request(request)

    def process_response(self, request, response):
        self.key_prefix = self.get_key_prefix(request)
        return super().process_response(request, response)

Puis dans votre code:

from Django.utils.lru_cache import lru_cache

@lru_cache()
def last_modified(request, blog_id):
    """return fresh key_prefix"""

@extended_cache_page(60 * 15, key_prefix=last_modified)
def view_blog(request, blog_id):
    """view blog page with comments"""
0
renskiy