Django и Django REST Fraemwork

Django и Django REST Fraemwork #

1. Как устроена архитектура Django и какие основные компоненты входят в её модель взаимодействия? #

Общая идея #

Django обычно описывают не как классический MVC, а как MTV: Model — Template — View. В терминах Django:

Model    → данные и работа с БД
Template → представление HTML
View     → функция/класс, который принимает request и возвращает response

При этом роль “controller” в классическом MVC в Django в основном выполняет сам фреймворк: механизм, который получает HTTP-запрос, находит подходящий URL-маршрут и вызывает нужную view. Официальная документация прямо описывает Django как MTV-фреймворк.

Общая схема взаимодействия #

Клиент / браузер
HTTP request
Web server / WSGI или ASGI
Django handler
Middleware
URL dispatcher / urls.py
View / views.py
Model / ORM
Database
Template / serializer / HttpResponse
Middleware
HTTP response
Клиент / браузер

1. URL dispatcher #

URL dispatcher отвечает за сопоставление URL с конкретной view.

Пример:

# urls.py
from django.urls import path
from . import views

urlpatterns = [
    path("articles/<int:year>/", views.year_archive),
]

Когда приходит запрос, Django берёт корневой URLconf, смотрит urlpatterns, проходит по маршрутам по порядку и вызывает первую view, которая подходит под URL. Захваченные параметры, например year, передаются во view как аргументы.

2. View #

View — это слой обработки запроса. Это может быть функция или class-based view.

Пример:

from django.http import HttpResponse

def hello(request):
    return HttpResponse("Hello")

View принимает HttpRequest и возвращает HttpResponse. Внутри view обычно находится прикладная логика: проверка прав, вызов ORM, подготовка данных, выбор шаблона или возврат JSON. Документация Django определяет view именно как Python-функцию, которая принимает web request и возвращает web response.

3. Model и ORM #

Model описывает структуру данных и поведение сущностей приложения.

Пример:

from django.db import models

class Article(models.Model):
    title = models.CharField(max_length=255)
    text = models.TextField()

Каждая модель — это Python-класс, который наследуется от django.db.models.Model. Обычно одна модель соответствует одной таблице в базе данных, а поля модели соответствуют колонкам таблицы. Django на основе моделей предоставляет ORM API для запросов к базе.

Пример использования ORM:

articles = Article.objects.filter(title__icontains="django")

То есть view обычно не пишет SQL напрямую, а работает через модель и ORM.

4. Template #

Template отвечает за генерацию HTML-представления.

Пример:

<h1>{{ article.title }}</h1>
<p>{{ article.text }}</p>

Шаблон получает context — словарь данных — и подставляет значения в HTML. В документации Django шаблон описывается как текстовый документ, размеченный Django template language, который рендерится с контекстом.

Пример во view:

from django.shortcuts import render

def article_detail(request, article_id):
    article = Article.objects.get(id=article_id)
    return render(request, "article_detail.html", {"article": article})

5. Middleware #

Middleware — это промежуточные обработчики между входящим запросом и view, а также между view и исходящим ответом.

Пример задач middleware:

Authentication
CSRF protection
Sessions
Security headers
Messages
Request/response logging

Middleware в Django работает как цепочка. На входе request проходит middleware сверху вниз, затем вызывается view, после чего response проходит обратно через middleware в обратном порядке. Middleware может изменить request, изменить response или вообще не пустить запрос дальше.

Упрощённо:

Request
SecurityMiddleware
SessionMiddleware
AuthenticationMiddleware
View
AuthenticationMiddleware
SessionMiddleware
SecurityMiddleware
Response

6. Project и apps #

Django-проект обычно состоит из проекта и приложений.

myproject/
    settings.py
    urls.py
    asgi.py
    wsgi.py

users/
    models.py
    views.py
    urls.py
    admin.py

blog/
    models.py
    views.py
    urls.py
    templates/

Разделение такое:

Project  → общая конфигурация всего сайта
App      → отдельный модуль бизнес-функциональности

Например:

accounts → регистрация, логин, пользователи
storage  → файлы
orders   → заказы
blog     → статьи

В официальной документации ключевые части Django перечисляются как модели и базы данных, обработка HTTP-запросов, URL dispatcher, views, middleware, templates, forms, migrations, auth, cache и другие подсистемы.

Полный цикл запроса #

Пример: пользователь открывает страницу статьи /articles/10/.

1. Браузер отправляет GET /articles/10/

2. Django получает HttpRequest.

3. Request проходит через middleware.

4. URL dispatcher ищет подходящий маршрут в urls.py.

5. Django находит view article_detail.

6. View получает request и article_id=10.

7. View обращается к модели Article.

8. ORM делает запрос в базу данных.

9. View получает объект Article.

10. View передаёт article в template context.

11. Template рендерит HTML.

12. Django создаёт HttpResponse.

13. Response проходит обратно через middleware.

14. Браузер получает HTML-страницу.

В коде это выглядит так:

# urls.py
from django.urls import path
from .views import article_detail

urlpatterns = [
    path("articles/<int:article_id>/", article_detail),
]
# views.py
from django.shortcuts import render, get_object_or_404
from .models import Article

def article_detail(request, article_id):
    article = get_object_or_404(Article, id=article_id)
    return render(request, "articles/detail.html", {"article": article})
# models.py
from django.db import models

class Article(models.Model):
    title = models.CharField(max_length=255)
    text = models.TextField()
<!-- templates/articles/detail.html -->
<h1>{{ article.title }}</h1>
<p>{{ article.text }}</p>

Главное отличие от FastAPI #

В FastAPI чаще явно строят архитектуру вокруг:

Router → Depends → Service → Repository → DB

В Django из коробки больше встроенных слоёв:

URLConf → View → Model/ORM → Template/Response
       + Middleware
       + Forms
       + Admin
       + Auth
       + Sessions
       + Migrations

То есть Django — более “batteries included” фреймворк: он уже содержит ORM, миграции, админку, middleware, систему шаблонов, формы, auth и другие компоненты. FastAPI чаще оставляет эти решения на выбор разработчика.


2. Что представляет собой менеджер модели в Django ORM и какую роль он выполняет при работе с объектами базы данных? #

Что такое менеджер модели #

Manager в Django ORM — это объект, через который модель получает доступ к операциям с базой данных.

Обычно он доступен через атрибут objects:

User.objects.all()
User.objects.filter(is_active=True)
User.objects.get(id=1)
User.objects.create(username="admin")

То есть objects — это не просто название. Это стандартный менеджер модели, который Django автоматически добавляет к каждой модели, если ты не указал свой. В документации Django Manager описан как интерфейс, через который модели получают операции запросов к базе данных. У каждой модели есть минимум один менеджер.

Где он находится в модели #

Пример:

from django.db import models

class UserFile(models.Model):
    name = models.CharField(max_length=255)
    owner_id = models.IntegerField()

Даже если в модели явно ничего не написано, Django фактически даёт ей менеджер:

UserFile.objects

Через него можно делать запросы:

UserFile.objects.all()
UserFile.objects.filter(owner_id=1)
UserFile.objects.create(name="photo.png", owner_id=1)

Роль менеджера #

Менеджер выполняет роль входной точки к таблице модели.

Model class
Manager: objects
QuerySet
SQL query
Database

Например:

UserFile.objects.filter(owner_id=1)

Упрощённо означает:

Возьми модель UserFile
Через её менеджер objects создай QuerySet
Добавь условие owner_id = 1
При выполнении запроса получи строки из БД
Преобразуй строки в объекты UserFile

Важно: методы вроде filter(), exclude(), order_by() обычно возвращают новый QuerySet, а не сразу список объектов. Документация Django отдельно указывает, что многие методы QuerySet изменяют состав результата или SQL-запрос, но сами по себе не выполняют запрос немедленно.

Manager vs QuerySet #

Их часто путают.

Manager  → точка входа к запросам модели
QuerySet → объект запроса, который можно уточнять и выполнять

Пример:

qs = UserFile.objects.filter(owner_id=1)

Здесь:

UserFile.objects        → Manager
.filter(owner_id=1)     → возвращает QuerySet
qs                      → QuerySet

Дальше QuerySet можно уточнять:

qs = UserFile.objects.filter(owner_id=1).order_by("-id")

Зачем нужен кастомный менеджер #

Менеджер можно переопределить, чтобы вынести часто используемую логику запросов.

Например, есть модель файлов:

class UserFile(models.Model):
    name = models.CharField(max_length=255)
    owner_id = models.IntegerField()
    is_deleted = models.BooleanField(default=False)

Можно создать менеджер, который по умолчанию возвращает только неудалённые файлы:

class ActiveFileManager(models.Manager):
    def get_queryset(self):
        return super().get_queryset().filter(is_deleted=False)


class UserFile(models.Model):
    name = models.CharField(max_length=255)
    owner_id = models.IntegerField()
    is_deleted = models.BooleanField(default=False)

    objects = ActiveFileManager()

Теперь:

UserFile.objects.all()

будет возвращать только файлы, у которых:

is_deleted=False

В документации Django это описано как один из основных способов настройки менеджера: можно переопределить get_queryset(), чтобы изменить базовый QuerySet, который возвращает менеджер.

