DeepLearning Blog

Джанго - путь воина

Published Feb. 26, 2026, 8:22 a.m. by a.glazyrin
image

1. Core Django

🔄 Жизненный цикл запроса (Детально)

1. WSGI — входные ворота
Когда сервер получает запрос, он вызывает callable-объект Django — application. Это функция в wsgi.py.

2. Middleware (Request Phase)
Запрос проходит через каждый middleware в MIDDLEWARE списке (сверху вниз). У каждого middleware может быть метод process_request(request). * Если какой-то middleware возвращает HttpResponse, цепочка прерывается, и ответ сразу идет назад. * Здесь происходит:
* SessionMiddleware: request.session = ...
* AuthenticationMiddleware: request.user = ...
* CsrfViewMiddleware: проверка токена.

3. URL Resolver * Django берет request.path_info. * Проходит по всем паттернам в urlpatterns, пока не находит совпадение. * Выполняется include(), если есть, и паттерны вложенных приложений проверяются рекурсивно. * Вызывается view с аргументами request и параметрами из URL (kwargs).

4. View (Три слота для логики) * Декораторы: Выполняются ДО входа в view (например, @login_required, @cache_page). * Тело view: Основная логика. Если это class-based view, Django вызывает нужный метод (get, post), проходя через миксины. * Обработка исключений: Если во view возникло исключение, Django ищет подходящий обработчик (например, для 404, 500), который можно кастомизировать через handler404 в urls.py.

5. Middleware (Response Phase) * Ответ проходит через все middleware, но теперь в обратном порядке (снизу вверх). * Вызывается метод process_response(request, response). * Пример: GZipMiddleware здесь сжимает ответ, если клиент поддерживает gzip.

6. Формирование HTTP ответа * WSGI-сервер преобразует Django-объект HttpResponse в байты и отправляет клиенту. * Если в ответе был middleware, который изменил заголовки или тело — всё фиксируется на этом этапе.

🛡️ Middleware (Подробно)

Как создать свой middleware (два способа):

Как функция:

def simple_middleware(get_response):
    def middleware(request):
        # Код до view (request phase)
        response = get_response(request)
        # Код после view (response phase)
        return response
    return middleware

Как класс:

class SimpleMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request):
        # До view
        response = self.get_response(request)
        # После view
        return response

Реальные примеры использования: * django.middleware.locale.LocaleMiddleware — определяет язык пользователя по заголовку Accept-Language или сессии и устанавливает request.LANGUAGE_CODE. * django.middleware.clickjacking.XFrameOptionsMiddleware — добавляет заголовок X-Frame-Options: DENY, запрещая встраивать сайт в iframe на чужих сайтах.

⚔️ CBV vs FBV (Битва за архитектуру)

Когда FBV выигрывает: * Уникальная логика: Вьюха делает что-то очень специфичное, что не вписывается в стандартные глаголы CRUD. * Читаемость для новичков: Любой разработчик сразу поймет поток выполнения. * Декораторы: Легко навешивать на функции.

Когда CBV непобедимы: * CRUD для модели: 5 минут на создание списка, деталки, создания, обновления, удаления. * Переиспользование: Создали миксин CacheControlMixin, добавили в 10 вьюх — готово. * Встроенная пагинация, фильтрация: ListView из коробки.

Проблема CBV — "магия":

class MyView(ListView):
    model = Product
    template_name = "shop/list.html"
    context_object_name = "products"

Где здесь запрос? Откуда берется пагинация? Нужно знать, что BaseListView.get() вызывает self.get_queryset(), а MultipleObjectMixin добавляет пагинацию.

📡 Signals (Сигналы под микроскопом)

Типичная ошибка — импорт сигналов:
Сигналы нужно импортировать, чтобы они зарегистрировались. Обычно это делают в ready() методе конфига приложения:

# apps.py
class MyAppConfig(AppConfig):
    name = 'myapp'

    def ready(self):
        import myapp.signals  # здесь лежат все обработчики

