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> превратится в <script>alert(1)</script> -->
Когда НЕ экранируется:
{{ 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 запроса независимо от количества статей!
Оставить отзыв
Комментарии
Загрузка комментариев...
★ Оставить отзыв