Чтение данных и QuerySet API | Курс Django ORM урок 2.2
Цель урока
Разобраться, что такое QuerySet и почему он ленивый. Изучить методы получения данных: all(), filter(), exclude(), get(). Понять разницу между методами, возвращающими QuerySet, и методами, немедленно выполняющими запрос. Разобрать values(), values_list(), only(), defer(), инструменты контроля над тем, какие данные попадают в SELECT.
Необходимые знания
- Урок 2.1: создание объектов
- Базовое понимание SELECT в SQL
Что такое QuerySet
QuerySet это объект, представляющий выборку из базы данных. Он описывает что нужно получить, но не выполняет запрос сразу.
from shop.models import Product
# Это не запрос к БД - это описание запроса
queryset = Product.objects.all()
# И это тоже не запрос
filtered = Product.objects.filter(is_active=True)
# SQL выполняется только здесь, при итерации
for product in filtered:
print(product.name)
Это называется ленивое вычисление (lazy evaluation). QuerySet хранит параметры запроса в памяти и отправляет SQL в PostgreSQL только когда результат действительно нужен.
Когда QuerySet выполняется
qs = Product.objects.filter(is_active=True)
# 1. Итерация
for p in qs: ...
# 2. Срез с шагом или конвертация в список
list(qs)
qs[0]
qs[0:10]
# 3. len()
len(qs)
# 4. bool()
if qs: ...
bool(qs)
# 5. repr() в shell Django выполняет QuerySet при выводе
qs # в shell это напечатает результат
# 6. Явные методы: count(), exists(), first(), last()
qs.count()
qs.exists()
qs.first()
Кеширование QuerySet
После первого выполнения результаты кешируются внутри объекта QuerySet:
qs = Product.objects.filter(is_active=True)
# Первый раз SQL выполняется
for p in qs:
print(p.name)
# Второй раз, данные из кеша, SQL не выполняется
for p in qs:
print(p.price)
Но это кеш именно данного объекта qs. Новый QuerySet, новый запрос:
# Два разных объекта QuerySet = два запроса
Product.objects.filter(is_active=True) # запрос 1
Product.objects.filter(is_active=True) # запрос 2
Цепочки методов
Методы QuerySet возвращают новый QuerySet, это позволяет строить запросы цепочками:
products = (
Product.objects
.filter(is_active=True)
.filter(category__slug="phones")
.exclude(stock=0)
.order_by("-price")
)
Каждый метод в цепочке добавляет условие к будущему SQL. Запрос выполняется один раз, когда цепочка завершена и нужно получить результат.
SELECT shop_product.*
FROM shop_product
INNER JOIN shop_category ON shop_product.category_id = shop_category.id
WHERE shop_product.is_active = true
AND shop_category.slug = 'phones'
AND shop_product.stock != 0
ORDER BY shop_product.price DESC;
all()
products = Product.objects.all()
SELECT "shop_product"."id", "shop_product"."name", ... FROM "shop_product"
ORDER BY "shop_product"."name" ASC; -- из Meta.ordering
all() возвращает QuerySet со всеми объектами. filter() без предварительного all() работает так же. Но all() полезен в двух сценариях:
# 1. Передача базового QuerySet в функцию как аргумент по умолчанию
def get_products(queryset=None):
if queryset is None:
queryset = Product.objects.all()
return queryset.filter(is_active=True)
# 2. Явное клонирование QuerySet перед модификацией
base_qs = Product.objects.filter(is_active=True)
phones_qs = base_qs.all().filter(category__slug="phones") # base_qs не затронут
filter() и exclude()
filter()
filter() используется для отбора объектов по условию, добавляет WHERE в SQL. Несколько условий в одном вызове объединяются через AND.
# Один фильтр
Product.objects.filter(is_active=True)
# Несколько условий в одном filter() - это AND
Product.objects.filter(is_active=True, stock__gt=0)
# Эквивалентно цепочке filter()
Product.objects.filter(is_active=True).filter(stock__gt=0)
-- Оба варианта дают одинаковый SQL
SELECT * FROM shop_product
WHERE is_active = true AND stock > 0
ORDER BY name;
exclude()
exclude() используется когда удобнее описать что исключить, а не что включить, оборачивает условие в NOT (...).
# Все продукты кроме отмененных
Product.objects.exclude(is_active=False)
# Все заказы кроме отмененных и доставленных
Order.objects.exclude(status__in=["cancelled", "delivered"])
SELECT * FROM shop_order
WHERE NOT (status = 'cancelled' OR status = 'delivered')
ORDER BY created_at DESC;
При фильтрации через связанные модели это может давать неожиданные результаты, разберем в уроке 3.1 при изучении Q objects.
get()
get() возвращает ровно один объект. Если условию соответствует 0 или 2+ объекта, будет поднято исключение:
# Нормальная работа
product = Product.objects.get(id=1)
product = Product.objects.get(slug="iphone-15")
# DoesNotExist, объект не найден
try:
product = Product.objects.get(slug="nonexistent")
except Product.DoesNotExist:
print("Не найдено")
# MultipleObjectsReturned когда найдено несколько
try:
product = Product.objects.get(is_active=True) # их много
except Product.MultipleObjectsReturned:
print("Найдено несколько объектов")
SELECT * FROM shop_product WHERE slug = 'iphone-15' LIMIT 2;
Django добавляет LIMIT 2, а не LIMIT 1, чтобы обнаружить случай "найдено больше одного" без загрузки всех строк.
get() немедленно выполняет SQL, это не ленивый метод.
get() vs filter().first()
# get() - исключение если нет объекта
product = Product.objects.get(slug="iphone-15")
# filter().first() - None если нет объекта
product = Product.objects.filter(slug="iphone-15").first()
-- filter().first()
SELECT * FROM shop_product WHERE slug = 'iphone-15' ORDER BY name LIMIT 1;
filter().first() безопаснее когда объект может отсутствовать и это нормально. get(), когда отсутствие объекта является ошибкой.
first() и last()
first() используйте когда объект может отсутствовать и это нормально, в отличие от get() не бросает исключение, возвращает None. last() нужен чтобы получить последний элемент: заказ пользователя, последнее событие в логе.
# Первый по Meta.ordering
Product.objects.filter(is_active=True).first()
# Последний по Meta.ordering
Order.objects.filter(user=user).last()
-- first() при ordering = ["name"]
SELECT * FROM shop_product WHERE is_active = true ORDER BY name ASC LIMIT 1;
-- last() при ordering = ["-created_at"]
SELECT * FROM shop_order WHERE user_id = 1 ORDER BY created_at ASC LIMIT 1;
last() разворачивает ordering, добавляет DESC. Если ordering не задан и не указан order_by(), результат непредсказуем.
values() - словари вместо объектов
По умолчанию QuerySet возвращает экземпляры модели. values() возвращает словари:
# Возвращает объекты Product
products = Product.objects.filter(is_active=True)
for p in products:
print(p.name, p.price) # доступ к атрибутам объекта
# Возвращает словари
products = Product.objects.filter(is_active=True).values("name", "price")
for p in products:
print(p["name"], p["price"]) # доступ по ключу словаря
-- values("name", "price"), только нужные колонки
SELECT "shop_product"."name", "shop_product"."price"
FROM "shop_product"
WHERE "shop_product"."is_active" = true
ORDER BY "shop_product"."name";
Без аргументов values() возвращает все поля как словари. С аргументами, только указанные. Меньше данных из БД, меньше нагрузка на сеть и память.
Синтаксис __ (двойное подчеркивание)
Django использует __ как разделитель для обхода связей между моделями и задания условий фильтрации. Общий принцип:
поле__поле_связанной_модели
поле__тип_условия
# Обход связи: перейти от Product к Category и взять её name
Product.objects.values("category__name")
# Тип условия: price больше 500
Product.objects.filter(price__gt=500)
# Комбинация: поле связанной модели + условие
Product.objects.filter(category__slug="phones")
Глубина вложенности не ограничена, можно идти по цепочке связей:
# Продукты заказов конкретного пользователя
OrderItem.objects.filter(order__user__username="admin")
Полный список типов условий (__gt, __contains, __in и другие) разберем в уроке 2.3.
values() через связи
# Получить название продукта и название его категории
Product.objects.values("name", "category__name")
SELECT "shop_product"."name", "shop_category"."name"
FROM "shop_product"
INNER JOIN "shop_category" ON "shop_product"."category_id" = "shop_category"."id"
ORDER BY "shop_product"."name";
Django автоматически делает JOIN. Колонка называется category__name (двойное подчеркивание), так же как в словаре.
Когда использовать values()
- Нужны только данные (для сериализации, экспорта, агрегаций), а не объекты модели
- Нужно снизить потребление памяти при большой выборке
- Работаете с агрегациями через
annotate(),values()+annotate(), стандартный паттерн для GROUP BY
values_list() - кортежи или отдельные значения
values_list() возвращает кортежи вместо словарей. Используйте его когда результат передается в другой QuerySet или нужен простой список значений, а не структура для сериализации.
values() |
values_list() |
|
|---|---|---|
| Результат | {"name": "iPhone", "price": ...} |
("iPhone", Decimal("...")) |
| Когда | сериализация, передача в шаблон | список id, передача в filter(__in=...) |
# Список кортежей
Product.objects.values_list("name", "price")
# [("iPhone 15", Decimal("999.99")), ("Samsung Galaxy S24", Decimal("899.99")), ...]
# flat=True, список значений (только одно поле)
Product.objects.values_list("name", flat=True)
# ["iPhone 15", "Samsung Galaxy S24", ...]
# named=True - именованные кортежи
Product.objects.values_list("name", "price", named=True)
# [Row(name="iPhone 15", price=Decimal("999.99")), ...]
-- values_list("name", "price")
SELECT "shop_product"."name", "shop_product"."price"
FROM "shop_product"
ORDER BY "shop_product"."name";
Часто нужно передать результат в другой QuerySet через __in:
# Получить id заказов пользователя и передать в следующий запрос
order_ids = Order.objects.filter(user=user).values_list("id", flat=True)
items = OrderItem.objects.filter(order_id__in=order_ids)
flat=True работает только с одним полем. Для нескольких полей используйте values_list()
без flat или values().
only() и defer() - контроль над колонками объектов
values() и values_list() возвращают словари и кортежи. Если нужны именно объекты модели, но только с частью полей, используйте only() и defer().
only() - загрузить только указанные поля
# Загрузить только name и price
products = Product.objects.only("name", "price")
for p in products:
print(p.name, p.price) # из кеша, без запроса
print(p.description) # ДОПОЛНИТЕЛЬНЫЙ запрос к БД
-- only("name", "price") - pk загружается всегда
SELECT "shop_product"."id", "shop_product"."name", "shop_product"."price"
FROM "shop_product"
ORDER BY "shop_product"."name";
Если обратиться к полю, которое не было загружено через only(), Django сделает дополнительный SELECT для этого объекта. Это называется deferred field: отложенное поле.
defer() - исключить указанные поля
# Загрузить все поля кроме description (тяжелое текстовое поле)
products = Product.objects.defer("description")
SELECT "shop_product"."id", "shop_product"."name", "shop_product"."slug",
"shop_product"."price", "shop_product"."stock", "shop_product"."is_active",
"shop_product"."created_at", "shop_product"."updated_at", "shop_product"."category_id"
FROM "shop_product"
ORDER BY "shop_product"."name";
-- description НЕ в SELECT
defer() удобен когда у модели есть одно-два тяжелых поля (большой TextField, JSONField), которые не нужны в конкретном запросе.
Ловушка only() и defer()
products = Product.objects.only("name", "price")
for p in products:
# Каждое обращение к отложенному полю = отдельный SELECT
print(p.description) # SELECT description FROM shop_product WHERE id = X
Это может создать N+1 на отложенных полях. Используйте only() и defer() только когда точно знаете, что не будете обращаться к исключенным полям.
Подробнее с примерами производительности в уроке 5.3.
Методы немедленно выполняющие запрос
Не все методы QuerySet ленивые. Эти методы выполняют SQL немедленно и возвращают не QuerySet:
qs = Product.objects.filter(is_active=True)
qs.count() # int - SELECT COUNT(*) ...
qs.exists() # bool - SELECT 1 ... LIMIT 1
qs.first() # объект или None
qs.last() # объект или None
qs.get(...) # объект или исключение
list(qs) # список объектов
bool(qs) # выполняет запрос для проверки наличия
# aggregate() словарь с результатами агрегации
from django.db.models import Avg
qs.aggregate(avg_price=Avg("price")) # {"avg_price": Decimal("...")}
count() vs len()
# count() - SELECT COUNT(*) эффективнее, не грузит объекты
count = Product.objects.filter(is_active=True).count()
# len() загружает все объекты в память, затем считает
count = len(Product.objects.filter(is_active=True))
-- count()
SELECT COUNT(*) FROM shop_product WHERE is_active = true;
-- len() - загружает всё
SELECT * FROM shop_product WHERE is_active = true ORDER BY name;
Всегда используйте count() когда нужно только количество.
exists() vs bool()
# exists() - SELECT 1 ... LIMIT 1 - проверяет только наличие
if Product.objects.filter(is_active=True, stock__gt=0).exists():
...
# bool() - загружает все объекты
if Product.objects.filter(is_active=True, stock__gt=0):
...
-- exists()
SELECT 1 FROM shop_product WHERE is_active = true AND stock > 0 LIMIT 1;
exists() останавливается при первом найденном объекте. Всегда используйте exists() вместо
bool() для проверки наличия.
Сводная таблица методов
| Метод | Возвращает | Запрос |
|---|---|---|
all() |
QuerySet | Ленивый |
filter() |
QuerySet | Ленивый |
exclude() |
QuerySet | Ленивый |
values() |
QuerySet[dict] | Ленивый |
values_list() |
QuerySet[tuple] | Ленивый |
only() |
QuerySet | Ленивый |
defer() |
QuerySet | Ленивый |
order_by() |
QuerySet | Ленивый |
get() |
объект | Немедленный |
first() |
объект/None | Немедленный |
last() |
объект/None | Немедленный |
count() |
int | Немедленный |
exists() |
bool | Немедленный |
aggregate() |
dict | Немедленный |
in_bulk() |
dict | Немедленный |
in_bulk() - словарь объектов по pk
in_bulk() получает список id и возвращает словарь {id: объект}. Полезен когда нужно быстро найти объекты по набору id без итерации:
# Получить продукты по списку id как словарь
product_ids = [1, 5, 12, 47]
products_map = Product.objects.in_bulk(product_ids)
# {1: <Product: iPhone 15>, 5: <Product: MacBook>, 12: <Product: ...>, 47: <Product: ...>}
# Доступ по id
product = products_map[5]
SELECT * FROM shop_product
WHERE id IN (1, 5, 12, 47)
ORDER BY shop_product.name;
Без id_list, загружает все объекты (как all()):
all_products = Product.objects.in_bulk()
# {1: <Product>, 2: <Product>, ...}
Можно использовать другое поле для ключа, если оно уникальное:
products_by_slug = Product.objects.in_bulk(
["iphone-15", "macbook-pro"],
field_name="slug",
)
# {"iphone-15": <Product>, "macbook-pro": <Product>}
SELECT * FROM shop_product
WHERE slug IN ('iphone-15', 'macbook-pro')
ORDER BY name;
Применяйте когда нужно многократно обратиться к объектам по id. Без in_bulk это N запросов, с in_bulk один:
# Без in_bulk: запрос на каждый элемент цикла
for item in order_items:
product = Product.objects.get(id=item.product_id) # SELECT на каждой итерации
print(product.name)
# С in_bulk: 1 запрос, затем доступ по ключу за O(1)
order_product_ids = OrderItem.objects.filter(
order__user=user
).values_list("product_id", flat=True)
products = Product.objects.in_bulk(list(order_product_ids))
for item in order_items:
product = products[item.product_id] # без запроса
print(product.name)
Практическое задание
Все задания выполняй в shell с включенным логированием SQL, чтобы видеть реальные запросы.
-
Получите все активные продукты с ценой выше 500. Проверьте через лог SQL что запрос выполнился только при итерации, а не при создании QuerySet.
-
Сравните SQL для этих двух вариантов, в чем разница?
Product.objects.filter(is_active=True).count()
len(Product.objects.filter(is_active=True))
-
Получите список названий всех категорий через
values_list("name", flat=True). Сколько запросов SQL? -
Получите имена и цены продуктов категории "phones" через
values("name", "price", "category__name"). Посмотрите на SQL, есть ли JOIN? -
Попробуйте
Product.objects.only("name")и в цикле обратитесь кp.description. Посчитайте количество запросов SQL для 5 продуктов. Почему их столько?
Возможные ошибки
Лишняя загрузка данных вместо count()
# Загружает все объекты в память ради подсчета
total = len(Product.objects.filter(is_active=True))
# Правильно
total = Product.objects.filter(is_active=True).count()
bool() вместо exists()
# Загружает объекты ради проверки наличия
if Product.objects.filter(stock=0):
notify_manager()
# Правильно
if Product.objects.filter(stock=0).exists():
notify_manager()
Повторное создание QuerySet в цикле
# Два разных QuerySet = два запроса
for product in Product.objects.filter(is_active=True):
if product in Product.objects.filter(price__gt=500): # новый запрос на каждой итерации!
...
# Правильно - один QuerySet
expensive_active = Product.objects.filter(is_active=True, price__gt=500)
for product in expensive_active:
...
Обращение к отложенному полю в цикле
# N дополнительных запросов для N объектов
products = Product.objects.only("name")
for p in products:
print(p.description) # запрос для каждого объекта
Связь со следующим уроком
В уроке 2.3 разберем lookups, все виды условий фильтрации: __gt, __lt, __contains, __in, __range и другие. Также покажу как фильтровать через связанные модели используя двойное подчеркивание.
Подписывайтесь на мой Telegram канал
Если вам нужен ментор и вы хотите научиться веб-разработке, узнать подробнее
Авторизуйтесь, чтобы оставить комментарий.
Нет комментариев.
Тут может быть ваша реклама
Пишите info@aisferaic.ru