Несколько менеджеров у одной модели #

Можно оставить обычный менеджер и добавить специальный:

class ActiveFileManager(models.Manager):
    def get_queryset(self):
        return super().get_queryset().filter(is_deleted=False)


class UserFile(models.Model):
    name = models.CharField(max_length=255)
    owner_id = models.IntegerField()
    is_deleted = models.BooleanField(default=False)

    objects = models.Manager()
    active = ActiveFileManager()

Теперь:

UserFile.objects.all()

вернёт все файлы.

UserFile.active.all()

вернёт только неудалённые файлы.

Django позволяет добавлять несколько менеджеров к одной модели. Это удобно, когда нужно иметь разные стандартные наборы запросов: например, active, archived, published, drafts.

Методы бизнес-уровня в менеджере #

В менеджер часто выносят методы, которые относятся не к одному объекту, а ко всей таблице или набору записей.

Пример:

class UserFileManager(models.Manager):
    def for_user(self, user):
        return self.get_queryset().filter(owner=user)

    def images(self):
        return self.get_queryset().filter(content_type__startswith="image/")

Использование:

UserFile.objects.for_user(request.user)
UserFile.objects.images()

Это лучше, чем размазывать одинаковые фильтры по разным view.

Важный момент про default manager #

Первый менеджер, объявленный в модели, Django считает менеджером по умолчанию. Некоторые части Django используют именно его. Поэтому опасно делать первым менеджером тот, который сильно фильтрует данные, например скрывает is_deleted=True, если где-то Django должен видеть все записи. Документация отдельно предупреждает, что первый менеджер имеет специальный статус _default_manager.

Пример осторожного варианта:

class UserFile(models.Model):
    name = models.CharField(max_length=255)
    is_deleted = models.BooleanField(default=False)

    objects = models.Manager()      # все записи
    active = ActiveFileManager()    # только активные

Так безопаснее, потому что обычный objects не скрывает записи.

Кратко #

Manager в Django ORM — это интерфейс модели для работы с базой данных.

Он нужен, чтобы:

1. Запускать запросы к таблице модели.
2. Создавать QuerySet.
3. Инкапсулировать часто используемые фильтры.
4. Добавлять методы уровня таблицы.
5. Настраивать базовый набор объектов через get_queryset().
6. Разделять разные способы доступа к одной модели через несколько менеджеров.

Главная формула:

Model.objects

это:

Модель → менеджер → QuerySet → SQL → база данных → объекты модели


3. Что такое middleware и какую роль он играет в обработке HTTP-запроса и HTTP-ответа? #

Middleware в Django — это промежуточный слой обработки запросов и ответов, представляющий собой набор функций/классов, которые обрабатывают запросы и ответы перед тем, как они достигнут view или после того, как view сгенерирует ответ. Каждый middleware компонент отвечает за определенную функцию, такую как аутентификация пользователей, управление сессиями или защита от атак.

Как работает middleware:

  1. Обертывает запрос: middleware выступает как обертка вокруг других функций или middleware.
  2. Обрабатывает запрос: перед тем как запрос достигнет целевого обработчика (например, вашей функции просмотра), middleware может модифицировать его или выполнить необходимую логику.
  3. Обрабатывает ответ: после того как запрос был обработан, middleware может модифицировать ответ перед его отправкой клиенту.
  4. Передает управление: middleware может либо передать управление следующему middleware (или просмотру), либо вернуть ответ самостоятельно, прервав дальнейшую обработку.

Архитектура Middleware

Запрос → MIDDLEWARE[0] → MIDDLEWARE[1] → ... → View
Ответ  ← MIDDLEWARE[1] ← MIDDLEWARE[0] ← ... ← View

Стандартные middleware в Django # settings.py

MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
]

Примеры использования middleware:

  • SecurityMiddleware: Обеспечивает защиту от различных атак, например, от XSS или CSRF.
  • AuthenticationMiddleware: Связывает пользователя с запросом на основе сессии.
  • SessionMiddleware: Обеспечивает поддержку механизма сессий в Django.
  • CsrfViewMiddleware: Проверяет запросы на наличие CSRF-токена для предотвращения подделки запросов.

Создание кастомного middleware

Пример: Логирование запросов

import time

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

    def __call__(self, request):
        start_time = time.time()

        response = self.get_response(request)

        duration = time.time() - start_time
        print(f"Запрос {request.path} выполнен за {duration:.2f} секунд")

        return response

В чем польза middleware:

  • Гибкость: позволяет добавлять глобальную логику обработки без необходимости изменять код приложения.
  • Модульность: каждый компонент отвечает за свою функцию, что делает код более чистым и легко поддерживаемым.
  • Переиспользуемость: один и тот же middleware может использоваться в разных проектах для выполнения одинаковых задач
  • Middleware — это мощный механизм для сквозной функциональности в Django-приложениях.


4. Что такое и как работает механизм CSRF-защиты в Django и какую роль играет CSRF-токен? #

Что такое CSRF #

CSRF — это атака, при которой злоумышленник заставляет браузер уже авторизованного пользователя отправить нежелательный запрос на доверенный сайт.

Например, пользователь залогинен на сайте:

bank.example.com

Параллельно он открывает вредоносный сайт. Этот сайт может попытаться отправить форму или запрос на bank.example.com. Браузер автоматически приложит cookies пользователя к запросу, потому что cookies принадлежат bank.example.com. Именно на этом и основана CSRF-атака: сервер может принять поддельный запрос за настоящий пользовательский запрос. OWASP описывает CSRF как атаку, при которой браузер аутентифицированного пользователя вынуждают выполнить нежелательное действие на доверенном сайте.

Зачем Django нужен CSRF-токен #

Проблема в том, что cookie сессии браузер отправляет автоматически:

Пользователь авторизован
В браузере есть sessionid cookie
Любой запрос к этому домену автоматически получает sessionid

Поэтому одной session cookie недостаточно, чтобы понять:

Запрос действительно отправил пользователь
или
запрос был подделан другим сайтом

CSRF-токен нужен как дополнительное доказательство, что запрос пришёл со страницы, сгенерированной самим Django-приложением, а не с чужого сайта.

Общая схема работы #

1. Django отдаёт страницу с формой.

2. В ответе устанавливается CSRF cookie.

3. В HTML-форму добавляется hidden input с CSRF-токеном.

4. Пользователь отправляет POST-запрос.

5. Браузер отправляет cookie.

6. Форма отправляет csrfmiddlewaretoken.

7. CsrfViewMiddleware сравнивает данные.

8. Если проверка успешна — запрос идёт во view.

9. Если проверка неуспешна — Django возвращает 403 Forbidden.

Схематично:

GET /profile/edit/
Django response:
    Set-Cookie: csrftoken=...
    <input type="hidden" name="csrfmiddlewaretoken" value="...">

POST /profile/edit/
Cookie: csrftoken=...
Form data: csrfmiddlewaretoken=...
CsrfViewMiddleware проверяет соответствие
View выполняется или Django возвращает 403

Официальная документация Django описывает механизм как комбинацию CSRF cookie со случайным секретом, hidden form field csrfmiddlewaretoken и проверки входящих небезопасных HTTP-запросов через CsrfViewMiddleware.

Где подключается CSRF-защита #

Обычно CSRF-защита включена через middleware:

MIDDLEWARE = [
    ...
    "django.middleware.csrf.CsrfViewMiddleware",
    ...
]

CsrfViewMiddleware стоит в цепочке обработки запроса до view. Он проверяет входящий запрос и может остановить его до выполнения бизнес-логики. Django указывает, что CSRF middleware включён по умолчанию в MIDDLEWARE, а если настройка переопределяется вручную, этот middleware нужно оставить.

Как добавляется CSRF-токен в HTML-форму #

В Django template используется тег:

<form method="post">
    {% csrf_token %}
    <input type="text" name="username">
    <button type="submit">Save</button>
</form>

После рендера шаблона это превращается примерно в:

<form method="post">
    <input type="hidden" name="csrfmiddlewaretoken" value="TOKEN_VALUE">
    <input type="text" name="username">
    <button type="submit">Save</button>
</form>

Тег {% csrf_token %} нужно добавлять внутрь POST-форм, которые отправляются на внутренние URL приложения. Django отдельно предупреждает, что CSRF-токен не нужно вставлять в формы, которые отправляются на внешние URL, потому что так можно утечь токен наружу.

Что именно проверяет Django #

Для небезопасных HTTP-методов Django требует, чтобы:

1. Была CSRF cookie.
2. Был передан csrfmiddlewaretoken.
3. Токен был корректным.
4. Origin или Referer проходил проверку для защищённых сценариев.

К небезопасным методам относятся, например:

POST
PUT
DELETE

Методы GET, HEAD, OPTIONS, TRACE Django рассматривает как safe methods и не требует для них CSRF-проверку в обычном смысле. Но это означает, что такие запросы не должны менять состояние на сервере. Django прямо указывает, что GET и другие safe methods должны быть без побочных эффектов, а небезопасные методы должны защищаться CSRF-механизмом.

Почему токен защищает от атаки #

Злоумышленник может заставить браузер пользователя отправить запрос на твой сайт, и браузер приложит cookies.

