Инвалидация views-based кэша

Напомню, что механизм кэширования в Django предлагает нам на выбор три отдельных уровня: шаблоны, код приложений и кэширование страниц целиком. В зависимости от того, что и как мы желаем сохранить для переиспользования на нашем веб-ресурсе, выбирается тот или иной.

Для каждого уровня предусмотрены свои инструменты сохранения кэша и его инвалидации в дальнейшем. Вроде бы. На деле - не совсем так. Если с проблемой очистки кэша шаблонов или кода объектов запросов к базе данных разобраться труда не составляет, то касательно инвалидации закэшированных view этого сказать уже нельзя. Затык тут происходит в момент, когда мы пожелаем получить ключ того фрагмента кэша, что нам нужно обработать.

Получение ключей в зависимости от уровня кэширования

Рассмотрим, что предоставляет Django для работы с ключами кэша на разных уровнях.

Кэш фрагментов шаблона

В данном случае указание ключей - исключительно наша забота.


{% load cache %}
# Указываем ключи - "user_info" + request.user.username(имя текущего
# пользователя.
{% cache 600 user_info request.user.username %}
    ...
{% endcache %}

Обратиться к сохраненном данным, зная на основании каких параметров задан ключ, труда не составляет. Для получения метки кэша, что сгенерировал нам фреймворк по собственноручно заданным лекалам, предназначен метод make_template_fragment_key из django.core.cache.utils.

from django.core.cache.utils import make_template_fragment_key
from django.core.cache import cache
  
# Некая функция, с помощью которой мы удаляем
# устаревший кэш.
def delete_user_info_tpl_cache(request):
    # Указываем аргументами метода наши ключи из шаблона
    # последовательно, через запятую.
    key = make_template_fragment_key('user_info', request.user.username)
    # Удаляем кэш по полученному ключу.
    cache.delete(key)

Низкоуровневое кэширование

Используя механизм этого уровня мы максимально независимы в выборе стратегий. Сохраняя результат какого-нибудь затратного по ресурсам запроса, как и в случае с "шаблонным" уровнем, решение каким будет ключ принимать нам самим. Посему и обращение по ключу предельно тривиально.

from django.core.cache import cache
from some_app.models import Post
  
def make_key(*args):
    '''
    Функция для генерации ключей.
    '''
    return ':'.join(args)
  
def get_all_posts_by_date(first_letter='a'):
    '''
    Некая функция получения мифических постов, чей заголовок
    начинается с определенной буквы.
    '''
    # Получаем/создаем ключ для кэша.
    key = make_key('posts', first_letter)
    # Достаем посты из кэша.
    posts = cache.get(key)
    # Если по нашему ключу в кэше ничего нет, 
    # тогда - заполняем.
    if not posts:
        # Некий надуманный запрос.
        posts = Post.object.all() \
                .filter(title__startwith=first_letter) \
                .order_by("-date")
        # Кэшируем наш запрос по нужному ключу.
        cache.set(key, posts)
    return posts

Постраничное кэширование

А вот тут нас поджидает засада. В том случае, конечно, если мы желаем освежать данный кэш в нужные моменты.

Если нас устраивает управление оным через установку времени его жизни, тогда никаких проблем не возникает. Но захоти мы удалять сохраненные данные при возникновении какого-либо события - окажется, что сделать это штатными средствами не подставляется возможным. По той причине, что в процесс формирования ключа, в данном случае, Django нас не вовлекает. Как и не предоставляет "изкоробочных" средств для его получения. По крайней мере, в актуальной ныне версии фреймворка (1.11) мной таких средств не обнаружено.

Получение ключа при использовании декоратора @cache_page

Как и при включении опции кеширования всего нашего сайта целиком, при указании кэшировать определенные view, посредством декоратора @cahce_page, мы вольны лишь указать время жизни кэша и префикс для его ключа. Вся остальная магия происходит в чреве фреймворка, через встроенные кэширующие middleware и от нас особо не зависит. Основой ключа выступают данные, содержащиеся в объекте запроса.

