📖 Теория: ORM-инструменты практикума 6
⚡ Ключевые концепции практикума 6
- aggregate(): одно значение для всего QuerySet —
Model.objects.aggregate(total=Sum(...)) - annotate(): вычисленное поле для каждой строки —
.values('group').annotate(avg=Avg(...)) - ExpressionWrapper: арифметика над полями с явным типом —
ExpressionWrapper(F('a') - F('b'), output_field=DurationField()) - order_by:
order_by('-price')— убывание; несколько полей —order_by('country', 'city') - Срезы:
[:5]— первые 5;order_by('-price')[:10]— топ-10 дорогих - Временные метки:
timezone.now() - timezone.timedelta(days=30)+filter(date__gte=...)
1. Агрегация — aggregate() vs annotate()
Оба метода используют агрегирующие функции (Sum, Avg, Min, Max, Count), но работают по-разному:
| Метод | Что делает | Возвращает |
|---|---|---|
aggregate() |
Вычисляет одно значение для всего QuerySet | Словарь {'key': value} |
annotate() |
Добавляет вычисленное поле к каждому объекту/группе | QuerySet с дополнительными полями |
# aggregate — одно значение для всего QuerySet
from django.db.models import Sum, F
from store.models import Product
result = Product.objects.aggregate(
total_value=Sum(F('price') * F('quantity'))
)
# result == {'total_value': Decimal('12345.67')}
# annotate — значение для каждой группы
from django.db.models import Avg
averages = Product.objects.values('category__name').annotate(
average_price=Avg('price')
).order_by('category__name')
# QuerySet с полями 'category__name' и 'average_price'
2. F-объекты в агрегациях
F() позволяет ссылаться на значение поля модели прямо в запросе, не загружая объекты в Python. Особенно полезен в арифметических выражениях для Sum:
from django.db.models import Sum, F
# Итого = цена * количество — вычисляется на уровне SQL
total = Product.objects.aggregate(
total_value=Sum(F('price') * F('quantity'))
)['total_value']
3. ExpressionWrapper и DurationField
Когда нужно вычесть одну дату из другой и получить разницу (тип timedelta), используется ExpressionWrapper с явным output_field:
from django.db.models import Avg, F, ExpressionWrapper, fields
from store.models import ProductDetail
average_lifetime = ProductDetail.objects.annotate(
lifetime=ExpressionWrapper(
F('expiration_date') - F('manufacturing_date'),
output_field=fields.DurationField() # обязательно!
)
).aggregate(average_lifetime=Avg('lifetime'))['average_lifetime']
print(f"Средняя продолжительность жизни: {average_lifetime.days} дней")
Важно: без
output_field=fields.DurationField() Django не знает, какой тип у результата вычитания дат — запрос упадёт с FieldError.
4. order_by() — сортировка
Сортировка результатов QuerySet. Префикс - означает убывание:
# Убывание по цене
products = Product.objects.order_by('-price')
# Несколько полей: сначала по стране, потом по городу
addresses = Address.objects.order_by('country', 'city')
# Сортировка по аннотированному полю
from django.db.models import Count
orders = Order.objects.annotate(
item_count=Count('order_items')
).order_by('-item_count')
5. Временные метки и timezone
Для работы с датами в Django нужно использовать django.utils.timezone, чтобы корректно учитывать часовые пояса:
from django.utils import timezone
from store.models import Order
# Дата 30 дней назад (timezone-aware)
one_month_ago = timezone.now() - timezone.timedelta(days=30)
# Заказы за последний месяц
recent_orders = Order.objects.filter(order_date__gte=one_month_ago)
Совет: всегда используйте
timezone.now() вместо datetime.now() — последний возвращает naive datetime без timezone, что может вызвать предупреждения или ошибки при USE_TZ = True.
6. Срезы QuerySet
Срезы работают как Python-срезы и транслируются в SQL LIMIT/OFFSET:
from store.models import Product
# Первые 5 продуктов (LIMIT 5)
first_five = Product.objects.all()[:5]
# Топ-10 дорогих продуктов (ORDER BY price DESC LIMIT 10)
top_ten = Product.objects.order_by('-price')[:10]
Ограничение: нельзя применять отрицательные индексы к QuerySet (
[-1] не работает). Для последнего элемента используйте .order_by('-id').first().
7. Связанные поля в lookup (__)
В практикуме часто используются двойные подчёркивания для перехода по связям:
# Группировка по имени категории (через ForeignKey)
Product.objects.values('category__name').annotate(avg=Avg('price'))
# Группировка по клиенту (через ForeignKey в Order)
Order.objects.values('customer__first_name', 'customer__last_name').annotate(
order_count=Count('id')
)
# Доступ к полям связанной модели ProductDetail
Product.objects.values('category__name').annotate(
total_weight=Sum(F('details__weight') * F('quantity'))
)