Но чужой сайт не может просто так узнать корректный CSRF-токен, который Django выдал пользователю.

Атакующий сайт может:
    отправить форму на твой домен

Атакующий сайт не должен иметь:
    правильный csrfmiddlewaretoken

Поэтому поддельный POST-запрос будет выглядеть неполным:

Cookie: sessionid=...
Cookie: csrftoken=...
csrfmiddlewaretoken отсутствует или неправильный

В результате CsrfViewMiddleware отклонит запрос:

403 Forbidden

В Django есть:

CSRF cookie      → хранит секрет
CSRF form token  → передаётся в форме или заголовке

Django не просто сравнивает две одинаковые строки. В форме используется masked token: значение маскируется и может отличаться при разных ответах, чтобы снижать риск атак типа BREACH. При проверке Django сравнивает именно секретную часть токена с секретом из cookie.

Упрощённо:

csrftoken cookie
секретное значение

csrfmiddlewaretoken
замаскированное значение, связанное с тем же секретом

Django
извлекает секретную часть и проверяет соответствие

Как работает CSRF при AJAX / fetch #

Если запрос отправляется не обычной HTML-формой, а через JavaScript, токен обычно передают в HTTP-заголовке:

fetch("/api/profile/", {
    method: "POST",
    headers: {
        "X-CSRFToken": csrftoken
    },
    body: formData,
    mode: "same-origin"
});

Django рекомендует для AJAX-запросов ставить заголовок X-CSRFToken, имя которого соответствует настройке CSRF_HEADER_NAME. Токен обычно берётся из cookie csrftoken, если не включены настройки CSRF_USE_SESSIONS или CSRF_COOKIE_HTTPONLY.

Что будет при ошибке CSRF #

Если проверка не прошла, Django не вызывает view и возвращает:

403 Forbidden

Типичные причины:

1. В форме нет {% csrf_token %}.
2. CSRF cookie не была установлена.
3. Токен устарел после логина.
4. Запрос пришёл с недоверенного Origin.
5. JavaScript не передал X-CSRFToken.
6. POST-запрос отправлен на внешний домен или с неправильной конфигурацией.

Django указывает, что при провале проверки CsrfViewMiddleware по умолчанию возвращает 403 Forbidden, а такие ошибки логируются через django.security.csrf.

Пример полного цикла #

# views.py
from django.shortcuts import render, redirect

def edit_profile(request):
    if request.method == "POST":
        # Если CSRF не прошёл, этот код вообще не выполнится
        request.user.username = request.POST["username"]
        request.user.save()
        return redirect("profile")

    return render(request, "edit_profile.html")
<!-- edit_profile.html -->
<form method="post">
    {% csrf_token %}
    <input type="text" name="username">
    <button type="submit">Save</button>
</form>

Цикл:

GET /edit-profile/
Django отдаёт форму с csrfmiddlewaretoken
Пользователь отправляет POST
CsrfViewMiddleware проверяет cookie + token + Origin/Referer
Только после этого выполняется edit_profile()

Когда CSRF особенно важен #

CSRF нужен там, где запрос меняет состояние:

изменение пароля
изменение email
создание заказа
удаление файла
создание записи
изменение профиля
logout
операции с деньгами

Не стоит делать такие действия через GET.

Плохо:

path("delete/<int:id>/", delete_file)
GET /delete/10/

Правильнее:

POST /delete/10/
DELETE /files/10/

и с CSRF-защитой, если используется cookie/session authentication.

Кратко #

CSRF-защита в Django — это механизм проверки, что небезопасный запрос пришёл со страницы самого приложения, а не был подделан внешним сайтом.

Главные элементы:

CsrfViewMiddleware     → проверяет запрос
csrftoken cookie       → хранит CSRF-секрет
{% csrf_token %}       → добавляет hidden input в форму
csrfmiddlewaretoken    → токен, который отправляется вместе с POST
X-CSRFToken            → способ передать токен в AJAX/fetch
403 Forbidden          → ответ при провале проверки

Основная схема:

Cookie сессии доказывает: пользователь авторизован.
CSRF-токен доказывает: запрос пришёл из доверенного источника приложения.


5. Что такое сигналы в Django и в каких случаях их применяют? #

Что такое сигналы в Django #

Сигналы в Django — это механизм уведомлений внутри фреймворка.

Они позволяют одному участку кода сообщить другим участкам кода, что произошло какое-то событие.

Официальная документация описывает это как signal dispatcher: отправитель сообщает получателям, что некоторое действие произошло. Это полезно, когда несколько независимых частей приложения должны реагировать на одно событие.

Упрощённо:

Произошло событие
Django отправил сигнал
Функция-обработчик получила сигнал
Выполнилась дополнительная логика

Например:

Пользователь зарегистрировался
Создался объект User
Сработал post_save
Автоматически создался Profile

Из чего состоит сигнал #

В сигналах обычно есть:

sender    — кто отправил сигнал
receiver  — функция, которая реагирует на сигнал
signal    — само событие
instance  — объект модели, с которым произошло событие

Пример:

from django.db.models.signals import post_save
from django.dispatch import receiver
from django.contrib.auth import get_user_model

from .models import Profile

User = get_user_model()


@receiver(post_save, sender=User)
def create_user_profile(sender, instance, created, **kwargs):
    if created:
        Profile.objects.create(user=instance)

Что здесь происходит:

sender=User

Сигнал будет слушать события, связанные с моделью User.

post_save

Сигнал сработает после вызова save() у объекта модели.

created

Показывает, был ли объект создан впервые или просто обновлён.

Основные встроенные сигналы Django #

Чаще всего используются сигналы моделей:

pre_save

Срабатывает перед сохранением объекта.

post_save

Срабатывает после сохранения объекта.

pre_delete

Срабатывает перед удалением объекта.

post_delete

Срабатывает после удаления объекта.

m2m_changed

Срабатывает при изменении связи ManyToManyField.

Эти сигналы перечислены в официальной справке Django по сигналам. Например, post_save отправляется в конце выполнения save(), а m2m_changed связан с изменениями ManyToManyField.

В каких случаях применяют сигналы #

Сигналы применяют, когда нужно выполнить дополнительное действие после события, но не хочется жёстко связывать это действие с основным кодом.

Типичные случаи:

1. Автоматическое создание связанных объектов

Например, после создания пользователя создать профиль:

User created

Profile created automatically
2. Логирование действий

Например, записать в журнал, что объект был создан, изменён или удалён.

3. Очистка связанных ресурсов

Например, после удаления записи удалить связанный файл из S3/MinIO.

4. Инвалидация кэша

Например, если объект изменился, удалить старые данные из Redis/cache.

5. Реакция на изменение ManyToMany-связей

Например, если пользователя добавили в группу, пересчитать права или отправить уведомление.

6. Отправка уведомлений

Например, после создания комментария отправить уведомление владельцу поста.

Когда сигналы лучше не использовать #

Сигналы не стоит использовать для основной бизнес-логики, от которой напрямую зависит результат операции.

Плохой пример:

Создание заказа
Через сигнал списываются деньги
Через сигнал создаётся доставка
Через сигнал меняется статус

Такой код сложнее читать и отлаживать, потому что важная логика спрятана не в сервисе/методе, а в сторонних обработчиках.

Лучше:

def create_order(user, items):
    order = Order.objects.create(user=user)

    reserve_items(order)
    create_payment(order)
    create_delivery(order)

    return order

Сигналы лучше использовать для побочных действий, а не для ядра бизнес-процесса.

Важный момент #

Сигналы Django не являются отдельной очередью задач.

То есть это не аналог:

Celery
RabbitMQ
Kafka
Redis Queue

Если внутри сигнала отправлять email, обращаться к внешнему API или делать тяжёлую операцию, это может замедлить основной запрос.

Например:

HTTP request
model.save()
post_save
долгая отправка email
response задерживается

Для тяжёлых действий лучше из сигнала поставить задачу в Celery, а не выполнять всё прямо внутри обработчика.

Отличие от переопределения save() #

Иногда возникает вопрос: использовать save() или сигнал?

class UserFile(models.Model):
    file = models.FileField()

    def save(self, *args, **kwargs):
        # логика прямо внутри модели
        super().save(*args, **kwargs)

Разница такая:

save()

Используется, когда логика является частью поведения самой модели.

signal

Используется, когда внешняя часть приложения должна отреагировать на событие модели.

Пример:

Нормализовать поле перед сохранением        → save() / clean() / serializer
Создать внешний audit-log после сохранения  → signal
Удалить файл после удаления модели          → signal

Кратко #

Сигналы в Django — это механизм реакции на события внутри приложения.

Их применяют для:

post_save     — действия после сохранения
pre_save      — действия перед сохранением
post_delete   — действия после удаления
pre_delete    — действия перед удалением
m2m_changed   — реакция на изменение ManyToMany-связей

Главная идея:

Сигналы подходят для побочных эффектов.
Сигналы не стоит использовать как основу бизнес-логики.


6. Для чего в Django используются метаклассы? #

В Django метаклассы используются для обработки классов в момент их создания.

То есть Django не просто видит такой код:

class UserFile(models.Model):
    file = models.FileField()
    uploaded_at = models.DateTimeField(auto_now_add=True)