Эмулируем фейковый запрос для получения ключа

Повлиять на формирование ключа для кэша мы в описываемой ситуации не можем. Если только косвенно. Зато мы можем создать фейковый объект запроса, сходный с тем, что получает соответствующая middleware, перед тем, как сохранить запрошенную страницу в кэш. И, используя такой объект, генерировать ключ нужного образа и подобия.

from django.http import HttpRequest
from django.core.urlresolvers import reverse
from django.conf import settings
from django.utils.cache import get_cache_key
  
request = HttpRequest()
request.path = reverse(url_name, [arg1, art2,])
request.method = 'GET'
request.META = { 'SERVER_NAME': 'example.com', 'SERVER_PORT': 90, }
# При USE_I18N, установленной в True, 
# в ключ добавляется соответствующие метки.
if settings.USE_I18N:
    request.LANGUAGE_CODE = settings.LANGUAGE_CODE
  
# Если задавался префикс - указываем.
key = get_cache_key(request, key_prefix='prefix')

Пример использования

Ключ у нас есть. Осталось собрать код в подобающий вид для выполнения нужных нам действий.

Создадим функцию для очистки устаревшего кэша определенных страниц, которую будет вызывать в нужный нам момент. Для более удобочитаемого вида разнесу отдельные логические операции по вложенным функциям.

from django.http import HttpRequest
from django.core.urlresolvers import reverse
from django.conf import settings
from django.utils.cache import get_cache_key
from django.core.cache import cache
  
def clear_page_cache(url_name, args=None, key_prefix=None, \
                     method='GET', host='localhost', port=80):
    '''
    Аргументы: 
      url_name - имя, определенное в urls.py для нужной страницы,
                 с указанием пространства имен, если задано; 
      args - список, содержащий аргументы для view; 
      method - http-метод, используемый в запросе; 
      host - http хост, цель запроса (examle.ru, localhost и т.п.); 
      port - порт, на который отправляется запрос;
    '''
    def create_fake_request():
        '''
        Функция-генератор фейкового объекта запроса 
        для получения ключа для нужной закэшерованной страницы.
        '''
        # Подстрахуемся, ошибка вполне ожидаема.
        try:
            request = HttpRequest()
            request.path = reverse(url_name, args=args)
            request.method = method
            request.META = {
                'SERVER_NAME': host,
                'SERVER_PORT': port,
            }
            if settings.USE_I18N:
                request.LANGUAGE_CODE = settings.LANGUAGE_CODE
        except:
            return False
        return request
   
    def get_request_key():
        '''
        Получение ключа кэша на основе фейкового объекта запроса.
        '''
        request = create_fake_request()
        if request:
            return get_cache_key(request, key_prefix=key_prefix)
        return None
    
    key = get_request_key()
    
    if key:
        # Удостоверяемся, напоследок, что у нас действительно имеется
        # кэш с таким ключем.
        if cache.has_key(key):
            cache.delete(key)
            return True
    return False

Ну и напоследок, в качестве последнего примера, очистим кэш гипотетической карты сайта. Url для страницы с sitemap.xml будет у нас фигурировать в urls.py под незамысловатым именем 'sitemap'.

Для вызова функции очистки переопределим метод save() модели. Cаму вызываемую функцию поместим в файл cache.py рядом с settings.py.

...
from some_project_name.cache import clear_page_cache
  
class Post(models.Model):
    title = models.CharField(max_length=200)
    body = models.TextField()
...
    def save(self, *args, **kwargs):
        # Вызываем очистку, только если создается новый пост.
        if not self.pk:
            # Из всех аргументов нам для тестовых целей
            # будет достаточно указать лишь имя урла.
            clear_page_cache('sitemap')
        super(Post, self).save(*args, **kwargs)

Приведенный код проверен и работает на Django версий 1.10 и 1.11.  За более пожилые релизы ручатся не могу.