Кеширование queryset.count в django

Кеширование queryset.count в django

Как-то обнаружил, что у меня идут несколько одинаковых запросов вида 'SELECT COUNT(*) ...'. Оказалось (да, для меня это было новостью :) ), что метод queryset.count() в джанго кешируется по особому. Но лучше начать рассказ издалека.

Как известно, объекты queryset у ORM django являются "ленивыми", а так же кешируются.

Т.е., преподолжим у нас такая модель:

class Item(models.Model):
    name = models.CharField(max_length=50)

Тогда при создании запроса фактически обращения к БД не происходит (отсюда название lazy - "ленивый"):

items = Item.objects.all()

Оно происходит, когда мы непосредственно обращаемся к объектам из запроса, например в цикле:

for item in items:
     print item.name

При исполнении инструкции for item in items: был такой запрос к БД:

SELECT "main_item"."id", "main_item"."name" FROM "main_item";

При следующем обращении к объектам уже запроса к БД не будет, т.к. все объекты уже были "потроганы" и они попали в кэш. Т.е. этот код сделает только одно обращение к БД:

for item in items: # БД
     print item.name
for item in items: # кеш
     print item.name

Тем не менее, есть некоторые нюансы, когда может произойти второй запрос к БД. Не буду дублировать документацию, чтобы не загромождать статью. Можно почитать здесь: https://docs.djangoproject.com/en/dev/topics/db/queries/#caching-and-querysets.

Теперь непосредственно про count.

Зная, что queryset кешируется, мне казалось, что и .count() тоже кешируется. Но нет (точнее не всегда). Если вызываем метод count() до того, как исходный queryset попал в кеш, будет обращение к БД при каждом вызове count (данное обращение не ленивое, ведь count() возвращает число, а не другой queryset, как это делают all, filter, exclude):

items = Item.objects.all() # нет обращения к БД
items.count() # обращение к БД
items.count() # обращение к БД
items.count() # обращение к БД
for item in items: # обращение к БД и попадание в кеш
     print item.name

Однако, если исходный queryset попал в кеш, то count уже не будет трогать БД:

items = Item.objects.all() # нет обращения к БД
for item in items: # БД и попадание в кеш
     print item.name
items.count() # кеш
items.count() # кеш
items.count() # кеш

Соответственно все это относится и к шаблонам django. В коде, который делал несколько одинаковых запросов 'SELECT COUNT(*) ...', как раз были проверки вида:

{% if items.count %}

и просто вывод количества:

{{ items.count }}

При этом до этих строк не было обращения к самим объектам items. В итоге на каждой из этих строк шел запрос к БД.

Опять же, если до этого где-то был цикл, например такой:

{% for item in items %}
    {{item.name}}
{% endfor %}

то {{ items.count }} уже не обращался к БД.

Итак, варианты для избежания лишних запросов.

1. Если мы знаем, что где-то дальше будет перебор всех элементов из queryset, то вполне уместно использовать len.

Python код:

len(items) # БД
len(items) # кеш
for item in items: # кеш
    # ...

или наоборот, что тоже верно:

for item in items: # БД
    # ...
len(items) # кеш
len(items) # кеш

Шаблон django:

{{ items|length }} # БД
{{ items|length }} # кеш
{% if items|length %} # кеш
{% for item in items %} # кеш

или наоборот:

{% for item in items %} # БД
{{ items|length }} # кеш
{{ items|length }} # кеш
{% if items|length %} # кеш
2. Если нужно только подсчитать количество, либо queryset, для которого нужно количество не совпадает с тем, который будет использоваться для доступа к элементам, то надо использовать count(). Но вызывать его лучше только единожды.

Если в шаблоне нужно обратиться к count более одного раза, то вместо этого:

{{ items.count }}
{{ items.count }}

надо либо во view, который генерит этот шаблон, добавить переменную items_count в контекст и в шаблоне использовать ее:

# views.py
context['items_count'] = items.count()

# шаблон
{{ items_count }}
{{ items_count }}

либо можно использовать {% with items.count as items_count %} (не добавляя в контекст новых переменных из views.py):

# шаблон .html
{% with items.count as items_count %}
     {{ items_count }}
     {{ items_count }}
{% endwith %}

Конечно, в этой статье под словом "кеш" имеется в виду внутренний кеш queryset. Он никак не связан с кешированием.

Опубликовано: Июль 11, 2013
Bookmark and Share
Comments powered by Disqus