Q objects Django ORM | Курс Django ORM урок 3.1
Цель урока
Разобраться с Q objects, инструментом для построения сложных условий фильтрации с OR, AND, NOT. Понять как динамически конструировать запросы и почему exclude() с несколькими условиями ведет себя иначе, чем можно ожидать.
Необходимые знания
Проблема, которую решают Q objects
Обычный filter() с несколькими аргументами, это всегда AND:
# AND: активные продукты с ценой > 500
Product.objects.filter(is_active=True, price__gt=500)
Но что если нужно OR? Стандартный filter() этого не умеет. Именно для этого существует Q:
from django.db.models import Q
# OR: продукты дешевле 100 ИЛИ дороже 1000
Product.objects.filter(Q(price__lt=100) | Q(price__gt=1000))
Синтаксис Q objects
from django.db.models import Q
Q(поле__lookup=значение)
Q objects поддерживают три оператора:
Q(a) & Q(b) # AND - оба условия
Q(a) | Q(b) # OR - хотя бы одно условие
~Q(a) # NOT - инверсия условия
OR условия
# Продукты в категории "phones" ИЛИ дешевле 50
Product.objects.filter(
Q(category__slug="phones") | Q(price__lt=50)
)
SELECT shop_product.*
FROM shop_product
INNER JOIN shop_category ON shop_product.category_id = shop_category.id
WHERE (shop_category.slug = 'phones' OR shop_product.price < 50)
ORDER BY shop_product.name;
AND условия
Q objects можно комбинировать через &, это эквивалент нескольких аргументов в filter():
# Эти три варианта дают одинаковый SQL
Product.objects.filter(is_active=True, price__gt=500)
Product.objects.filter(Q(is_active=True) & Q(price__gt=500))
Product.objects.filter(Q(is_active=True), Q(price__gt=500)) # запятая = AND
WHERE is_active = true AND price > 500
Смешивать Q objects и обычные kwargs в одном filter() можно, но Q objects должны идти первыми:
# Правильно
Product.objects.filter(Q(price__gt=500) | Q(stock=0), is_active=True)
# Ошибка, kwargs не могут идти перед Q objects
Product.objects.filter(is_active=True, Q(price__gt=500) | Q(stock=0)) # SyntaxError
NOT условия
# Все продукты кроме телефонов
Product.objects.filter(~Q(category__slug="phones"))
WHERE NOT (shop_category.slug = 'phones')
Разница между ~Q() и exclude():
# Эти два запроса эквивалентны для простых условий
Product.objects.filter(~Q(category__slug="phones"))
Product.objects.exclude(category__slug="phones")
Но exclude() с несколькими условиями работает иначе, чем ~Q() с несколькими условиями, об этом ниже.
Сложные комбинации
# телефоны дороже 500 ИЛИ ноутбуки дешевле 1000
Product.objects.filter(
Q(category__slug="phones", price__gt=500) |
Q(category__slug="laptops", price__lt=1000)
)
SELECT shop_product.*
FROM shop_product
INNER JOIN shop_category ON shop_product.category_id = shop_category.id
WHERE (
(shop_category.slug = 'phones' AND shop_product.price > 500)
OR
(shop_category.slug = 'laptops' AND shop_product.price < 1000)
)
ORDER BY shop_product.name;
Скобки в SQL расставляются согласно порядку операций Python: & имеет приоритет над |,
как * над +. При сомнении, группируйте явно через скобки:
# Явная группировка
Product.objects.filter(
(Q(category__slug="phones") & Q(price__gt=500)) |
(Q(category__slug="laptops") & Q(price__lt=1000))
)
exclude() с несколькими условиями - тонкий момент
Это одна из самых частых источников неожиданного поведения в Django ORM.
# Что возвращает этот запрос?
Product.objects.exclude(category__slug="phones", price__gt=500)
Интуитивно ожидаете: "все продукты, которые НЕ являются дорогими телефонами". То есть, ноутбуки, книги и дешевые телефоны.
На самом деле:
WHERE NOT (shop_category.slug = 'phones' AND shop_product.price > 500)
По закону де Моргана: NOT (A AND B) = NOT A OR NOT B. Это значит, исключаются строки, где оба условия выполнены одновременно. Дорогие телефоны исключаются. Дешевые телефоны, остаются.
Сравним три варианта:
# 1. exclude() с несколькими условиями - NOT (A AND B) = NOT A OR NOT B
Product.objects.exclude(category__slug="phones", price__gt=500)
# SQL: WHERE NOT (slug = 'phones' AND price > 500)
# Возвращает: всё кроме (телефоны И дорогие) = дешевые телефоны ВКЛЮЧЕНЫ
# 2. ~Q() с OR (де Морган: NOT (A AND B) = NOT A OR NOT B) - то же самое что вариант 1
Product.objects.filter(~Q(category__slug="phones") | ~Q(price__gt=500))
# 3. Два отдельных exclude() - NOT A AND NOT B
Product.objects.exclude(category__slug="phones").exclude(price__gt=500)
# SQL: WHERE slug != 'phones' AND price <= 500
# Возвращает только не телефоны с ценой <= 500 - это другой результат!
# 4. ~Q() с AND (правильный вариант для "не дорогие телефоны")
Product.objects.filter(~(Q(category__slug="phones") & Q(price__gt=500)))
# SQL: WHERE NOT (slug = 'phones' AND price > 500) - то же что вариант 1
Правило: если хотите исключить строки, где выполнены все условия из набора, используйте один exclude() или ~(Q(...) & Q(...)). Если хотите исключить строки, где выполнено хотя бы одно: используйте два exclude() или ~Q(...) & ~Q(...).
Динамическое построение запросов
Q objects позволяют строить фильтры программно, это их главное преимущество перед статическими kwargs.
Накопление условий
def search_products(name=None, min_price=None, max_price=None, category_slug=None, in_stock=None):
filters = Q() # пустой Q - нейтральный элемент для AND
if name:
filters &= Q(name__icontains=name)
if min_price is not None:
filters &= Q(price__gte=min_price)
if max_price is not None:
filters &= Q(price__lte=max_price)
if category_slug:
filters &= Q(category__slug=category_slug)
if in_stock:
filters &= Q(stock__gt=0)
return Product.objects.filter(filters)
# Поиск: дорогие ноутбуки в наличии
results = search_products(min_price=1000, category_slug="laptops", in_stock=True)
WHERE price >= 1000
AND shop_category.slug = 'laptops'
AND stock > 0
ORDER BY name;
Пустой Q() не добавляет условий, это удобно для начального значения при накоплении:
Q() & Q(price__gt=500) # эквивалентно Q(price__gt=500)
Q() | Q(price__gt=500) # эквивалентно Q(price__gt=500)
Построение OR из списка
# Найти продукты с любым из этих слов в названии
keywords = ["Pro", "Max", "Ultra"]
q = Q()
for keyword in keywords:
q |= Q(name__icontains=keyword)
Product.objects.filter(q)
WHERE (
UPPER(name) LIKE UPPER('%Pro%')
OR UPPER(name) LIKE UPPER('%Max%')
OR UPPER(name) LIKE UPPER('%Ultra%')
)
Элегантнее через reduce():
from functools import reduce
import operator
keywords = ["Pro", "Max", "Ultra"]
q = reduce(operator.or_, [Q(name__icontains=kw) for kw in keywords])
Product.objects.filter(q)
Условные фильтры с OR по разным полям
# Поиск: совпадение по имени ИЛИ по описанию
search_term = "wireless"
Product.objects.filter(
Q(name__icontains=search_term) | Q(description__icontains=search_term)
)
WHERE (
UPPER(name) LIKE UPPER('%wireless%')
OR UPPER(description) LIKE UPPER('%wireless%')
)
Q objects и exclude() через связанные модели
При работе со связями exclude() имеет дополнительную тонкость:
# Найти продукты у которых НЕТ отзывов с rating < 3
# все продукты с хорошими отзывами или без отзывов
# Вариант 1 - может работать неожиданно
Product.objects.exclude(reviews__rating__lt=3)
-- Django делает LEFT JOIN и исключает продукты
-- у которых ХОТЯ БЫ ОДИН отзыв с rating < 3
SELECT DISTINCT shop_product.*
FROM shop_product
WHERE NOT (shop_product.id IN (
SELECT product_id FROM shop_review WHERE rating < 3
))
Это корректное поведение, исключаются продукты, у которых есть хотя бы один плохой отзыв. Если у продукта 5 отзывов с rating 5 и один с rating 2, он будет исключен.
Для точного контроля используйте Exists() (разберем в уроке 3.4) или явный subquery.
Практическое задание
1) Найдите все продукты дешевле 100 ИЛИ дороже 1500. Посмотрите на SQL.
2) Реализуйте функцию filter_orders(status=None, min_total=None, user_id=None), которая принимает опциональные параметры и строит QuerySet динамически через Q objects.
3) Найдите все активные продукты, у которых название содержит "Pro" ИЛИ "Max" ИЛИ "Ultra". Используйте reduce + operator.or_.
4) Объясните разницу в результатах:
# A
Product.objects.exclude(is_active=False, stock=0)
# B
Product.objects.exclude(is_active=False).exclude(stock=0)
Напишите SQL для каждого варианта и опиши что каждый возвращает.
5) Найдите все заказы со статусом "pending" ИЛИ "processing" у пользователей с id от 1 до 5.
Возможные ошибки
kwargs после Q objects в filter()
# SyntaxError
Product.objects.filter(is_active=True, Q(price__gt=500) | Q(stock=0))
# Правильно когда Q objects стоят первыми
Product.objects.filter(Q(price__gt=500) | Q(stock=0), is_active=True)
Неправильное понимание exclude() с несколькими условиями
# Ошибка если хотели исключить только дорогие ноутбуки, но исключили больше
Product.objects.exclude(category__slug="laptops").exclude(price__gt=1000)
# SQL: WHERE slug != 'laptops' AND price <= 1000
# Исключает ВСЕ ноутбуки и ВСЕ товары дороже 1000, а не только дорогие ноутбуки
# Правильно, исключить только дорогие ноутбуки (строки где ОБА условия true):
Product.objects.exclude(category__slug="laptops", price__gt=1000)
# или
Product.objects.filter(~(Q(category__slug="laptops") & Q(price__gt=1000)))
# SQL: WHERE NOT (slug = 'laptops' AND price > 1000)
Пустой Q в неправильном месте
# Q() - нейтральный элемент, не влияет на результат
Product.objects.filter(Q()) # возвращает все продукты, нет условий
# Но это не ошибка, используйте для динамического накопления
Избыточное использование Q там, где достаточно kwargs
# Избыточно
Product.objects.filter(Q(is_active=True) & Q(stock__gt=0))
# Проще и читабельнее
Product.objects.filter(is_active=True, stock__gt=0)
Q objects нужны только когда необходим OR, NOT или динамическое построение запроса.
Связь со следующим уроком
В уроке 3.2 разберем F expressions, способ выполнять вычисления на уровне базы данных без загрузки объектов в Python. Это решает проблему race condition при обновлении счетчиков и позволяет сравнивать поля одной модели между собой прямо в SQL.
Подписывайтесь на мой Telegram канал
Если вам нужен ментор и вы хотите научиться веб-разработке, узнать подробнее
Авторизуйтесь, чтобы оставить комментарий.
Нет комментариев.
Тут может быть ваша реклама
Пишите info@aisferaic.ru