Он перехватывает создание класса UserFile, анализирует его поля, Meta, наследование, менеджеры и собирает полноценную модель для ORM.

Главный пример — модели Django. Каждая модель наследуется от django.db.models.Model, а Django через внутренний метакласс модели превращает обычный Python-класс в ORM-модель. В документации указано, что модель — это Python-класс, наследующийся от django.db.models.Model, а атрибуты модели представляют поля базы данных.

Зачем это нужно Django #

Метаклассы нужны Django, чтобы во время объявления класса выполнить дополнительную настройку.

Примерно так:

class UserFile(models.Model):
    file = models.FileField()
    uploaded_at = models.DateTimeField()

↓ во время импорта models.py

Django анализирует класс
собирает список полей
создаёт _meta
подключает менеджер objects
регистрирует модель в приложении
ORM понимает, как работать с таблицей

То есть метакласс работает не во время создания объекта, а раньше — во время создания самого класса.

user_file = UserFile()

Это уже создание экземпляра модели.

А вот это:

class UserFile(models.Model):
    ...

Это момент, где Django применяет свою внутреннюю магию.

Главный пример: модели #

Когда ты пишешь:

class Product(models.Model):
    name = models.CharField(max_length=100)
    price = models.DecimalField(max_digits=10, decimal_places=2)

    class Meta:
        ordering = ["name"]

Django должен понять:

name  → поле таблицы
price → поле таблицы
Meta.ordering → настройка сортировки
objects → менеджер модели
_meta → техническое описание модели

Для этого и используется метакласс.

Внутри Django у модели есть _meta. Это центральный объект ORM, через который другие части Django — запросы, формы, админка — понимают возможности модели. Официальная документация прямо указывает, что _meta API находится в основе Django ORM и доступен через атрибут _meta каждой модели.

Пример:

Product._meta.get_fields()
Product._meta.db_table
Product._meta.ordering

То есть после обработки класса Django уже знает:

какие поля есть у модели
какая таблица используется
какой primary key
какие связи есть с другими моделями
какие Meta-настройки применены

Важно: class Meta — это не метакласс #

В Django часто путают:

class Meta:
    ordering = ["name"]

и метакласс.

Это разные вещи.

class Meta внутри модели — это обычный вложенный класс с настройками Django-модели.

Пример:

class Product(models.Model):
    name = models.CharField(max_length=100)

    class Meta:
        db_table = "products"
        ordering = ["name"]

А метакласс — это механизм Python/Django, который читает этот Meta и применяет настройки к модели.

class Meta

Это данные для Django.

metaclass

Это механизм, который эти данные обрабатывает.

Пример с формами #

Метаклассы используются не только в моделях, но и в формах.

Например:

from django import forms

class LoginForm(forms.Form):
    username = forms.CharField()
    password = forms.CharField()

Django должен собрать поля username и password в структуру формы. В исходниках Django Form использует DeclarativeFieldsMetaclass, который позволяет описывать поля декларативно — прямо как атрибуты класса. В комментарии Django указано, что это сделано ради declarative syntax, то есть чтобы поля формы можно было задавать прямо в классе.

То есть Django обрабатывает:

username = forms.CharField()
password = forms.CharField()

и превращает это в набор полей формы.

Упрощённо:

class LoginForm(forms.Form):
    username = forms.CharField()
    password = forms.CharField()

↓ метакласс формы

LoginForm.base_fields = {
    "username": CharField,
    "password": CharField,
}

Пример с ModelForm #

Метаклассы также важны в ModelForm.

class ProductForm(forms.ModelForm):
    class Meta:
        model = Product
        fields = ["name", "price"]

Django читает Meta.model и Meta.fields, после чего автоматически создаёт поля формы на основе модели.

Документация Django указывает, что ModelForm автоматически генерирует определённые поля, а список этих полей зависит от содержимого внутреннего класса Meta и от полей, которые уже были объявлены явно.

То есть Django делает примерно это:

Product.name  → forms.CharField
Product.price → forms.DecimalField

Поэтому тебе не нужно вручную писать:

class ProductForm(forms.Form):
    name = forms.CharField()
    price = forms.DecimalField()

Достаточно:

class ProductForm(forms.ModelForm):
    class Meta:
        model = Product
        fields = ["name", "price"]

Где именно Django использует метаклассы #

Основные места:

1. models.Model

Чтобы собрать поля, связи, менеджеры, _meta, наследование и настройки модели.

2. forms.Form

Чтобы собрать декларативно объявленные поля формы.

3. forms.ModelForm

Чтобы на основе модели и Meta автоматически сгенерировать поля формы.

4. Admin / ORM / migrations

Они напрямую зависят от информации, которую Django собрал при создании модели.

Упрощённая аналогия #

Без метакласса Django пришлось бы писать примерно так:

Product = create_model(
    name="Product",
    fields={
        "name": models.CharField(max_length=100),
        "price": models.DecimalField(max_digits=10, decimal_places=2),
    },
    ordering=["name"],
)

Но благодаря метаклассам можно писать удобно:

class Product(models.Model):
    name = models.CharField(max_length=100)
    price = models.DecimalField(max_digits=10, decimal_places=2)

    class Meta:
        ordering = ["name"]

То есть метаклассы дают Django возможность поддерживать декларативный стиль:

Ты описываешь структуру класса.
Django сам превращает её в рабочую ORM-модель или форму.

Краткий вывод #

Метаклассы в Django нужны для того, чтобы Django мог обрабатывать классы моделей и форм в момент их объявления.

Они позволяют Django:

собирать поля модели
создавать _meta
подключать objects-менеджер
обрабатывать class Meta
регистрировать модель в ORM
создавать поля ModelForm
собирать поля обычных Form
поддерживать декларативный синтаксис

Главная идея:

Метаклассы позволяют Django превратить обычный Python-класс
в полноценный объект фреймворка: модель, форму или ModelForm.


7. Что такое OuterRef в Django ORM? #

Что такое OuterRef #

OuterRef в Django ORM — это специальное выражение, которое позволяет внутреннему подзапросу обратиться к полю внешнего запроса.

Официально Django описывает OuterRef так: он используется, когда QuerySet внутри Subquery должен ссылаться на поле внешнего запроса. По поведению он похож на F(), но проверка корректности поля откладывается до момента, когда внешний запрос будет собран.

Упрощённо:

Внешний запрос:
Post.objects.all()

Внутренний подзапрос:
Comment.objects.filter(post=OuterRef("pk"))

OuterRef("pk") означает:
"возьми pk текущего Post из внешнего запроса"

Зачем нужен OuterRef #

OuterRef нужен для коррелированных подзапросов.

То есть для таких запросов, где внутренний запрос зависит от каждой строки внешнего запроса.

Например:

Для каждого поста
найти его последний комментарий
добавить email автора последнего комментария к результату

Без OuterRef внутренний запрос был бы независимым.
С OuterRef он может ссылаться на конкретную строку внешнего запроса.

Пример моделей #

Допустим, есть модели:

class Post(models.Model):
    title = models.CharField(max_length=255)


class Comment(models.Model):
    post = models.ForeignKey(Post, on_delete=models.CASCADE)
    email = models.EmailField()
    created_at = models.DateTimeField()

Задача: получить все посты и добавить к каждому посту email автора последнего комментария.

from django.db.models import OuterRef, Subquery

latest_comment = (
    Comment.objects
    .filter(post=OuterRef("pk"))
    .order_by("-created_at")
)

posts = Post.objects.annotate(
    latest_comment_email=Subquery(
        latest_comment.values("email")[:1]
    )
)

Что здесь важно:

OuterRef("pk")

означает:

Сравни Comment.post с pk текущего Post из внешнего запроса

То есть для каждого Post Django строит подзапрос примерно такого смысла:

SELECT post.*,
       (
           SELECT comment.email
           FROM comment
           WHERE comment.post_id = post.id
           ORDER BY comment.created_at DESC
           LIMIT 1
       ) AS latest_comment_email
FROM post;

Почему нельзя просто использовать F() #

F() ссылается на поле текущего запроса.

Например:

Product.objects.filter(price__gt=F("discount_price"))

Здесь price и discount_price находятся в одном QuerySet.

А OuterRef() нужен, когда поле находится во внешнем запросе, а обращение идёт из подзапроса.

Comment.objects.filter(post=OuterRef("pk"))

Здесь pk принадлежит не Comment, а внешней модели Post.

F("field")        → поле текущего QuerySet
OuterRef("field") → поле внешнего QuerySet

OuterRef почти всегда используется вместе с Subquery #

Обычно OuterRef сам по себе не используется. Он нужен внутри:

Subquery(...)

Пример:

Post.objects.annotate(
    latest_comment_email=Subquery(
        Comment.objects
        .filter(post=OuterRef("pk"))
        .order_by("-created_at")
        .values("email")[:1]
    )
)

Subquery позволяет вставить один запрос внутрь другого, а OuterRef связывает внутренний запрос с внешним. Django отдельно описывает Subquery, OuterRef и Exists как выражения для работы с подзапросами.

Важное ограничение: values() и [:1] #

Если Subquery используется как значение в annotate(), он должен вернуть один столбец.