Слабые места сигналов: * Трудно отлаживать: Непонятно, откуда пришло изменение. * Проблемы с циклами: Сохраняешь объект → сигнал post_save → меняешь связанный объект → его post_save снова триггерит первый сигнал. * Транзакции: Сигнал выполняется в той же транзакции, что и сохранение. Если в сигнале ошибка — всё откатится. Это хорошо, но может быть неожиданно.

Решение для транзакций: on_commit — выполнить код только после успешного коммита транзакции.

from django.db import transaction

def my_signal_handler(sender, instance, **kwargs):
    transaction.on_commit(lambda: send_email(instance.id))

🧠 Context Processors (Глубже)

Как это работает технически:
1. В настройках TEMPLATES есть опция 'context_processors'.
2. Это список путей к функциям.
3. При рендере шаблона Django вызывает каждую функцию с аргументом request.
4. Все возвращенные словари мержатся в один контекст.

Пример кастомного процессора:

# myapp/context_processors.py
def unread_notifications(request):
    if request.user.is_authenticated:
        count = Notification.objects.filter(user=request.user, read=False).count()
        return {'unread_count': count}
    return {}

Теперь в любом шаблоне доступна переменная {{ unread_count }}.


2. ORM и Базы данных (Углубленно)

🔍 N+1: Анатомия проблемы

Как увидеть N+1 в реальности:

# views.py
authors = Author.objects.all()
for author in authors:
    print(author.books.count())  # count() делает запрос для каждого автора!

Здесь: 1 запрос на авторов + N запросов на count().

Решение через annotate:

from django.db.models import Count
authors = Author.objects.annotate(book_count=Count('books'))
for author in authors:
    print(author.book_count)  # 1 запрос всего!

Разница select_related vs prefetch_related технически: * select_related: Делает INNER/LEFT JOIN в SQL. Работает только для одиночных связей (ForeignKey, OneToOne). Результат — одна большая таблица. * prefetch_related: Делает отдельный запрос: SELECT * FROM related_table WHERE foreign_key_id IN (список ID). Затем в Python связывает объекты. Работает для ManyToMany и обратных связей.

💰 Транзакции: Тонкости atomic

Вложенные блоки atomic:

with transaction.atomic():  # Точка сохранения 1
    obj.save()
    with transaction.atomic():  # Точка сохранения 2
        obj2.save()
        raise IntegrityError  # Откатится только obj2, obj1 сохранится!

Внутренний блок — это "savepoint". Ошибка внутри откатывает только до savepoint'а, но не всю транзакцию.

Как заставить упасть всё:
Если нужно, чтобы любая ошибка откатывала всё, используй transaction.atomic() на самом верху, без вложенных блоков, или выбрасывай исключение из внешнего блока.

⚡ F() и Q() — Суперсилы ORM

F() — атомарные обновления без гонок:

# Плохо (гонка!):
product = Product.objects.get(id=1)
product.stock -= 1
product.save()

# Хорошо (атомарно):
Product.objects.filter(id=1).update(stock=F('stock') - 1)

F() с арифметикой и строками:

from django.db.models.functions import Concat, Lower
Author.objects.update(name=Concat(F('first_name'), Value(' '), F('last_name')))
Author.objects.update(email=Lower(F('email')))

Q() — сложные фильтры:

from django.db.models import Q

# ИЛИ: Возраст > 65 ИЛИ ребенок
Q(age__gt=65) | Q(age__lt=18)

# И: Женщины И старше 18
Q(gender='F') & Q(age__gte=18)

# Отрицание: Все, кто НЕ из Москвы
~Q(city='Moscow')

# Динамическое построение:
filters = Q()
if name:
    filters &= Q(name__icontains=name)
if price_min:
    filters &= Q(price__gte=price_min)
Product.objects.filter(filters)

🔧 Индексы: Когда и какие

Типы индексов в Django: * B-Tree (по умолчанию): для точного сравнения, range-запросов, сортировки. * Hash: только для равенства, но быстрее. * Gin/Gist: для полнотекстового поиска, массивов, JSON-полей.

Добавление индекса:

