9

Here is the problem: I have blog app and I cache the post output view for 5 minutes.

@cache_page(60 * 5)
def article(request, slug):
    ...

However, I'd like to invalidate the cache whenever a new comment is added to the post. I'm wondering how best to do so?

I've seen this related question, but it is outdated.

Community
  • 1
  • 1
Jand
  • 2,527
  • 12
  • 36
  • 66
  • 1
    Note that `cache_page` doesn't just cache the page server-side, it also sets the HTTP header `Cache-Control`, so to invalidate the cache, you would have to invalidate both the server-side cache and the client-side browser cache. – Flimm Sep 07 '19 at 14:52

3 Answers3

2

This was the first hit for me when searching for a solution, and the current answer wasn't terribly helpful, so after a lot of poking around Django's source, I have an answer for this one.

Yes you can know the key programmatically, but it takes a little work.

Django's page caching works by referencing the request object, specifically the request path and query string. This means that for every request to your page that has a different query string, you will have a different cache key. For most cases, this isn't likely to be a problem, since the page you want to cache/invalidate will be a known string like /blog/my-awesome-year, so to invalidate this, you just need to use Django's RequestFactory:

from django.core.cache import cache
from django.test import RequestFactory
from django.urls import reverse
from django.utils.cache import get_cache_key


cache.delete(get_cache_key(RequestFactory().get("/blog/my-awesome-year")))

If your URLs are a fixed list of values (ie. no differing query strings) then you can stop here. However if you've got lots of different query strings (say ?q=xyz for a search page or something), then your best bet is probably to create a separate cache for each view. Then you can just pass cache="cachename" to cache_page() and later clear that entire cache with:

from django.core.cache import caches


caches["my_cache_name"].clear()

Important note about this tactic

It only really works for unauthenticated pages. The minute your user is logged in, the cookie data is made part of the cache key creation process, and therefore re-creating that key programmatically becomes much harder. I suppose you could try pulling the cookie data out of your session store, but there could be thousands of keys in there, and you'd have to invalidate/pre-cache each and every one of them.

Daniel Quinn
  • 6,010
  • 6
  • 38
  • 61
  • Are you sure about that when a user logged in, the `cache_page` also includes cookie info when setting/getting cache? Per my knowledge, you have to use vary_on_cookie to differ the caches. – kozhioyrin Apr 16 '23 at 00:01
1

I would cache in a bit different way:

def article(request, slug):
    cached_article = cache.get('article_%s' % slug)
    if not cached_article:
        cached_article = Article.objects.get(slug=slug)
        cache.set('article_%s' % slug, cached_article, 60*5)

    return render(request, 'article/detail.html', {'article':cached_article})

then saving the new comment to this article object:

# ...
# add the new comment to this article object, then
if cache.get('article_%s' % article.slug): 
    cache.delete('article_%s' % article.slug)
# ...
doniyor
  • 36,596
  • 57
  • 175
  • 260
  • This makes perfect sense, just a bit fiddly specially if you have several queries of different objects on the same view. So I thought there may be a more straightforward way to do so. – Jand Nov 17 '15 at 05:56
  • If you cache the whole view, then you dont know that exact cache key to update it later, so i thought this way you have more control over it.. and which has the same result as when you cache the whole view – doniyor Nov 17 '15 at 06:05
  • 1
    I'm hoping that there should be a way to know the views' cach key. If Django knows it, we can probably know it too :) – Jand Nov 17 '15 at 06:11
  • You can know but not programmatically – doniyor Nov 17 '15 at 06:31
0

After going deep into the code, It's very clear that we can do it but we need to have the API URL which you want to invalidate (with query and headers) which looks kind of impossible but if yes than Django uses md5 for hash keys you can try to generate the same hash using the exact same url with the headers.

def _generate_cache_key(request, method, headerlist, key_prefix):
"""Return a cache key from the headers given in the header list."""
ctx = hashlib.md5()
for header in headerlist:
    value = request.META.get(header)
    if value is not None:
        ctx.update(value.encode())
url = hashlib.md5(iri_to_uri(request.build_absolute_uri()).encode('ascii'))
cache_key = 'views.decorators.cache.cache_page.%s.%s.%s.%s' % (
    key_prefix, method, url.hexdigest(), ctx.hexdigest())
return _i18n_cache_key_suffix(request, cache_key)

It doesn't look very easy then you can set a prefix for each URL you have and find all the keys with the same prefix and delete them one by one that is the much better option.

cache_page(DEFAULT_TIMEOUT, None, 'your-cache-prefix')

I don't recommend to change the query params everytime you want to show the uncached data but yes it's a way other way is you can pass no-cache header when you want the fresh data. It will work by default.

Just a note

Header no-cache and also modifying the query params will never invalidate your actual cache.