Friday, September 21, 2012

Django cache templatetag with dynamic backend parameter

I've found a nice project django-adv-cache-tag.

From the project website:

With django-adv-cache-tag you can :
  • add a version number (int, string, date or whatever, it will be stringified) to you templatetag : the version will be compared to the cached one, and the exact same cache key will be used for the new cached template, avoiding keeping old unused keys in your cache, allowing you to cache forever.
  • avoid to be afraid of an incompatible update in our algorithm, because we also use an internal version number, updated only when the internal algorithm changes
  • define your own cache keys (or more simple, just add the primary key (or what you want, it's a templatetag parameter) to this cache key
  • compress the data to be cached, to reduce memory consumption in your cache backend, and network latency (but it will use more time and cpu to compress/decompress)
  • choose which cache backend will be used
  • define {% nocache %}...{% endnocache %} blocks, inside your cached template, that will only be rendered when asked (for these parts, the content of the template is cached, not the rendered result)
  • easily define your own algorithm, as we provide a single class you can inherit from, and simply change options or whatever behaviour you want, and define your own tags for them
I am using template fragments caching a lot, but some parts of website need invalidating depending on products type etc. 
I am invalidating cache on save & delete signals for model.

Here's the example:
Say there are 5 types of products and your website includes listing for them (which needs lots of data processing etc) and you want to cache it.

from django.db import models


class Product(models.Model):
    attr1 = ...
    attrn = ...
    type =  models.CharField("Product Type", max_length=6, choices=PRODUCT_TYPE_CHOICES)
Using one backend, modifying product of type1 will purge the entire cache backend. Set up more backends:

    'templates_products_type1': {
        'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache',
        'LOCATION': '',
    'templates_products_type2': {
        'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache',
        'LOCATION': '',
You can also use Redis for caching, which requires you to run one redis instance with multiple databases. Let's create our own {% my_cache %} templatetag with cache backend name as a first vary_on parameter. (this is the quickest way as you can also use get_pk() or even add your own parameters to {% my_cache %} templatetag. You could also use fragment name but it's not resolvable. Let's get to the code:
from adv_cache_tag.tag import CacheTag
from django.core.cache import get_cache
from django import template

register = template.Library()

class MyCacheTag(CacheTag):
    def get_cache_object(self):
        backend = self.vary_on[0]
        return get_cache(backend)
MyCacheTag.register(register, 'my_cache')
Here's the template code:
# assuming products_type variables is assigned
{% load my_cache_tags %}

{% with backend="templates_products_"|add:products_type %}
{% my_cache 0 products backend other_vary_on_variables %}
{% for product in product_list %}
{{ product }}
{% endfor %}
{% endmy_cache %}
When saving or deleting products of some type, just purge template cache for produdcts only of this specified type.
from django.core.cache import get_cache
#function run on model save / delete

def clear_products_template_cache(type):
    cache_name = "templates_products_" + type
    cache = get_cache(cache_name)
NOTE: This is a SIMPLIFIED solution. I just wanted to point out that you can easily use different backends for one template cache tag. It's your job to make some additional processing code (especialy get_cache_object() for selecting cache backend etc.


Twidi said...

Hi !

I'm the author of this lib and i wanted to thank you for this note ;)

Glad you liked it !


Robert said...

Thank You, for great piece of code. Hope many like it.