Поэтому пишут:

.values("email")

А если нужно получить только одно значение, используют срез:

[:1]

Полный вариант:

latest_email = (
    Comment.objects
    .filter(post=OuterRef("pk"))
    .order_by("-created_at")
    .values("email")[:1]
)

posts = Post.objects.annotate(
    latest_comment_email=Subquery(latest_email)
)

Без [:1] подзапрос может вернуть несколько строк, и SQL-запрос станет некорректным для такого использования.

Пример с Exists #

OuterRef часто используют вместе с Exists, когда нужно проверить наличие связанных записей.

Например: получить посты, у которых есть комментарии.

from django.db.models import Exists, OuterRef

comments = Comment.objects.filter(post=OuterRef("pk"))

posts = Post.objects.annotate(
    has_comments=Exists(comments)
)

В результате у каждого Post появится дополнительное поле:

post.has_comments

Оно будет True или False.

Примерно такой SQL-смысл:

SELECT post.*,
       EXISTS (
           SELECT 1
           FROM comment
           WHERE comment.post_id = post.id
       ) AS has_comments
FROM post;

Exists в Django — это подкласс Subquery, который использует SQL EXISTS и часто применяется для проверки наличия подходящих строк.

Где это применяют на практике #

OuterRef применяют, когда нужно:

получить последнюю связанную запись
проверить наличие связанных объектов
сравнить объект с агрегатом по связанным данным
добавить к QuerySet вычисляемое поле через подзапрос
избежать N+1 запросов в сложных случаях

Пример задачи:

Для каждого пользователя получить дату его последнего заказа.
latest_order = (
    Order.objects
    .filter(user=OuterRef("pk"))
    .order_by("-created_at")
    .values("created_at")[:1]
)

users = User.objects.annotate(
    last_order_date=Subquery(latest_order)
)

Главное отличие от обычной фильтрации #

Обычная фильтрация:

Comment.objects.filter(post_id=1)

Здесь значение известно заранее.

С OuterRef:

Comment.objects.filter(post=OuterRef("pk"))

Значение не известно заранее. Оно берётся из каждой строки внешнего запроса.

Для Post(id=1) → ищем Comment(post_id=1)
Для Post(id=2) → ищем Comment(post_id=2)
Для Post(id=3) → ищем Comment(post_id=3)

Кратко #

OuterRef — это ссылка из подзапроса на поле внешнего запроса.

Главная идея:

Subquery создаёт внутренний запрос.
OuterRef связывает этот внутренний запрос с текущей строкой внешнего запроса.

Минимальный пример:

Post.objects.annotate(
    latest_comment_email=Subquery(
        Comment.objects
        .filter(post=OuterRef("pk"))
        .order_by("-created_at")
        .values("email")[:1]
    )
)

Смысл:

Для каждого Post найти последний Comment,
у которого Comment.post_id равен Post.id.


select_related и prefetch_related решают одну проблему: уменьшают количество SQL-запросов при доступе к связанным объектам.

Главное отличие:

select_related   → делает SQL JOIN и получает связанные данные одним запросом
prefetch_related → делает отдельные SQL-запросы и связывает данные в Python

Официальная документация Django прямо указывает, что select_related() использует SQL JOIN и включает поля связанного объекта в SELECT, а prefetch_related() выполняет отдельные запросы и делает связывание в Python.

select_related используют для связей, где у одного объекта может быть только один связанный объект:

ForeignKey
OneToOneField

Пример моделей:

class Author(models.Model):
    name = models.CharField(max_length=100)


class Book(models.Model):
    title = models.CharField(max_length=100)
    author = models.ForeignKey(Author, on_delete=models.CASCADE)

Без select_related:

books = Book.objects.all()

for book in books:
    print(book.author.name)

Проблема:

1 запрос  → получить книги
N запросов → получить автора для каждой книги

Это классическая проблема N+1.

С select_related:

books = Book.objects.select_related("author")

for book in books:
    print(book.author.name)

Django сделает примерно такой SQL:

SELECT book.*, author.*
FROM book
JOIN author ON book.author_id = author.id;

Итого:

1 SQL-запрос

prefetch_related используют для связей, где у одного объекта может быть много связанных объектов:

ManyToManyField
reverse ForeignKey

Например:

class Author(models.Model):
    name = models.CharField(max_length=100)


class Book(models.Model):
    title = models.CharField(max_length=100)
    author = models.ForeignKey(Author, related_name="books", on_delete=models.CASCADE)

Задача: получить авторов и их книги.

Без prefetch_related:

authors = Author.objects.all()

for author in authors:
    print(author.books.all())

Проблема:

1 запрос  → получить авторов
N запросов → получить книги каждого автора

С prefetch_related:

authors = Author.objects.prefetch_related("books")

for author in authors:
    print(author.books.all())

Django сделает примерно так:

SELECT * FROM author;

SELECT * FROM book
WHERE author_id IN (...);

И потом свяжет книги с авторами уже на уровне Python.

Итого:

2 SQL-запроса

select_related работает через JOIN.

Для связей вида ForeignKey и OneToOneField это безопасно, потому что одной строке основной таблицы соответствует один связанный объект.

Например:

Book → Author

Одна книга имеет одного автора.

А для ManyToMany или обратного ForeignKey может быть много связанных строк:

Author → many Books
Post → many Comments
User → many Groups

Если делать обычный JOIN, строки основной модели начнут дублироваться:

Author 1 + Book 1
Author 1 + Book 2
Author 1 + Book 3

Поэтому Django использует prefetch_related: отдельный запрос за связанными объектами и последующая сборка связей в Python. Документация Django указывает, что prefetch_related() поддерживает many-to-many, many-to-one, GenericRelation, а также foreign key и one-to-one, которые поддерживает select_related().

Главное правило выбора #

ForeignKey от текущей модели        → select_related
OneToOneField                       → select_related
ManyToManyField                     → prefetch_related
reverse ForeignKey / related_name   → prefetch_related

Примеры:

# Book.author — ForeignKey
Book.objects.select_related("author")
# Author.books — reverse ForeignKey через related_name
Author.objects.prefetch_related("books")
# Post.tags — ManyToManyField
Post.objects.prefetch_related("tags")
# User.profile — OneToOneField
User.objects.select_related("profile")

Можно использовать вместе #

Например:

books = (
    Book.objects
    .select_related("author")
    .prefetch_related("tags")
)

Смысл:

author → один объект, берём через JOIN
tags   → много объектов, берём отдельным запросом

Это нормальная практика.

Сравнение #

Критерийselect_relatedprefetch_related
Как работаетSQL JOINОтдельные запросы
Где связывает данныеВ базе данныхВ Python
Количество запросовОбычно 1Обычно 2 или больше
Подходит дляForeignKey, OneToOneManyToMany, reverse ForeignKey
Может дублировать строки при many-связяхДа, поэтому не применяетсяНет, потому что собирает отдельно
Основная цельЗагрузить один связанный объектЗагрузить коллекцию связанных объектов

Типичная ошибка #

Плохо:

posts = Post.objects.all()

for post in posts:
    for comment in post.comments.all():
        print(comment.text)

Будет:

1 запрос на posts
N запросов на comments

Лучше:

posts = Post.objects.prefetch_related("comments")

for post in posts:
    for comment in post.comments.all():
        print(comment.text)

Теперь:

1 запрос на posts
1 запрос на comments

Краткий вывод #

select_related

Используй, когда связь ведёт к одному объекту:

ForeignKey
OneToOneField
prefetch_related

Используй, когда связь ведёт к набору объектов:

ManyToManyField
reverse ForeignKey

Самая короткая формула:

select_related   → JOIN для "один объект"
prefetch_related → отдельный запрос для "много объектов"


9. Какие подходы вы использовали бы для оптимизации Django-приложения? #

Общий подход #

Оптимизацию Django-приложения я бы начинал не с хаотичного переписывания кода, а с измерений:

найти узкое место
подтвердить его метриками
исправить конкретную проблему
снова измерить результат

В Django чаще всего проблемы находятся в ORM-запросах, N+1, отсутствии индексов, тяжёлых сериализаторах, неправильном кэшировании, медленных внешних API и неоптимальной настройке production-окружения.

1. Диагностика и профилирование #

Сначала нужно понять, что именно тормозит:

SQL-запросы
рендеринг шаблонов
DRF-сериализация
внешние HTTP-запросы
работа с файлами
кэш
очереди
БД

Для локальной диагностики можно использовать django-debug-toolbar: у него есть SQL-панель, которая показывает SQL-запросы, время выполнения и ссылки на EXPLAIN; также есть панели cache, templates, signals и request/response.

Пример практической проверки:

Открываем endpoint
Смотрим количество SQL-запросов
Ищем дублирующиеся запросы
Проверяем самые долгие queries
Используем EXPLAIN / EXPLAIN ANALYZE

PostgreSQL официально рекомендует использовать EXPLAIN, чтобы увидеть план выполнения запроса, потому что выбор правильного плана критичен для производительности.

2. Оптимизация ORM-запросов #

Самое частое узкое место в Django — это не сам Django, а неправильное использование ORM.

Плохой пример:

posts = Post.objects.all()

