Вычисление и кэширование Django QuerySet

QuerySet может быть сконструирован, отфильтрован, нарезан и, как правило, передан без обращения к базе данных. Фактически никаких действий с базой данных не происходит, пока вы не сделаете что-нибудь для оценки QuerySet.

В этой статье мы будем говорить о следующих моделях:

from django.db import models

class Blog(models.Model):
    name = models.CharField(max_length=100)
    tagline = models.TextField()
    
    def __str__(self):
        return self.name

class Entry(models.Model):
    blog = models.ForeignKey(Blog, on_delete=models.CASCADE)
    headline = models.CharField(max_length=255)
    body_text = models.TextField(blank=True)

    class Meta:
        default_related_name = 'entries'

    def __str__(self):
        return self.headline

Взгляните на следующий пример, чтобы понять ленивые запросы:

q1 = Entry.objects.filter(blog=2)
q2 = q1.filter(headline__contains='food')
entry_list = list(q3)

Хотя это выглядит как два запроса в базу данных, на самом деле запрос в базу данных выполняется только один раз, в последней строке (entry_list = list (q3)).

Каждый раз, когда вы уточняете QuerySet, вы получаете отдельный QuerySet, не связанный с предыдущим, который можно сохранять, использовать и повторно использовать.

В следующем коде q и q3 выполняют ту же операцию с базой данных. Обратите внимание, что q1, q2, q3 - это три различных QuerySet.

q = Entry.objects.filter(blog=2).exclude(body_text__icontains="food")

q1 = Entry.objects.filter(blog=2)
q2 = q1.exclude(body_text__icontains="food")
q3 = q2[:10]

Теперь поговорим о выполнении и кешировании.

Выполнение означает фактическое обращение к базе данных. В тот момент, когда вы начинаете перебирать QuerySet, все строки, соответствующие QuerySet, извлекаются из базы данных и конвертируются в модели Django. Эти модели затем сохраняются во встроенном кэше QuerySet, поэтому при повторении QuerySet вам не нужно снова обращаться к базе данных.

Включение кеша

Чтобы включить кеширование в QuerySet, просто сохраните QuerySet в переменной и повторно используйте её. Класс Django QuerySet имеет переменную _result_cache, в которой он сохраняет результаты запроса (модели Django) в списке. _result_cache имеет значение None, если QuerySet не имеет кеша, в противном случае - список объектов модели. Когда вы перебираете кэшированный QuerySet, вы в основном перебираете _result_cache, который является списком.

# Следующий код создаст два QuerySet, выполнит их и забудет,
# потому что они нигде не сохраняют набор запросов, чтобы использовать их позже.
print([e.headline for e in Entry.objects.all()])
print([e.pub_date for e in Entry.objects.all()])

# Следующий код сохраняет QuerySet в переменной.
# Когда он выполнится, он сохранит результаты в свой кеш (_result_cache).
queryset = Entry.objects.all()
# выполнение с итерацией.
for each in queryset:
    print(each.headline)
    
# Использование кеша из предыдущей оценки.
for each in queryset:
    print(each.id)

Итерация - не единственный способ оценки, есть много других способов оценки, давайте обсудим их с примерами.

Итерация

QuerySet является итеративным, и до того, как вы начнете проходить свою первую строку, происходят обращения к базе данных, и результаты сохраняются в кеше. В следующем примере обращения к базе данных и кэширование происходит до печати первого заголовка:

queryset = Entry.objects.all()    
# Выполнение и кэширование
for each in queryset:
    print(each.headline)

# Использование кеша из предыдущей оценки.
for each in queryset:
    print(each.headline)

Срезы

Нарезка невычисленного QuerySet возвращает новый QuerySet. Возвращенный QuerySet не допускает дальнейшие модификации (например, добавление дополнительных фильтров или изменение порядка), но допускает большее количество срезов. Queryset (нарезанный или нет) сохраняет результаты в свой кеш, если вы выполняете итерацию по нему:

# Вы больше не можете использовать фильтр для набора запросов.
queryset = Entry.objects.all()[10:100]
# You can use filter to q1 but not to q2, q3.
q1 = Entry.objects.all()
q2 = q1[1:10]
q3 = q2[1:5]