class Product(models.Model):
    name = models.CharField(max_length=100)
    created_at = models.DateTimeField(auto_now_add=True)
    category = models.ForeignKey(Category, on_delete=models.CASCADE)

    class Meta:
        indexes = [
            models.Index(fields=['name']),  # простой
            models.Index(fields=['category', '-created_at']),  # композитный + сортировка
        ]

Когда индекс бесполезен: * На поле, где мало уникальных значений (например, пол: "M"/"F"). * На поле, которое почти никогда не используется в WHERE. * На очень маленьких таблицах (индекс может быть даже медленнее).

📊 Агрегация vs Аннотация (Мастер-класс)

aggregate — итог по всему набору:

from django.db.models import Avg, Max, Min

stats = Order.objects.filter(status='paid').aggregate(
    total=Sum('amount'),
    avg=Avg('amount'),
    max=Max('amount'),
    min=Min('amount'),
    count=Count('id')
)
# stats = {'total': 100500, 'avg': 2500, 'max': 10000, 'min': 500, 'count': 42}

annotate — итог по группам:

from django.db.models import Count

# Книги с количеством отзывов
books = Book.objects.annotate(
    reviews_count=Count('reviews'),
    avg_rating=Avg('reviews__rating')
)

# Фильтрация по аннотированному полю
books = books.filter(reviews_count__gt=5, avg_rating__gte=4)

Секретный прием — conditional aggregation:

from django.db.models import Count, Q

# Количество завершенных и незавершенных заказов для каждого пользователя
users = User.objects.annotate(
    completed_orders=Count('orders', filter=Q(orders__status='completed')),
    pending_orders=Count('orders', filter=Q(orders__status='pending')),
)

3. Django Rest Framework (DRF) (Углубленно)

🔧 Сериализаторы: Валидация под микроскопом

Уровни валидации:
1. На уровне поля (Field-level): Метод validate_<field_name>.
python def validate_price(self, value): if value < 0: raise serializers.ValidationError("Price cannot be negative") return value
2. На уровне объекта (Object-level): Метод validate.
python def validate(self, data): if data['start_date'] > data['end_date']: raise ValidationError("Start must be before end") return data
3. Встроенные валидаторы: EmailValidator, URLValidator, MinValueValidator и др.

create() и update() — что внутри:

class ProductSerializer(serializers.ModelSerializer):
    class Meta:
        model = Product
        fields = '__all__'

    def create(self, validated_data):
        # validated_data уже очищено и проверено
        return Product.objects.create(**validated_data)

    def update(self, instance, validated_data):
        # instance — существующий объект
        instance.name = validated_data.get('name', instance.name)
        instance.price = validated_data.get('price', instance.price)
        instance.save()
        return instance

🏛️ ViewSets vs APIView — стратегия выбора

Когда APIView: * Эндпоинт делает не CRUD (например, /api/analyze/, /api/generate-report/). * Нужна разная логика для GET и POST на одном URL. * Хочется полный контроль над кодом и ответами.

Когда ViewSet: * Стандартный CRUD для модели. * Нужно быстро набросать прототип. * Используется Router для автоматической генерации URL.

Кастомизация ViewSet:

class ProductViewSet(ModelViewSet):
    queryset = Product.objects.all()
    serializer_class = ProductSerializer

    @action(detail=False, methods=['get'])
    def bestsellers(self, request):
        best = self.get_queryset().filter(sales__gt=100)[:10]
        serializer = self.get_serializer(best, many=True)
        return Response(serializer.data)

    @action(detail=True, methods=['post'])
    def add_to_cart(self, request, pk=None):
        product = self.get_object()
        # ... логика добавления в корзину
        return Response({'status': 'added'})

🔐 Authentication vs Permission — раз и навсегда

Аутентификация (кто ты): * Токен в заголовке Authorization: Token 123.... * Куки с sessionid. * JWT токен. * Аутентификация возвращает request.user (или AnonymousUser).

Пермишн (что тебе можно): * IsAuthenticated: проверяет user.is_authenticated. * IsAdminUser: проверяет user.is_staff. * IsOwner: кастомный пермишн, проверяет, принадлежит ли объект пользователю. * DjangoModelPermissions: сверяется с правами Django.

