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:
- Обертывает запрос: middleware выступает как обертка вокруг других функций или middleware.
- Обрабатывает запрос: перед тем как запрос достигнет целевого обработчика (например, вашей функции просмотра), middleware может модифицировать его или выполнить необходимую логику.
- Обрабатывает ответ: после того как запрос был обработан, middleware может модифицировать ответ перед его отправкой клиенту.
- Передает управление: 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
Cookie и hidden token — это не одно и то же #
В 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.
8. Чем отличается select_related от prefetch_related #
select_related и prefetch_related решают одну проблему: уменьшают количество SQL-запросов при доступе к связанным объектам.
Главное отличие:
select_related → делает SQL JOIN и получает связанные данные одним запросом
prefetch_related → делает отдельные SQL-запросы и связывает данные в Python
Официальная документация Django прямо указывает, что select_related() использует SQL JOIN и включает поля связанного объекта в SELECT, а prefetch_related() выполняет отдельные запросы и делает связывание в Python.
select_related #
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 #
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 нельзя использовать для ManyToMany #
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_related | prefetch_related |
|---|---|---|
| Как работает | SQL JOIN | Отдельные запросы |
| Где связывает данные | В базе данных | В Python |
| Количество запросов | Обычно 1 | Обычно 2 или больше |
| Подходит для | ForeignKey, OneToOne | ManyToMany, 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 готов!
Сравнительная таблица
| Критерий | APIView | ViewSet |
|---|---|---|
| Уровень контроля | ✅ Полный | ⚠️ Ограниченный |
| Количество кода | ❌ Много | ✅ Мало |
| Стандартизация | ❌ Свободная | ✅ Единая |
| Маршрутизация | ❌ Ручная | ✅ Ручная |
| 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.