for post in posts:
    print(post.author.username)

Проблема:

1 запрос на posts
N запросов на authors

Для ForeignKey и OneToOneField нужно использовать select_related():

posts = Post.objects.select_related("author")

select_related() работает через SQL JOIN и получает связанные объекты в том же запросе. Он подходит для одиночных связей: ForeignKey и OneToOneField.

Для ManyToManyField и обратных связей через related_name нужно использовать prefetch_related():

authors = Author.objects.prefetch_related("books")

prefetch_related() делает отдельные запросы и связывает данные на уровне Python, поэтому подходит для many-to-many, reverse foreign key и других связей, где у одного объекта может быть много связанных объектов.

3. Не доставать лишние данные из БД #

Не всегда нужно получать полноценные model instances.

Например, плохо:

users = User.objects.all()

for user in users:
    usernames.append(user.username)

Лучше:

usernames = User.objects.values_list("username", flat=True)

Django рекомендует использовать values() и values_list(), когда нужны словари или списки значений, а не полноценные ORM-объекты.

Также можно использовать only() и defer(), но осторожно:

User.objects.only("id", "username")

Django предупреждает, что only() и defer() могут дать обратный эффект, если потом всё равно обратиться к отложенным полям: ORM сделает дополнительный запрос. Поэтому их нужно применять только после профилирования.

4. Использовать exists(), count(), update() #

Вместо загрузки объектов в Python лучше выполнять операции на уровне БД.

Плохо:

if len(User.objects.filter(is_active=True)) > 0:
    ...

Лучше:

if User.objects.filter(is_active=True).exists():
    ...

Плохо:

users = User.objects.filter(is_active=False)

for user in users:
    user.is_active = True
    user.save()

Лучше:

User.objects.filter(is_active=False).update(is_active=True)

Django рекомендует использовать count(), когда нужен только счётчик, exists(), когда нужно проверить наличие строк, и update()/bulk delete вместо загрузки множества объектов и индивидуального сохранения. При этом update() не вызывает save() модели и связанные сигналы.

5. Индексы в базе данных #

Если endpoint часто фильтрует, сортирует или ищет по конкретным полям, нужно проверять индексы.

Пример:

class UserFile(models.Model):
    user = models.ForeignKey(User, on_delete=models.CASCADE)
    path = models.CharField(max_length=512)
    uploaded_at = models.DateTimeField(auto_now_add=True)

    class Meta:
        indexes = [
            models.Index(fields=["user", "uploaded_at"]),
            models.Index(fields=["user", "path"]),
        ]

Django позволяет задавать индексы через Meta.indexes; документация указывает, что index-классы используются для создания индексов базы данных.

Для одиночного поля есть db_index=True, но документация Django рекомендует по возможности использовать Meta.indexes, потому что этот вариант функциональнее.

Важно: индекс не нужно ставить на всё подряд.

Индекс полезен для:

частых WHERE
частых ORDER BY
частых JOIN
уникальности
поиска по внешним ключам

Но лишние индексы замедляют вставку и обновление данных, потому что БД должна поддерживать их в актуальном состоянии.

6. Пагинация и ограничение выдачи #

Нельзя отдавать большие списки без лимитов.

Плохо:

UserFile.objects.filter(user=request.user)

Лучше:

UserFile.objects.filter(user=request.user).order_by("-uploaded_at")[:50]

Для API обычно нужна пагинация:

limit / offset
cursor pagination
page number pagination

Особенно это важно для таблиц с файлами, уведомлениями, заказами, логами, комментариями.

7. Кэширование #

Кэшировать стоит данные, которые:

часто читаются
редко изменяются
дорого вычисляются
не зависят от уникального состояния каждого запроса

В Django есть cache framework. Он поддерживает разные уровни кэширования: можно кэшировать страницу целиком, отдельный view, фрагмент шаблона или конкретные данные через low-level cache API. Для точечного контроля Django предоставляет low-level cache API через cache.get() / cache.set().

Пример:

from django.core.cache import cache

def get_user_permissions(user_id):
    key = f"user_permissions:{user_id}"

    permissions = cache.get(key)

    if permissions is None:
        permissions = calculate_permissions(user_id)
        cache.set(key, permissions, timeout=300)

    return permissions

Практически часто кэшируют:

права пользователя
настройки приложения
справочники
дорогие агрегации
результаты внешних API
часто открываемые страницы

Главная проблема кэша — инвалидация. Нужно заранее понимать, когда удалять или обновлять ключ.

8. Вынос тяжёлых задач из HTTP-запроса #

Внутри request/response не стоит выполнять долгие операции:

отправка email
обработка изображений
генерация PDF
загрузка больших файлов
синхронизация с внешним API
массовые операции

Такие задачи лучше выносить в фоновые воркеры:

Celery
RQ
Dramatiq
cron/systemd timer
отдельный management command

Celery описывает task как основную единицу работы: задача определяет, что происходит при вызове, и что выполняет worker при получении сообщения.

Пример:

# views.py
send_email_task.delay(user.id)
return Response({"status": "queued"})

Так HTTP-запрос быстро возвращает ответ, а тяжёлая работа выполняется отдельно.

9. Оптимизация сериализаторов DRF #

В DRF часто тормозит не только SQL, но и сериализация.

Проблемные места:

SerializerMethodField с запросами внутри
сложные nested serializers
отсутствие select_related / prefetch_related под serializer
вычисления на каждый объект списка

Плохой пример:

class PostSerializer(serializers.ModelSerializer):
    comments_count = serializers.SerializerMethodField()

    def get_comments_count(self, obj):
        return obj.comments.count()

На списке это может дать N дополнительных запросов.

Лучше заранее аннотировать:

from django.db.models import Count

posts = Post.objects.annotate(comments_count=Count("comments"))

И потом читать уже готовое поле:

class PostSerializer(serializers.ModelSerializer):
    comments_count = serializers.IntegerField(read_only=True)

10. Оптимизация шаблонов #

Для Django templates важно не делать скрытые запросы внутри шаблона.

Плохо:

{% for post in posts %}
    {{ post.author.username }}
    {{ post.comments.count }}
{% endfor %}

Лучше подготовить данные во view/queryset:

posts = (
    Post.objects
    .select_related("author")
    .annotate(comments_count=Count("comments"))
)

Шаблон должен отображать данные, а не провоцировать N+1 запросы.

11. Настройки production-окружения #

Для production нужно отключать DEBUG, корректно настроить статику, БД, HTTPS, allowed hosts, логи и мониторинг.

Django deployment checklist отдельно указывает, что в production статические файлы не должны просто обслуживаться dev-сервером: нужно определить STATIC_ROOT и запускать collectstatic; также для сайтов с логином нужно использовать HTTPS, потому что через HTTP могут передаваться session cookie, пароль и reset tokens.

Из практических пунктов:

DEBUG=False
настроенный ALLOWED_HOSTS
gunicorn/uvicorn workers
nginx перед Django
static/media через nginx или object storage
PostgreSQL connection pooling
логирование ошибок
healthcheck endpoint
метрики

12. Оптимизация работы с файлами #

Для файлового хранилища лучше не гонять большие файлы через Django, когда можно использовать object storage.

Например, для MinIO/S3:

Django проверяет права
генерирует presigned URL
клиент скачивает файл напрямую из S3/MinIO

Это разгружает приложение: Django не держит соединение и не стримит большой файл через себя.

Краткий ответ для собеседования #

Я бы оптимизировал Django-приложение так:

1. Сначала профилирование: django-debug-toolbar, логи, метрики, EXPLAIN.
2. Устранение N+1 через select_related и prefetch_related.
3. Сокращение лишних данных через values(), values_list(), only(), defer().
4. Использование exists(), count(), update(), bulk_create(), bulk_update().
5. Индексы под реальные WHERE / JOIN / ORDER BY.
6. Пагинация больших списков.
7. Кэширование через Redis/Django cache framework.
8. Вынос тяжёлых задач в Celery/RQ/Dramatiq.
9. Оптимизация DRF-сериализаторов и nested serializers.
10. Подготовка данных во view/queryset, а не в шаблонах.
11. Настройка production: DEBUG=False, nginx, workers, static/media, HTTPS, мониторинг.
12. Повторное измерение после каждого изменения.

Главная мысль:

Оптимизация Django — это не набор случайных трюков.
Это цикл: измерить → найти узкое место → исправить → снова измерить.


10. Для чего нужен метод dispatch в BaseView в DRF #

В DRF метод dispatch() — это центральная точка обработки запроса внутри class-based view.

Он принимает входящий HTTP-запрос и решает:

GET    → вызвать get()
POST   → вызвать post()
PUT    → вызвать put()
PATCH  → вызвать patch()
DELETE → вызвать delete()

В обычном Django View.dispatch() смотрит на HTTP-метод и делегирует запрос соответствующему методу view-класса. Например, GET передаётся в get(), POST — в post(). Если метод не поддерживается, вызывается http_method_not_allowed() и возвращается 405.

Что добавляет DRF #

В DRF APIView наследуется от Django View, но расширяет стандартный dispatch() дополнительной API-логикой. Документация DRF указывает, что APIView отличается от обычного Django View тем, что:

