✅ Решения практикума 6

⚡ Ключевые паттерны решений

# Задача 1: Sum(F*F)
Product.objects.aggregate(total_value=Sum(F('price') * F('quantity')))['total_value']
# Задача 2: group by category
Product.objects.values('category__name').annotate(average_price=Avg('price')).order_by('category__name')
# Задача 3: Min/Max
Product.objects.aggregate(min_price=Min('price'), max_price=Max('price'))
# Задача 7: ExpressionWrapper
ProductDetail.objects.annotate(lifetime=ExpressionWrapper(F('expiration_date')-F('manufacturing_date'), output_field=fields.DurationField())).aggregate(avg=Avg('lifetime'))['avg'].days
# Задача 13: timezone
Order.objects.filter(order_date__gte=timezone.now()-timezone.timedelta(days=30))
# Задача 15: slice
Product.objects.order_by('-price')[:10]

Агрегация

Задача 1: Общая стоимость всех продуктов

Идея: aggregate() возвращает одно значение для всего QuerySet. Перемножаем price и quantity с помощью F() — всё вычисляется на уровне SQL.

from django.db.models import Sum, F
from store.models import Product

total_value = Product.objects.aggregate(
    total_value=Sum(F('price') * F('quantity'))
)['total_value']

print(f"Общая стоимость всех продуктов: {total_value}")

Пояснение: F('price') * F('quantity') создаёт выражение на уровне SQL. Sum() суммирует результаты по всем строкам. aggregate() выполняет запрос и возвращает словарь — берём значение по ключу 'total_value'.

Задача 2: Средняя цена продуктов по категориям

Идея: Паттерн GROUP BY в Django: values('group_field').annotate(aggregate_func).

from django.db.models import Avg
from store.models import Product, Category

average_price_by_category = Product.objects.values('category__name').annotate(
    average_price=Avg('price')
).order_by('category__name')

for entry in average_price_by_category:
    print(f"Категория: {entry['category__name']}, Средняя цена: {entry['average_price']}")

Пояснение: values('category__name') — группировка по имени категории (через ForeignKey). annotate(average_price=Avg('price')) — добавляет вычисленное поле к каждой группе. Результат — QuerySet словарей с ключами category__name и average_price.

Задача 3: Самый дешевый и самый дорогой продукт

Идея: Min() и Max() можно вызвать отдельно или совместить в одном aggregate().

from django.db.models import Min, Max
from store.models import Product

cheapest_product = Product.objects.aggregate(
    min_price=Min('price')
)['min_price']

most_expensive_product = Product.objects.aggregate(
    max_price=Max('price')
)['max_price']

print(f"Самый дешевый продукт: {cheapest_product}")
print(f"Самый дорогой продукт: {most_expensive_product}")

Пояснение: Два отдельных вызова aggregate() — каждый делает SQL-запрос. Можно объединить: Product.objects.aggregate(min_price=Min('price'), max_price=Max('price')) — один запрос.

Задача 4: Заказы — количество и сумма по клиентам

Идея: Группировка по нескольким полям клиента, два аннотируемых поля одновременно.

from django.db.models import Count, Sum, F
from store.models import Order, Customer, OrderItem

orders_summary_by_customer = Order.objects.values(
    'customer__first_name',
    'customer__last_name'
).annotate(
    order_count=Count('id'),
    total_spent=Sum(F('order_items__price') * F('order_items__quantity'))
).order_by('customer__last_name')

for entry in orders_summary_by_customer:
    print(
        f"Клиент: {entry['customer__first_name']} {entry['customer__last_name']}, "
        f"Заказов: {entry['order_count']}, "
        f"Общая сумма: {entry['total_spent']}"
    )

Пояснение: values() с несколькими полями группирует по комбинации значений. Count('id') считает строки в группе. Sum(F('order_items__price') * F('order_items__quantity')) — сумма по всем позициям заказа через связь order_items.

Задача 5: Общий вес продуктов по категориям

Идея: Доступ к данным из связанной модели ProductDetail через двойное подчёркивание (details__weight).

from django.db.models import Sum, F
from store.models import Product

total_weight_by_category = Product.objects.values('category__name').annotate(
    total_weight=Sum(F('details__weight') * F('quantity'))
).order_by('category__name')

for entry in total_weight_by_category:
    print(f"Категория: {entry['category__name']}, Общий вес: {entry['total_weight']}")

Пояснение: details__weight — обращение к полю weight в модели ProductDetail через RelatedName details. Умножаем на quantity из самого Product.

Задача 6: Количество продуктов у поставщиков

from django.db.models import Count
from store.models import Product

products_count_by_supplier = Product.objects.values('supplier__name').annotate(
    product_count=Count('id')
).order_by('supplier__name')

for entry in products_count_by_supplier:
    print(f"Поставщик: {entry['supplier__name']}, Количество продуктов: {entry['product_count']}")