Кастомный пермишн:

from rest_framework.permissions import BasePermission

class IsOwnerOrReadOnly(BasePermission):
    def has_object_permission(self, request, view, obj):
        if request.method in SAFE_METHODS:
            return True
        return obj.owner == request.user

🚦 Throttling — защита от спама

Настройка в settings.py:

REST_FRAMEWORK = {
    'DEFAULT_THROTTLE_CLASSES': [
        'rest_framework.throttling.AnonRateThrottle',
        'rest_framework.throttling.UserRateThrottle',
    ],
    'DEFAULT_THROTTLE_RATES': {
        'anon': '100/day',   # 100 запросов в день для анонимов
        'user': '1000/day',  # 1000 для авторизованных
    }
}

Кастомный троттлинг (по IP):

class IPBasedThrottle(SimpleRateThrottle):
    scope = 'ip'

    def get_cache_key(self, request, view):
        return self.get_ident(request)  # IP-адрес

🪆 Nested Serializers — магия вложенности

Для чтения:

class OrderItemSerializer(serializers.ModelSerializer):
    class Meta:
        model = OrderItem
        fields = ['product', 'quantity', 'price']

class OrderSerializer(serializers.ModelSerializer):
    items = OrderItemSerializer(many=True, read_only=True)

    class Meta:
        model = Order
        fields = ['id', 'customer', 'created_at', 'items']

Для записи (переопределяем create):

class OrderSerializer(serializers.ModelSerializer):
    items = OrderItemSerializer(many=True)

    class Meta:
        model = Order
        fields = ['customer', 'items']

    def create(self, validated_data):
        items_data = validated_data.pop('items')
        order = Order.objects.create(**validated_data)
        for item_data in items_data:
            OrderItem.objects.create(order=order, **item_data)
        return order

4. Архитектура и Инструменты (Углубленно)

🥒 Celery — промышленная очередь задач

Почему Celery, а не потоки/многопроцессность? * Устойчивость к падениям: Задачи хранятся в брокере (RabbitMQ/Redis). Если воркер упал, задача вернется в очередь. * Масштабирование: Можно запустить 1000 воркеров на разных серверах. * Распределенность: Воркеры могут быть на других машинах, общаясь через брокер.

Гарантия выполнения — цепочка:
1. Задача → отправляется в брокер.
2. Воркер забирает задачу (ACK — подтверждение получения).
3. Если воркер упал без ACK, задача возвращается в очередь.
4. Retry — если задача упала с ошибкой, Celery может перезапустить её.

Идемпотентность (почему важно):
Допустим, задача "начислить бонусы за заказ". Если её выполнить дважды — пользователь получит двойные бонусы. Идемпотентное решение: проверять флаг bonuses_awarded перед начислением.

🚀 Кэширование — стратегии

Бэкенды сравнение: * LocMemCache: Быстро, но только для одного процесса (не для продакшна с несколькими воркерами). * Redis: Лучший выбор. Поддерживает структуры данных, персистентность, Pub/Sub. * Memcached: Быстрее Redis на простых операциях, но данные только в RAM (потеря при ребуте). * DatabaseCache: Медленно, только для разработки.

Стратегии кэширования:
1. Кэш всего view:
python @cache_page(60 * 15) # кэш на 15 минут def my_view(request): ...
2. Кэш фрагмента в шаблоне:
django {% load cache %} {% cache 500 sidebar request.user.id %} ... тяжелый блок ... {% endcache %}
3. Кэш данных вручную:
```python
from django.core.cache import cache

def get_expensive_data():
    data = cache.get('expensive_key')
    if not data:
        data = heavy_computation()
        cache.set('expensive_key', data, 3600)  # на час
    return data
```

🧪 Testing — профессиональный подход

pytest-django — киллер-фича: фикстуры:

import pytest

@pytest.fixture
def user(db):
    return User.objects.create_user(username='testuser', password='123')