# сохраняет результаты в кеш q1
lst1 = [each.blog.id for each in q1]
# сохраняет результаты в кеш q2
lst2 = [each.blog.id for each in q2]

Если вы разрезаете уже оцененный Queryset, он возвращает список объектов, а не объекты QuerySet, потому что после оценки, когда вы снова выполняете итерацию, QuerySet использует свое кэшированное (_result_cache) значение, которое является списком.

queryset = Entry.objects.all()
lst = list(queryset)
# возвращает список входных объектов
first_ten = queryset[:10]
# нарезка списка, а не нарезка набора запросов, потому что first_ten - это список.
first_five = first_ten[:5]

Если вы используете индекс для выбора одного элемента из неоцененного QuerySet, он вызывает запрос в базу данных, но если вы выбираете из уже оцененного QuerySet, он использует кеш.

queryset = Entry.objects.all()
# Запрашивает базу данных, поскольку набор запросов еще не был оценен.
print(queryset[5])
# Запрашивает базу данных, поскольку набор запросов еще не был оценен.
print(queryset[5])
lst = list(queryset)
# Использование кешей, поскольку оценка выполнялась в предыдущем list().
print(queryset[5])
print(queryset[10])

Исключением является использование параметра step синтаксиса фрагмента Python для неоцененного QuerySet. В этом случае он немедленно выполняет запрос и возвращает список объектов модели, а не объект QuerySet.

entry_list = Entry.objects.all()[1:100:2]

Упаковка/Кеширование

Если вы упаковываете QuerySet, он будет оценен.

repr()

Метод repr() возвращает печатаемую строку представления данного объекта. QuerySet оценивается, когда вы вызываете для него repr(), но он не сохраняет результаты в кеш.

# repr() оценивает, но не сохраняет результаты в кеш.
queryset = Entry.objects.all()
str_repr = repr(queryset)
# Не используется кеш. Повторный запрос в базу данных.
for each in queryset:
    print(each.headline)

Примечание: функция print() также оценивает QuerySet, но не сохраняет результаты в кеш.

len()

QuerySet оценивается, когда вы вызываете len(), и сохраняет оцененные результаты в кеш.

# len() оценивает и сохраняет результаты в кеш.
queryset = Entry.objects.all()
ln = len(queryset)
# Использование кеша из предыдущей оценки.
for each in queryset:
    print(each.headline)

Примечание. Не используйте это, если вам нужно знать только количество элементов в QuerySet. По этой причине в Django есть функция count().

list()

Принудительно оценивает QuerySet, вызвав для него list(), который возвращает список объектов моделей и сохраняет результаты в кеше.

# Оценивает набор запросов и сохраняет результаты в кеше.
queryset = Entry.objects.all()
lst = list(queryset)
# Использование кеша из предыдущей оценки list().
for each in queryset:
    print(each.headline)

If оператор

Оператор if вызовет выполнение запроса и сохранит результаты в кеше. Если есть хотя бы один результат, QuerySet имеет значение True, в противном случае - False. Например:

# Оператор `if` оценивает набор запросов и сохраняет результаты в кеше.
queryset = Entry.objects.all()
if queryset:     
    # Использование кеша из предыдущей оценки оператора if.     
    for each in queryset:         
        print(each.headline)

Связанные атрибуты модели не кэшируются

Когда Django оценивает QuerySet, поля прямых или обратных отношений не включаются в запрос и, следовательно, не включаются в кеш, если вы не используете select_related или prefetch_related.

queryset = Entry.objects.all()
    for each in queryset:
        print(each.headline)
        # запрос в БД
        print(each.blog.name)

    for each in queryset:
        # использует кеш
        print(each.headline)
        # Нет кеша, снова запрос в базу данных для блога.
        print(each.blog.name)

    # Используйте select_related или prefetch_related для кеширования связанных объектов
    queryset = Entry.objects.select_related('blog')
    for each in queryset:
        print(each.headline)
        print(each.blog.name)

    for each in queryset:
        # использует кеш
        print(each.headline)
        # использует кеш
        print(each.blog.name)
Вернуться на верх