Django ORM

Q objects Django ORM | Курс Django ORM урок 3.1

Q objects Django ORM | Курс Django ORM урок 3.1
Mikhail
Автор
Mikhail
Опубликовано 16.03.2026
0,0
Views 3

Цель урока

Разобраться с 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.


<< Урок 2.5

Урок 3.2 >>


Подписывайтесь на мой Telegram канал

Если вам нужен ментор и вы хотите научиться веб-разработке, узнать подробнее

Авторизуйтесь, чтобы оставить комментарий.

Комментариев: 0

Нет комментариев.

Тут может быть ваша реклама

Пишите info@aisferaic.ru

Похожие туториалы