Пояснение: Паттерн аналогичен задаче 2. Count('id') подсчитывает строки в каждой группе поставщиков.

Задача 7: Средняя продолжительность жизни продуктов

Идея: Вычитание двух DateField требует ExpressionWrapper с явным output_field=fields.DurationField().

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} дней")

Пояснение: Шаги: 1) annotate(lifetime=...) — создаём поле разницы дат для каждой строки; 2) aggregate(Avg('lifetime')) — вычисляем среднее по всем строкам; 3) результат — объект timedelta, у которого берём .days. ExpressionWrapper необходим, чтобы Django знал тип результата и мог правильно строить SQL.

Задача 8: Общая сумма заказов для каждого клиента

from django.db.models import Sum, F
from store.models import Order, OrderItem

total_spent_by_customer = Order.objects.values(
    'customer__first_name',
    'customer__last_name'
).annotate(
    total_spent=Sum(F('order_items__price') * F('order_items__quantity'))
).order_by('customer__last_name')

for entry in total_spent_by_customer:
    print(
        f"Клиент: {entry['customer__first_name']} {entry['customer__last_name']}, "
        f"Общая сумма заказов: {entry['total_spent']}"
    )

Пояснение: Похоже на задачу 4, но без Count — только итоговая сумма через связь order_items.

Сортировка (order_by)

Задача 9: Сортировка продуктов по цене

from store.models import Product

# Сортировка по цене по убыванию
products_descending = Product.objects.order_by('-price')

print("Продукты по убыванию цены:")
for product in products_descending:
    print(f"{product.name}: {product.price}")

Пояснение: order_by('-price') — префикс - означает DESC (убывание). SQL: ORDER BY price DESC.

Задача 10: Сортировка заказов по общей стоимости

from django.db.models import Sum, F
from store.models import Order

# Сортировка заказов по общей стоимости
orders_by_total = Order.objects.annotate(
    total=Sum(F('order_items__price') * F('order_items__quantity'))
).order_by('-total')

print("Заказы по общей стоимости:")
for order in orders_by_total:
    print(f"Заказ {order.id}, Общая стоимость: {order.total}")

Пояснение: annotate() добавляет поле total к каждому объекту Order, затем order_by('-total') сортирует по нему.

Задача 11: Сортировка адресов по стране и городу

from store.models import Address

# Сортировка по стране и городу
addresses_sorted = Address.objects.order_by('country', 'city')

print("Адреса по стране и городу:")
for address in addresses_sorted:
    print(f"{address.country}, {address.city}, {address.street}")

Пояснение: Несколько полей в order_by() — аналог SQL ORDER BY country ASC, city ASC. Приоритет — первый аргумент.

Задача 12: Сортировка заказов по количеству позиций

from django.db.models import Count
from store.models import Order

# Сортировка по количеству позиций заказа
orders_by_item_count = Order.objects.annotate(
    item_count=Count('order_items')
).order_by('-item_count')

print("Заказы по количеству позиций:")
for order in orders_by_item_count:
    print(f"Заказ {order.id}, Количество позиций: {order.item_count}")

Пояснение: Count('order_items') подсчитывает связанные объекты OrderItem для каждого заказа. Сортировка по убыванию — заказы с наибольшим количеством позиций первые.

Временные метки

Задача 13: Заказы за последний месяц

from django.utils import timezone
from store.models import Order

# Дата один месяц назад
one_month_ago = timezone.now() - timezone.timedelta(days=30)

# Заказы за последний месяц
recent_orders = Order.objects.filter(order_date__gte=one_month_ago)

print("Заказы за последний месяц:")
for order in recent_orders:
    print(f"Заказ {order.id}, Дата: {order.order_date}")

Пояснение: timezone.now() — текущий момент с учётом часового пояса. timezone.timedelta(days=30) — 30 дней. Лукап __gte — greater than or equal (>=). SQL: WHERE order_date >= '...'.

Срезы

Задача 14: Первые 5 продуктов

from store.models import Product

# Первые 5 продуктов
first_five_products = Product.objects.all()[:5]

print("Первые 5 продуктов:")
for product in first_five_products:
    print(f"{product.name}: {product.price}")

Пояснение: Срез [:5] транслируется в SQL LIMIT 5. QuerySet lazy — запрос выполняется только при итерации в цикле.

Задача 15: 10 самых дорогих продуктов

from store.models import Product

# 10 самых дорогих продуктов
top_ten_expensive_products = Product.objects.order_by('-price')[:10]

print("10 самых дорогих продуктов:")
for product in top_ten_expensive_products:
    print(f"{product.name}: {product.price}")

Пояснение: Комбинация order_by('-price')[:10] — сначала сортировка по убыванию цены, затем ограничение. SQL: ORDER BY price DESC LIMIT 10.

← К оглавлению урока    Ошибки →