request превращается в DRF Request
handler может вернуть DRF Response
APIException обрабатываются и превращаются в API-ответы
перед вызовом handler выполняются authentication, permissions, throttling

То есть DRF не просто вызывает get() или post(), а сначала подготавливает запрос и проверяет API-политики.

Упрощённая схема dispatch в DRF #

HTTP request
APIView.dispatch()
initialize_request()
initial()
выбор handler по HTTP-методу
get() / post() / put() / patch() / delete()
handle_exception(), если была ошибка
finalize_response()
HTTP response

Что происходит внутри #

Примерно логика такая:

def dispatch(self, request, *args, **kwargs):
    request = self.initialize_request(request, *args, **kwargs)
    self.request = request

    try:
        self.initial(request, *args, **kwargs)

        if request.method.lower() in self.http_method_names:
            handler = getattr(self, request.method.lower())
        else:
            handler = self.http_method_not_allowed

        response = handler(request, *args, **kwargs)

    except Exception as exc:
        response = self.handle_exception(exc)

    return self.finalize_response(request, response, *args, **kwargs)

Это не точная копия исходника, а упрощённое представление.

initialize_request() #

initialize_request() превращает обычный Django HttpRequest в DRF Request.

Обычный Django view получает:

django.http.HttpRequest

DRF view получает:

rest_framework.request.Request

Благодаря этому внутри get() / post() доступны DRF-возможности:

request.data
request.query_params
request.user
request.auth

В документации DRF указано, что initialize_request() гарантирует передачу в handler именно DRF Request, а не обычного Django HttpRequest.

initial() #

initial() выполняется до вызова get(), post() и других handler-методов.

Через него DRF запускает:

content negotiation
authentication
permissions
throttling

То есть перед тем как попасть в твой метод:

def get(self, request):
    ...

DRF уже может проверить:

кто пользователь
авторизован ли он
имеет ли права
не превышен ли throttle limit
какой renderer/parser использовать

Документация DRF прямо указывает, что initial() используется для permissions, throttling и content negotiation.

handle_exception() #

Если внутри view возникает ошибка:

raise PermissionDenied()
raise NotAuthenticated()
raise ValidationError()
raise Http404()

DRF перехватывает её через handle_exception() и превращает в нормальный API-ответ.

Например:

{
  "detail": "Authentication credentials were not provided."
}

А не просто HTML-страницу ошибки Django.

DRF указывает, что handle_exception() обрабатывает APIException, а также Django Http404 и PermissionDenied, возвращая соответствующий response.

finalize_response() #

finalize_response() вызывается после выполнения handler-метода.

Он доводит ответ до корректного DRF-формата:

выбирает renderer
применяет content negotiation
готовит Response к отдаче клиенту

Например, ты возвращаешь:

return Response({"status": "ok"})

А DRF уже решает, как это отрендерить:

JSON
Browsable API
другой renderer

Документация DRF указывает, что finalize_response() гарантирует рендеринг Response в правильный content type, выбранный через content negotiation.

Зачем нужен dispatch на практике #

dispatch() нужен как общий входной механизм для всех HTTP-методов.

Без него пришлось бы отдельно обрабатывать:

GET-запросы
POST-запросы
PUT-запросы
PATCH-запросы
DELETE-запросы
OPTIONS-запросы

А с ним view выглядит просто:

from rest_framework.views import APIView
from rest_framework.response import Response

class UserView(APIView):
    def get(self, request):
        return Response({"method": "GET"})

    def post(self, request):
        return Response({"method": "POST"})

dispatch() сам решит:

GET-запрос  → UserView.get()
POST-запрос → UserView.post()

Нужно ли переопределять dispatch #

Обычно — нет.

В DRF чаще переопределяют не dispatch(), а более узкие методы:

def initial(self, request, *args, **kwargs):
    ...
def get_permissions(self):
    ...
def get_authenticators(self):
    ...
def finalize_response(self, request, response, *args, **kwargs):
    ...

Переопределять dispatch() стоит редко, потому что легко сломать стандартный DRF-пайплайн:

authentication
permissions
throttling
exception handling
response finalization

Краткий вывод #

dispatch() в DRF нужен для того, чтобы принять HTTP-запрос, подготовить его как DRF Request, выполнить проверки, выбрать нужный handler-метод и вернуть корректный DRF Response.

Главная схема:

APIView.dispatch()
подготовить request
проверить auth / permissions / throttling
вызвать get() / post() / put() / patch() / delete()
обработать ошибки
финализировать response


11. View Set vs Base View #

APIView и ViewSet — это два разных уровня абстракции для создания API endpoint’ов в DRF.

APIView — Базовый уровень (ручное управление)

Концепция:

  • Один класс = Один endpoint
  • Явное определение HTTP-методов
  • Полный контроль над логикой

Структура:

class UserAPI(APIView):
    def get(self, request): pass    # GET /users/
    def post(self, request): pass   # POST /users/

class UserDetailAPI(APIView):
    def get(self, request, pk): pass     # GET /users/1/
    def put(self, request, pk): pass     # PUT /users/1/
    def delete(self, request, pk): pass  # DELETE /users/1/

Когда использовать:

  • Нестандартная логика обработки запросов
  • Специфичные endpoint’ы (не CRUD)
  • Тонкая настройка каждого метода
  • Микросервисы с небольшой функциональностью

ViewSet — Высокий уровень (автоматизация)

Концепция:

  • Один класс = Все операции с ресурсом
  • Автоматическая маршрутизация
  • Стандартные CRUD операции

Структура:

class UserViewSet(ViewSet):
    def list(self, request): pass      # GET /users/
    def create(self, request): pass    # POST /users/
    def retrieve(self, request, pk): pass   # GET /users/1/
    def update(self, request, pk): pass     # PUT /users/1/
    def destroy(self, request, pk): pass    # DELETE /users/1/

Когда использовать:

  • Стандартные CRUD операции
  • Быстрая разработка API
  • Ресурс-ориентированная архитектура
  • Админ-панели и бэкенды

ModelViewSet — Максимальная автоматизация

Концепция:

  • Автоматические CRUD операции для модели
  • Минимальный код
class UserViewSet(ModelViewSet):
    queryset = User.objects.all()
    serializer_class = UserSerializer
    # Полный CRUD готов!

Сравнительная таблица

КритерийAPIViewViewSet
Уровень контроля✅ Полный⚠️ Ограниченный
Количество кода❌ Много✅ Мало
Стандартизация❌ Свободная✅ Единая
Маршрутизация❌ Ручная✅ Ручная
CRUD операции❌ Явные✅ Автоматические


12. Объясни как происходит обработка HTTP запросов на Django от получения запроса клиентом до отдачи ответа сервером #

Когда к приложению приходит запрос, то URL dispatcher определяет, с каким ресурсом сопоставляется данный запрос и передает этот запрос выбранному ресурсу. Ресурс фактически представляет функцию или View, который получает запрос и определенным образом обрабатывает его. В процессе обработки View может обращаться к моделям и базе данных, получать из нее данные, или, наоборот, сохранять в нее данные. Результат обработки запроса отправляется обратно, и этот результат пользователь видит в своем браузере. Как правило, результат обработки запроса представляет сгенерированный html-код, для генерации которого применяются шаблоны (Template)

HTTP Запрос → WSGI Сервер → Middleware → URLconf → View → Model 
→ Template → Middleware → HTTP Ответ


13. Какие механизмы наследования моделей есть в Django? #

Механизмы наследования моделей в Django #

В Django есть 3 основных механизма наследования моделей:

1. Abstract base class
2. Multi-table inheritance
3. Proxy model

Официальная документация Django описывает именно эти варианты: абстрактные базовые модели, multi-table inheritance и proxy models.

1. Abstract base class #

Абстрактная базовая модель используется, когда нужно вынести общие поля и методы в родительский класс, но не создавать для него отдельную таблицу в базе данных.

from django.db import models


class TimeStampedModel(models.Model):
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    class Meta:
        abstract = True


class Article(TimeStampedModel):
    title = models.CharField(max_length=255)
    text = models.TextField()


class Comment(TimeStampedModel):
    text = models.TextField()

В базе будут таблицы:

app_article
app_comment

Но таблицы для TimeStampedModel не будет.

Поля created_at и updated_at физически попадут в таблицы дочерних моделей.

Используется, когда:

- нужно переиспользовать одинаковые поля;
- нужно переиспользовать методы модели;
- родительская модель сама по себе не должна существовать в БД.

Документация Django прямо указывает, что abstract base class подходит, когда родительский класс нужен только для хранения общей информации, которую не хочется повторять в каждой дочерней модели.

2. Multi-table inheritance #

Multi-table inheritance используется, когда родительская модель и дочерняя модель должны быть полноценными моделями со своими таблицами.

from django.db import models


class Place(models.Model):
    name = models.CharField(max_length=100)
    address = models.CharField(max_length=255)


class Restaurant(Place):
    serves_pizza = models.BooleanField(default=False)
    serves_hot_dogs = models.BooleanField(default=False)

В базе будут таблицы:

app_place
app_restaurant

Django автоматически создаёт связь между дочерней и родительской таблицей через OneToOneField.

Условно это выглядит так:

