Представьте, что у вас есть интернет-магазин, в котором нужно найти всех покупателей, которые потратили больше определенной суммы за последний месяц. Как это сделать? Именно это мы и узнаем, углубившись в annotate() и фильтрацию!
Самое лучшее, что Django позволяет совмещать аннотации и фильтрацию для получения сложных и изысканных запросов. После этого вам уже не нужно будет писать SQL-запросы вручную для 90% типичных задач. Давайте разберёмся, как это делать.
Фильтрация аннотированных данных: основная идея
Когда мы применяем аннотации в Django ORM, мы создаем дополнительные вычисляемые поля, которые становятся частью нашего QuerySet. После этого фильтрация с этими новыми полями становится доступной — и именно это позволяет комбинировать мощь аннотаций с гибкостью фильтров.
Однако здесь есть нюансы. Важно понимать порядок применения аннотаций и фильтров. Давайте рассмотрим, как это работает.
Начнем с простого примера. Представим, что у нас есть следующая модель:
from django.db import models
class Order(models.Model):
customer = models.CharField(max_length=255)
amount = models.DecimalField(max_digits=10, decimal_places=2)
created_at = models.DateTimeField(auto_now_add=True)
У нас есть заказы с полями: customer (имя покупателя), amount (сумма заказа) и created_at (дата создания заказа). Нужно найти всех покупателей, которые сделали общих заказов на сумму больше 1000.
Для этого нам потребуется:
- Использовать
annotate()для вычисления суммы всех заказов каждого покупателя. - Применить фильтр для этой аннотации.
Пример кода:
from django.db.models import Sum
from .models import Order
# Вычисляем сумму заказов каждого покупателя
customers = Order.objects.values('customer') \
.annotate(total_spent=Sum('amount')) \
.filter(total_spent__gt=1000)
for customer in customers:
print(customer)
Здесь:
values('customer')группирует данные по полюcustomer.annotate(total_spent=Sum('amount'))добавляет вычисляемое полеtotal_spent, содержащее сумму всех заказов для каждого покупателя.filter(total_spent__gt=1000)фильтрует только тех, у когоtotal_spentбольше 1000.
Результат: список словарей, где каждый словарь содержит customer и его total_spent.
Порядок аннотаций и фильтров
Порядок действия методов в Django ORM имеет значение. Давайте посмотрим, что произойдет, если мы поменяем методы annotate() и filter():
# Неправильный порядок!
customers = Order.objects.filter(amount__gt=1000) \
.values('customer') \
.annotate(total_spent=Sum('amount'))
Этот запрос не даст нужного результата, потому что мы сначала фильтруем заказы на уровне отдельных записей (amount__gt=1000), а затем аннотируем данные. В данном случае в расчетах будут участвовать только заказы с суммой больше 1000, что не соответствует нашей задаче.
Секрет успеха прост: всегда аннотируйте данные перед фильтрацией, если хотите фильтровать по аннотированным значениям.
Сложные примеры фильтрации с аннотациями
Теперь рассмотрим примеры посложнее. Мы добавим несколько "реальных" кейсов, где фильтрация аннотированных данных будет незаменима.
Пример 1: Фильтрация по количеству заказов
Допустим, мы хотим найти покупателей, которые сделали больше 5 заказов.
from django.db.models import Count
# Считаем количество заказов каждого покупателя
customers = Order.objects.values('customer') \
.annotate(order_count=Count('id')) \
.filter(order_count__gt=5)
for customer in customers:
print(customer['customer'], "заказов:", customer['order_count'])
Здесь:
Count('id')считает количество строк для каждого покупателя.filter(order_count__gt=5)ограничивает результат только покупателями, у которых больше 5 заказов.
Пример 2: Выборка покупателей с "богатыми" заказами
Нам нужно найти всех покупателей, у которых есть хотя бы одна покупка на сумму больше 500.
from django.db.models import Max
# Считаем максимальную сумму заказа для каждого покупателя
customers = Order.objects.values('customer') \
.annotate(max_order=Max('amount')) \
.filter(max_order__gt=500)
for customer in customers:
print(customer['customer'], "максимальный заказ:", customer['max_order'])
Пример 3: Фильтрация по месячным тратам
Теперь предположим, что мы хотим вычислить суммарные траты покупателей за текущий месяц.
from django.db.models import Sum
from django.utils.timezone import now
from datetime import timedelta
# Получаем дату начала месяца
start_date = now().replace(day=1)
end_date = start_date + timedelta(days=31)
# Фильтруем заказы за текущий месяц и аннотируем траты покупателей
customers = Order.objects.filter(created_at__range=(start_date, end_date)) \
.values('customer') \
.annotate(monthly_spent=Sum('amount')) \
.filter(monthly_spent__gt=1000)
for customer in customers:
print(f"{customer['customer']} потратил: {customer['monthly_spent']} за текущий месяц")
Тут:
filter(created_at__range=(start_date, end_date))ограничивает выборку заказов текущим месяцем.annotate(monthly_spent=Sum('amount'))вычисляет сумму всех заказов за этот период.filter(monthly_spent__gt=1000)оставляет только покупателей с общими тратами больше 1000.
Типичные ошибки при фильтрации аннотированных данных
Фильтрация до аннотации. Как мы уже обсуждали, важно фильтровать аннотированные данные только после использования
annotate(). Если сделать наоборот, фильтрация будет работать не на аннотированных значениях, а на "сырых" данных, что приведет к неправильным результатам.Неправильное использование
values(). Использованиеvalues()может ограничить выбранные поля, что иногда ведет к ошибкам. Например, если вы используетеvalues('customer'), то другие поля, даже аннотированные, могут быть исключены. Чтобы этого избежать, будьте внимательны с тем, как именно формируется ваш запрос.Выборка слишком большого объема данных. Аннотации могут быть ресурсоемкими, особенно для больших таблиц. Используйте индексы для ключевых полей, чтобы ускорить выполнение запросов.
Когда это пригодится?
Во множестве случаев на самом деле. Например:
- Анализ покупок: подсчет количества или суммы заказов, выборка "топовых" покупателей.
- Подготовка отчетов: например, для построения аналитики по тратам пользователей.
- Оптимизация запросов: выбрать данные для отображения без загрузки лишних записей.
Эти техники полезны и в реальных проектах, будь то интернет-магазины, системы управления заказами или CRM.
Итак, с основами мы разобрались. Надеемся, что в вашем "дебагере жизни" аннотации и фильтрация теперь работают точно. А если вдруг что-то забудете или захотите углубиться, взгляните в документацию Django QuerySet, там много полезного.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