@pytest.fixture
def product(db):
    return Product.objects.create(name='Тестовый товар', price=1000)

def test_order_creation(user, product, client):
    client.login(username='testuser', password='123')
    response = client.post('/cart/add/', {'product_id': product.id})
    assert response.status_code == 200
    assert Order.objects.filter(user=user).exists()

factory_boy — генерация данных:

import factory
from myapp.models import Product, Category

class CategoryFactory(factory.django.DjangoModelFactory):
    class Meta:
        model = Category
    name = factory.Sequence(lambda n: f'Category {n}')

class ProductFactory(factory.django.DjangoModelFactory):
    class Meta:
        model = Product
    name = factory.Sequence(lambda n: f'Product {n}')
    price = factory.Faker('pydecimal', left_digits=4, right_digits=2, positive=True)
    category = factory.SubFactory(CategoryFactory)

# В тесте:
product = ProductFactory(price=500)

🐳 Docker — правильная сборка

Dockerfile с оптимизациями:

FROM python:3.11-slim

# Устанавливаем зависимости системы
RUN apt-get update && apt-get install -y --no-install-recommends \
    gcc \
    libpq-dev \
    && rm -rf /var/lib/apt/lists/*

# Устанавливаем зависимости Python (копируем только requirements для кэширования)
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# Копируем код
COPY . .

# Запускаем entrypoint
COPY entrypoint.sh /
RUN chmod +x /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]

CMD ["gunicorn", "myproject.wsgi:application", "--bind", "0.0.0.0:8000"]

entrypoint.sh:

#!/bin/sh
python manage.py migrate --noinput
python manage.py collectstatic --noinput
exec "$@"

5. Безопасность (Углубленно)

🛡️ CSRF — анатомия защиты

Как Django защищает:
1. Генерирует случайный CSRF-токен при первом GET-запросе.
2. Кладёт токен в куку (csrftoken).
3. Во всех формах добавляет скрытое поле {% csrf_token %} с тем же токеном.
4. При POST-запросе сравнивает токен из куки и из тела формы.

Почему это работает: Атакующий не может прочитать куку с другого домена (same-origin policy) и не может подделать запрос с нужным токеном.

Когда отключать (@csrf_exempt): * Webhook от внешнего сервиса (чужой сервер не знает наш CSRF-токен). * API для SPA (защита через JWT, не через куки).

🗃️ SQL Injection — где ORM спасает, а где нет

Безопасно:

# ORM экранирует параметры
Product.objects.filter(name=user_input)  # безопасно
Product.objects.raw('SELECT * FROM products WHERE name = %s', [user_input])  # безопасно

Опасно:

# Конкатенация строк — катастрофа!
Product.objects.raw(f'SELECT * FROM products WHERE name = "{user_input}"')  # SQL Injection!

# extra() с подстановкой без параметров — тоже опасно
Product.objects.extra(where=[f"name = '{user_input}'"])  # SQL Injection!

Всегда используйте параметризацию:

# Правильно:
Product.objects.raw('SELECT * FROM products WHERE name = %s', [user_input])
Product.objects.extra(where=['name = %s'], params=[user_input])

📝 XSS — шаблоны спасают

Автоэкранирование:

{{ user_input }}  <!-- <script>alert(1)</script> превратится в &lt;script&gt;alert(1)&lt;/script&gt; -->

Когда НЕ экранируется:

{{ user_input|safe }}  <!-- HTML выполнится! Опасно! -->
{% autoescape off %}
    {{ user_input }}  <!-- тоже не экранируется -->
{% endautoescape %}

Используйте |safe ТОЛЬКО для доверенного контента:

{{ post.body|safe }}  <!-- если это текст из админки от админа -->

🔐 Пароли — глубже, чем просто хеш

Алгоритмы (от слабого к сильному):
1. PBKDF2 — по умолчанию в Django. Многократное хеширование с солью.
2. Argon2 — победитель конкурса Password Hashing Competition. Самый устойчивый к атакам на GPU.
3. bcrypt — тоже хорошо, адаптивная сложность.

Смена алгоритма:
Django поддерживает несколько алгоритмов одновременно. Если залогинился пользователь с устаревшим алгоритмом — Django автоматически перехеширует пароль на новый.


6. Практическая задача (Live Coding) — Углубленно

Задача 1: Заказы за месяц с суммой (с оптимизацией)

Усложним: Нужно учесть скидки, налоги, доставку.

from django.db.models import Sum, F, ExpressionWrapper, DecimalField, Q
from django.db.models.functions import Coalesce
from datetime import datetime, timedelta

def get_monthly_orders_with_totals(year, month):
    """
    Возвращает заказы за месяц с общей суммой, 
    включая стоимость товаров, скидку и доставку
    """
    start_date = datetime(year, month, 1)
    if month == 12:
        end_date = datetime(year + 1, 1, 1)
    else:
        end_date = datetime(year, month + 1, 1)

    # Базовый запрос
    orders = Order.objects.filter(
        created_at__gte=start_date,
        created_at__lt=end_date,
        status='completed'
    ).select_related(
        'customer', 'promo_code'  # подтягиваем связанные данные
    ).prefetch_related(
        'items',  # подтягиваем все позиции заказа
        'items__product',  # и даже продукты в позициях (если нужно)
    ).annotate(
        # Сумма товаров
        subtotal=Coalesce(Sum('items__price'), 0),
        # Сумма скидки (если есть промокод)
        discount=ExpressionWrapper(
            F('subtotal') * Coalesce(F('promo_code__discount_percent'), 0) / 100,
            output_field=DecimalField()
        ),
        # Итог с доставкой
        total=ExpressionWrapper(
            F('subtotal') - F('discount') + F('shipping_cost'),
            output_field=DecimalField()
        )
    )

    # Дополнительная статистика за месяц
    stats = orders.aggregate(
        total_revenue=Sum('total'),
        avg_order=Avg('total'),
        order_count=Count('id'),
        unique_customers=Count('customer', distinct=True)
    )

    return orders, stats

Задача 2: Оптимизация статей (с подводными камнями)

from django.db.models import Prefetch, Count

def optimized_article_list(request):
    """
    Возвращает статьи с авторами, 
    последними 3 комментариями и количеством комментариев
    """
    # Prefetch с фильтрацией и сортировкой
    recent_comments = Prefetch(
        'comments',
        queryset=Comment.objects.select_related('author')
                               .order_by('-created_at')[:3],
        to_attr='recent_comments'  # будут доступны как article.recent_comments
    )

    articles = Article.objects.select_related(
        'author'  # один JOIN для автора
    ).prefetch_related(
        recent_comments,  # кастомный prefetch
        'tags',  # многие-ко-многим (отдельный запрос)
    ).annotate(
        comment_count=Count('comments')  # количество комментариев
    ).filter(
        is_published=True
    ).order_by('-created_at')

    # Проверяем количество запросов
    from django.db import connection
    print(f'Запросов выполнено: {len(connection.queries)}')

    return render(request, 'blog/list.html', {'articles': articles})

Что будет в шаблоне:

{% for article in articles %}
    <h2>{{ article.title }}</h2>
    <p>Автор: {{ article.author.name }}</p>  <!-- уже подгружено -->
    <p>Комментариев: {{ article.comment_count }}</p>  <!-- из annotate -->
    <div>
        Последние комментарии:
        {% for comment in article.recent_comments %}  <!-- из Prefetch -->
            <p>{{ comment.author.name }}: {{ comment.text }}</p>
        {% endfor %}
    </div>
{% endfor %}

Итог по запросам:
1. Основной запрос статей + JOIN с авторами.
2. Запрос для recent_comments (с JOIN авторов) для всех статей сразу.
3. Запрос для tags (многие-ко-многим).
Всего 3 запроса независимо от количества статей!

0.0
0 оценок
5★
0
4★
0
3★
0
2★
0
1★
0

Оставить отзыв

Нажмите на звезду для оценки от 1 до 5
Необязательно. Используется только для связи
0/2000

Комментарии

Все С ответами Проверенные Только 4-5★