Place
    id
    name
    address

Restaurant
    place_ptr_id
    serves_pizza
    serves_hot_dogs

То есть Restaurant наследует поля Place, но данные хранятся в разных таблицах. Django указывает, что при multi-table inheritance каждая модель в иерархии соответствует собственной таблице, а связь между дочерней и родительской моделью создаётся через автоматически созданный OneToOneField.

Используется, когда:

- родительская модель имеет самостоятельный смысл;
- дочерняя модель расширяет родительскую;
- нужно отдельно работать и с родителем, и с потомком.

Минус: запросы могут становиться тяжелее, потому что Django часто должен делать JOIN между таблицами.

3. Proxy model #

Proxy model используется, когда не нужно менять структуру таблицы, но нужно изменить Python-поведение модели.

Например:

from django.db import models


class User(models.Model):
    username = models.CharField(max_length=100)
    is_active = models.BooleanField(default=True)


class ActiveUser(User):
    class Meta:
        proxy = True
        ordering = ["username"]

    def deactivate(self):
        self.is_active = False
        self.save()

В базе будет только одна таблица:

app_user

Отдельной таблицы для ActiveUser не будет.

Proxy-модель может использоваться для:

- другого manager;
- другого ordering;
- дополнительных методов;
- отдельного отображения модели в Django admin;
- изменения поведения без изменения схемы БД.

Django описывает proxy model как способ изменить Python-поведение модели, например default manager, ordering или методы, без изменения исходной таблицы. Данные при этом сохраняются так, как если бы использовалась оригинальная модель.

Краткое сравнение #

МеханизмСоздаёт таблицу для родителяСоздаёт таблицу для потомкаДля чего нужен
abstract = TrueНетДаПереиспользовать поля и методы
Multi-table inheritanceДаДаРеальное наследование моделей в БД
proxy = TrueИспользует таблицу родителяНетИзменить поведение модели без изменения БД

Практический выбор #

Нужны общие поля created_at / updated_at / is_deleted?
    -> Abstract base class

Нужно, чтобы родитель и потомок были отдельными сущностями в БД?
    -> Multi-table inheritance

Нужно изменить manager, ordering, методы или admin-поведение без новой таблицы?
    -> Proxy model

Важный нюанс #

Кроме этих механизмов, можно использовать обычные Python mixins, но они подходят в основном для методов и вспомогательной логики.

Например:

class SoftDeleteMixin:
    def soft_delete(self):
        self.is_deleted = True
        self.save()

Но если mixin должен добавлять поля модели, обычно его делают абстрактной Django-моделью:

class SoftDeleteModel(models.Model):
    is_deleted = models.BooleanField(default=False)

    class Meta:
        abstract = True

Ответ для собеседования #

В Django есть три основных вида наследования моделей:

Abstract base class

Используется для переиспользования общих полей и методов. Родительская таблица не создаётся.

Multi-table inheritance

Каждая модель получает свою таблицу. Django связывает дочернюю таблицу с родительской через OneToOneField.

Proxy model

Новая таблица не создаётся. Используется та же таблица, что и у родительской модели, но можно изменить Python-поведение: manager, ordering, методы, admin-представление.


14. Что такое и зачем нужен GenericForeignKey в Django? #

Что такое GenericForeignKey #

GenericForeignKey — это механизм Django, который позволяет одной модели ссылаться на объекты разных моделей, а не только на одну конкретную модель.

Обычный ForeignKey жёстко привязан к одной таблице:

class Comment(models.Model):
    article = models.ForeignKey("Article", on_delete=models.CASCADE)

Такой комментарий может относиться только к Article.

GenericForeignKey позволяет сделать так:

Comment может относиться к Article
Comment может относиться к Video
Comment может относиться к Product
Comment может относиться к любой другой модели

Django реализует это через contenttypes framework. Этот framework хранит информацию о моделях, установленных в проекте, и даёт универсальный способ ссылаться на модель по её типу. ( Django Project)

Из чего состоит GenericForeignKey #

Обычно используются 3 поля:

from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
from django.db import models


class Comment(models.Model):
    text = models.TextField()

    content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
    object_id = models.PositiveIntegerField()

    content_object = GenericForeignKey("content_type", "object_id")

Здесь:

content_type
    хранит тип модели, например Article или Video

object_id
    хранит id конкретного объекта

content_object
    Python-обёртка, через которую можно получить сам объект

Например:

article = Article.objects.get(id=1)

comment = Comment.objects.create(
    text="Good article",
    content_object=article,
)

Django сам заполнит:

content_type = ContentType для Article
object_id = article.id

После этого можно обращаться так:

comment.content_object

И получить объект Article.

Зачем нужен GenericForeignKey #

Главная задача — реализовать полиморфную связь.

То есть связь вида:

одна модель может быть связана с объектами разных моделей

Типичные примеры:

- комментарии к разным сущностям;
- лайки к разным сущностям;
- теги для разных моделей;
- избранное;
- история действий;
- вложения к разным объектам;
- уведомления, связанные с разными объектами.

Например, модель Like:

class Like(models.Model):
    user = models.ForeignKey("auth.User", on_delete=models.CASCADE)

    content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
    object_id = models.PositiveIntegerField()
    content_object = GenericForeignKey("content_type", "object_id")

Теперь один и тот же Like можно привязать к статье, комментарию, посту, товару и т.д.

Почему нельзя просто сделать несколько ForeignKey #

Можно сделать так:

class Comment(models.Model):
    article = models.ForeignKey("Article", null=True, blank=True, on_delete=models.CASCADE)
    video = models.ForeignKey("Video", null=True, blank=True, on_delete=models.CASCADE)
    product = models.ForeignKey("Product", null=True, blank=True, on_delete=models.CASCADE)

Но это плохой вариант, если моделей много или список моделей может расширяться.

Проблемы:

- много nullable-полей;
- нужно следить, чтобы было заполнено только одно поле;
- при добавлении новой модели нужно менять схему БД;
- модель становится грязной и плохо масштабируется.

GenericForeignKey решает это тем, что хранит не отдельное поле на каждую модель, а пару:

тип модели + id объекта

Как это выглядит в базе данных #

Допустим, есть комментарии к статье и видео.

Article
id | title
1  | Django ORM

Video
id | title
5  | DRF lesson

Comment
id | text        | content_type_id | object_id
1  | Nice post   | Article         | 1
2  | Good video  | Video           | 5

Физически в таблице Comment нет настоящего внешнего ключа на Article или Video.

Есть только:

content_type_id
object_id

Django на уровне Python понимает, к какой модели и объекту нужно обратиться.

Важный минус #

GenericForeignKey не является обычным ForeignKey на уровне базы данных.

Из-за этого:

- база данных не может нормально проверить ссылочную целостность;
- нельзя сделать полноценный SQL-level foreign key на разные таблицы сразу;
- сложнее делать JOIN;
- сложнее оптимизировать запросы;
- можно получить битую ссылку, если объект удалён;
- не всегда удобно фильтровать через ORM.

Например, такой запрос напрямую не сработает как с обычным ForeignKey:

Comment.objects.filter(content_object=article)

Обычно фильтруют через content_type и object_id:

content_type = ContentType.objects.get_for_model(article)

Comment.objects.filter(
    content_type=content_type,
    object_id=article.id,
)

Django в документации показывает, что GenericForeignKey строится на двух реальных полях — content_type и object_id, а само поле GenericForeignKey является интерфейсом для доступа к связанному объекту.

Обратная связь через GenericRelation #

Чтобы с объекта удобно получать связанные generic-объекты, можно добавить GenericRelation.

from django.contrib.contenttypes.fields import GenericRelation


class Article(models.Model):
    title = models.CharField(max_length=255)
    comments = GenericRelation(Comment)

Теперь можно делать так:

article = Article.objects.get(id=1)

article.comments.all()

Без GenericRelation тоже можно найти комментарии, но пришлось бы вручную фильтровать по content_type и object_id.

Когда использовать #

GenericForeignKey уместен, когда связь действительно должна быть универсальной.

Хорошие случаи:

Comment -> любой комментируемый объект
Like -> любой лайкаемый объект
Attachment -> любой объект, к которому можно прикрепить файл
ActivityLog -> любой объект, с которым связано действие

Когда лучше не использовать #

Не стоит использовать GenericForeignKey, если связь на самом деле ограничена одной моделью.

Например:

Order всегда относится только к User
Book всегда относится только к Author
Comment всегда относится только к Article

В таких случаях лучше обычный ForeignKey:

class Comment(models.Model):
    article = models.ForeignKey(Article, on_delete=models.CASCADE)

Обычный ForeignKey проще, надёжнее и лучше контролируется базой данных.

Обобщая #

GenericForeignKey — это механизм Django из contenttypes framework, который позволяет одной модели ссылаться на объекты разных моделей. Он хранит не обычный внешний ключ, а пару content_type и object_id: первый определяет модель, второй — id объекта. Это удобно для комментариев, лайков, тегов, истории действий и вложений, которые могут относиться к разным сущностям.

Главный минус: это не настоящий внешний ключ на уровне БД, поэтому база не гарантирует ссылочную целостность, а запросы и оптимизация сложнее, чем с обычным ForeignKey.