Django: как устроен Python-фреймворк для веб-приложений
Django - веб-фреймворк на Python для разработки серверной части приложений.
Он предоставляет готовую структуру и набор компонентов, которые обычно приходится собирать отдельно:
- маршрутизацию HTTP-запросов;
- работу с базой данных;
- систему моделей и миграций;
- HTML-шаблоны;
- обработку форм;
- аутентификацию и права доступа;
- административный интерфейс;
- защиту от распространённых веб-уязвимостей;
- тестовые инструменты;
- механизм расширения через приложения и middleware.
Главная задача Django - не написать приложение вместо разработчика, а задать правила взаимодействия его частей.
HTTP-запрос - маршрутизация - представление - бизнес-логика - данные - HTTP-ответ
Django особенно полезен, когда в одном приложении нужно связать данные, пользователей, формы, административные операции и HTTP-интерфейс.
Что такое веб-фреймворк
Веб-приложение принимает HTTP-запросы и возвращает HTTP-ответы.
Даже простому серверу нужно решить несколько задач:
Как запустить сервер
Как определить обработчик URL
Как прочитать параметры запроса
Как обратиться к базе данных
Как сформировать ответ
Как обработать ошибку
Как проверить пользователя
Как защитить изменяющие запросы
Веб-фреймворк предоставляет общую инфраструктуру:
- жизненный цикл запроса;
- соглашения о структуре кода;
- готовые абстракции;
- расширяемые компоненты;
- стандартные способы решения типовых задач.
Django относится к полнофункциональным фреймворкам. Он включает большинство базовых средств для серверного веб-приложения.
Микрофреймворк - предоставляет минимальное ядро
Полнофункциональный фреймворк - предоставляет связанный набор компонентов
Основные механизмы уже входят в общую архитектуру, независимо от размера приложения.
Что именно делает Django
Django находится между веб-сервером, кодом приложения и внешними ресурсами.
Клиент - веб-сервер - Django - код приложения - база данных и другие сервисы
В упрощённом виде запрос проходит следующий путь:
1. Django получает запрос.
2. Middleware обрабатывает запрос до представления.
3. URL dispatcher выбирает обработчик.
4. Представление выполняет логику.
5. При необходимости выполняются запросы к базе данных.
6. Представление создаёт ответ.
7. Middleware обрабатывает ответ.
8. Ответ возвращается клиенту.
Django управляет связями между компонентами, но конкретные правила приложения пишет разработчик.
Например, Django умеет:
- найти запись в базе;
- проверить форму;
- определить текущего пользователя;
- отрисовать шаблон.
Но Django не знает:
- когда заказ можно отменить;
- кому разрешено менять договор;
- как рассчитать скидку;
- какой статус назначить заявке;
- что считать успешной операцией.
Это бизнес-логика конкретной системы.
Архитектура Django
Архитектуру Django часто называют MVT.
Model - данные и правила работы с ними
View - обработка запроса и формирование ответа
Template - представление HTML
Название похоже на MVC, но термины распределены немного иначе.
В Django:
Model - модель данных
View - контроллер запроса
Template - визуальное представление
MVT описывает основной поток, но приложение может содержать дополнительные слои:
- сервисы;
- селекторы;
- формы;
- сериализаторы;
- менеджеры;
- команды;
- фоновые задачи;
- интеграции;
- политики доступа;
- валидаторы.
MVT описывает основной поток, а не запрещает дополнительные слои.
Проект и приложение
В Django различаются проект и приложение.
Проект - конфигурация всей системы
Приложение - отдельный функциональный модуль
Проект
Проект хранит общую конфигурацию:
- настройки;
- корневую маршрутизацию;
- конфигурацию серверного интерфейса;
- список подключённых приложений;
- настройки базы данных;
- middleware;
- шаблоны;
- статические файлы;
- локализацию.
После создания проекта обычно появляется структура:
config/
├── __init__.py
├── settings.py
├── urls.py
├── asgi.py
└── wsgi.py
Название config условно. Оно может быть другим.
Приложение
Приложение отвечает за часть предметной области или функциональности.
Примеры:
users - пользователи и профили
orders - заказы
catalog - товары и категории
payments - платежи
support - обращения
Типичная структура приложения:
orders/
├── migrations/
├── __init__.py
├── admin.py
├── apps.py
├── models.py
├── tests.py
├── urls.py
└── views.py
Дополнительные файлы добавляются по мере необходимости.
forms.py - формы
services.py - операции предметной области
selectors.py - чтение данных
validators.py - проверки
Приложение не равно странице
Ошибка начинающих - создавать отдельное Django-приложение для каждой страницы.
Приложение лучше выделять по связной области ответственности.
Плохое деление - homepage, contacts_page, profile_page
Более логичное деление - users, orders, catalog
Одна страница может использовать несколько приложений, а одно приложение может участвовать в нескольких страницах.
Создание проекта
Django устанавливается как Python-пакет.
python -m pip install Django
Проект создаётся командой:
django-admin startproject config .
Точка означает, что файлы проекта нужно создать в текущем каталоге.
Приложение создаётся отдельно:
python manage.py startapp orders
После создания его нужно подключить в INSTALLED_APPS.
INSTALLED_APPS = [
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
"orders",
]
manage.py - командный интерфейс проекта.
Через него запускаются:
- сервер разработки;
- миграции;
- тесты;
- интерактивная оболочка;
- административные команды;
- собственные команды проекта.
Настройки проекта
Основные настройки находятся в settings.py.
Это обычный Python-модуль.
В нём задаются:
INSTALLED_APPS - подключённые приложения
MIDDLEWARE - цепочка middleware
DATABASES - подключения к базам данных
TEMPLATES - шаблонизаторы
STATIC_URL - адрес статических файлов
MEDIA_ROOT - хранилище пользовательских файлов
LANGUAGE_CODE - основной язык
TIME_ZONE - часовой пояс
SECRET_KEY - секрет проекта
DEBUG - режим отладки
ALLOWED_HOSTS - допустимые имена хостов
Настройки являются частью окружения
Часть значений поступает из окружения:
- секретные ключи;
- пароли;
- адрес базы данных;
- настройки почты;
- внешние API-ключи;
- режим отладки;
- список хостов.
Обычно они поступают из переменных окружения.
import os
SECRET_KEY = os.environ["DJANGO_SECRET_KEY"]
DEBUG = os.environ.get("DJANGO_DEBUG") == "1"
Режим DEBUG
При DEBUG = True Django показывает подробную отладочную страницу с информацией о запросе и коде. Этот режим используют во время разработки, чтобы быстрее находить ошибки.
Путь HTTP-запроса
Чтобы понять Django, полезно проследить один запрос целиком.
Пусть клиент отправляет:
GET /orders/42/
Общий путь:
Запрос - middleware - корневой urls.py - urls.py приложения - view - ORM или другая логика - template или JSON - response - middleware - клиент
Каждый уровень решает свою задачу.
URL dispatcher
URL dispatcher сопоставляет путь запроса с представлением.
Корневой файл:
from django.contrib import admin
from django.urls import include, path
urlpatterns = [
path("admin/", admin.site.urls),
path("orders/", include("orders.urls")),
]
Здесь:
admin/ - маршруты административной панели
orders/ - передача маршрутизации приложению orders
В приложении:
from django.urls import path
from . import views
app_name = "orders"
urlpatterns = [
path("", views.order_list, name="list"),
path("<int:order_id>/", views.order_detail, name="detail"),
]
Маршрут состоит из частей
Паттерн пути - какой URL подходит
View - какой обработчик вызвать
Name - как обращаться к маршруту из кода
В маршруте:
path("<int:order_id>/", views.order_detail, name="detail")
<int:order_id> является конвертером пути.
Он:
- проверяет, что сегмент похож на целое число;
- преобразует строку в
int; - передаёт значение в представление под именем
order_id.
Запрос:
GET /orders/42/
приведёт к вызову:
order_detail(request, order_id=42)
Имена маршрутов
Жёстко записывать адреса в шаблонах и коде неудобно.
Django позволяет строить URL по имени:
<a href="{% url 'orders:detail' order.id %}">
Открыть заказ
</a>
Представление
View, или представление, принимает HTTP-запрос и возвращает HTTP-ответ.
Функциональное представление:
from django.http import HttpResponse
def order_list(request):
return HttpResponse("Список заказов")
Минимальный контракт:
Вход - HttpRequest
Выход - HttpResponse
Представление не обязано возвращать HTML. Оно может вернуть:
- текст;
- HTML;
- JSON;
- файл;
- перенаправление;
- потоковый ответ;
- ошибку.
Объект request
request содержит данные текущего HTTP-запроса.
Основные части:
request.method - HTTP-метод
request.path - путь
request.GET - query parameters
request.POST - поля формы
request.body - исходное тело
request.headers - заголовки
request.COOKIES - cookie
request.FILES - загруженные файлы
request.user - текущий пользователь
request.session - данные сессии
Пример:
from django.http import JsonResponse
def order_list(request):
status = request.GET.get("status")
return JsonResponse({
"status_filter": status,
})
Запрос:
GET /orders/?status=paid
передаст:
request.GET["status"] - paid
Функциональные и классовые представления
Django поддерживает два основных подхода.
Function-based view - представление в виде функции
Class-based view - представление в виде класса
Они решают одну задачу: получают HttpRequest и возвращают HttpResponse.
Разница заключается не в назначении, а в способе организации кода.
Оба подхода можно использовать в одном проекте. Не нужно выбирать один стиль для всех представлений.
Простое нестандартное представление - функция
Повторяющийся CRUD-сценарий - класс
Один проект - функции и классы одновременно
Даже похожие представления могут быть реализованы по-разному, если так код остаётся понятнее.
Функциональное представление
def order_detail(request, order_id):
...
Функция получает запрос напрямую, выполняет нужную последовательность действий и возвращает ответ.
Преимущества функциональных представлений
- поток выполнения виден сверху вниз;
- меньше скрытого поведения;
- удобно реализовывать нестандартную последовательность действий;
- проще быстро понять небольшой обработчик;
- не нужно разбираться в наследовании и mixin;
- удобно для представлений с небольшим числом ветвлений.
Недостатки функциональных представлений
- повторяющийся код приходится выносить вручную;
- обработка разных HTTP-методов может превратиться в большой
if; - сложнее переиспользовать поведение через наследование;
- крупная функция быстро становится перегруженной;
- типовые CRUD-сценарии приходится собирать самостоятельно.
Пример обработки нескольких методов:
def order_detail(request, order_id):
if request.method == "GET":
...
if request.method == "POST":
...
return HttpResponseNotAllowed([
"GET",
"POST",
])
Для небольшого обработчика такая структура может быть понятной. При большом числе методов и повторяющихся действий она становится громоздкой.
Классовое представление
from django.http import JsonResponse
from django.views import View
class OrderDetailView(View):
def get(self, request, order_id):
return JsonResponse({
"order_id": order_id,
})
HTTP-методы распределяются по методам класса:
GET - get()
POST - post()
PUT - put()
PATCH - patch()
DELETE - delete()
В маршруте классовое представление преобразуется в вызываемый обработчик:
path(
"<int:order_id>/",
OrderDetailView.as_view(),
name="detail",
)
Преимущества классовых представлений
- HTTP-методы разделены по отдельным методам класса;
- общую логику можно вынести в базовый класс;
- поведение можно переиспользовать через наследование;
- mixin позволяют добавлять отдельные возможности;
- generic views сокращают код типовых операций;
- удобно строить семейство похожих представлений;
- настройка может задаваться атрибутами класса.
Django предоставляет generic views для распространённых сценариев:
- вывод списка объектов;
- просмотр одного объекта;
- создание;
- обновление;
- удаление;
- обработка формы.
Например, список объектов можно описать через ListView:
from django.views.generic import ListView
from .models import Order
class OrderListView(ListView):
model = Order
template_name = "orders/list.html"
context_object_name = "orders"
Здесь базовый класс уже реализует получение QuerySet, обработку GET и передачу объектов в шаблон.
Недостатки классовых представлений
- часть поведения находится в базовых классах;
- поток выполнения не всегда виден в одном файле;
- нужно понимать порядок вызова методов;
- наследование может усложнить отладку;
- большое число mixin делает поведение неочевидным;
- для нестандартного сценария generic view иногда приходится переопределять сильнее, чем написать функцию;
- несколько похожих базовых классов могут создавать ложное ощущение, что задача уже решена автоматически.
Особенно важно понимать method resolution order, или MRO, если класс наследуется от нескольких mixin.
Mixin добавляет поведение
Порядок наследования влияет на порядок вызовов
Неправильная комбинация усложняет понимание представления
Generic views и обычный View
Не все классовые представления одинаковы.
View - базовое распределение по HTTP-методам
Generic view - готовая реализация типового сценария
Mixin - отдельное переиспользуемое поведение
Базовый View оставляет основную логику разработчику.
Generic views уже реализуют часть процесса:
- получение объекта;
- работу с формой;
- сохранение;
- удаление;
- формирование контекста;
- перенаправление после успеха.
Чем больше готового поведения используется, тем важнее понимать, какие методы вызывает базовый класс.
Можно ли смешивать оба подхода
Да. Функциональные и классовые представления не конфликтуют.
В одном приложении могут одновременно использоваться:
healthcheck - функция
нестандартная операция импорта - функция
список заказов - ListView
редактирование профиля - UpdateView
API-обработчик - View
Не существует требования реализовать все представления одинаково.
Также необязательно выбирать один подход для всех похожих страниц. Например, два представления списков могут быть устроены по-разному:
Обычный список моделей - ListView
Сложный отчёт из нескольких источников - функция
Главный критерий:
Выбранная форма представления должна делать поток выполнения и ответственность кода понятнее.
Функция обычно удобнее, когда логика короткая или нестандартная. Класс полезен, когда есть типовой сценарий, повторяющееся поведение или понятная возможность переиспользования. Если наследование и mixin скрывают больше, чем упрощают, лучше использовать функцию или более простой класс.
Модели
Модель описывает структуру и поведение данных предметной области.
from django.db import models
class Order(models.Model):
number = models.CharField(max_length=50, unique=True)
total = models.DecimalField(max_digits=12, decimal_places=2)
created_at = models.DateTimeField(auto_now_add=True)
Класс модели связан с таблицей.
Модель - описание сущности в Python
Поле модели - столбец таблицы
Экземпляр модели - строка
После миграции может появиться таблица:
orders_order
Django по умолчанию формирует имя из приложения и модели.
Поля модели
Часто используются:
CharField - короткая строка
TextField - большой текст
IntegerField - целое число
DecimalField - точное десятичное число
BooleanField - логическое значение
DateField - дата
DateTimeField - дата и время
UUIDField - UUID
JSONField - JSON
FileField - файл
ImageField - изображение
ForeignKey - связь многие к одному
OneToOneField - связь один к одному
ManyToManyField - связь многие ко многим
Тип поля влияет на:
- структуру базы;
- преобразование Python-значений;
- проверку формы;
- отображение в админ-панели;
- доступные выражения ORM.
Первичный ключ
Если поле с primary_key=True не объявлено, Django добавляет идентификатор автоматически.
id - автоматически созданный первичный ключ
Его тип зависит от настройки DEFAULT_AUTO_FIELD.
Связи между моделями
Многие к одному
Один пользователь может иметь несколько заказов.
from django.conf import settings
from django.db import models
class Order(models.Model):
user = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.PROTECT,
related_name="orders",
)
На уровне базы хранится внешний ключ.
Order.user - связанный объект
order.user_id - значение внешнего ключа
user.orders - обратная связь
on_delete
on_delete определяет, что делать при удалении связанного объекта.
Основные варианты:
CASCADE - удалить зависимые объекты
PROTECT - запретить удаление
RESTRICT - ограничить удаление с учётом связей
SET_NULL - установить NULL
SET_DEFAULT - установить значение по умолчанию
DO_NOTHING - не выполнять действие средствами ORM
Выбор зависит от смысла данных.
Один к одному
class Profile(models.Model):
user = models.OneToOneField(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name="profile",
)
Один пользователь связан не более чем с одним профилем.
Многие ко многим
class Product(models.Model):
name = models.CharField(max_length=200)
class Collection(models.Model):
name = models.CharField(max_length=200)
products = models.ManyToManyField(
Product,
related_name="collections",
)
Django создаёт промежуточную таблицу.
Если связь содержит собственные атрибуты, лучше объявить промежуточную модель явно.
Meta модели
Вложенный класс Meta задаёт параметры модели.
class Order(models.Model):
created_at = models.DateTimeField()
class Meta:
ordering = ["-created_at"]
indexes = [
models.Index(fields=["created_at"]),
]
Через Meta можно определить:
- сортировку;
- имя таблицы;
- индексы;
- ограничения;
- разрешения;
- абстрактность модели;
- человекочитаемые названия.
Ограничения
Правила целостности нужно закреплять не только в Python-коде, но и в базе.
class Product(models.Model):
price = models.DecimalField(
max_digits=12,
decimal_places=2,
)
class Meta:
constraints = [
models.CheckConstraint(
condition=models.Q(price__gte=0),
name="product_price_non_negative",
),
]
Ограничение базы защищает данные независимо от того, откуда пришла операция.
ORM
ORM, Object-Relational Mapping, связывает Python-объекты с реляционной базой данных.
Python-класс - таблица
Объект - строка
Атрибут - столбец
QuerySet - запрос и набор результатов
ORM позволяет описывать запросы средствами Python.
orders = Order.objects.filter(
total__gte=1000,
)
Упрощённый SQL-смысл:
SELECT *
FROM orders_order
WHERE total >= 1000;
ORM не отменяет SQL и устройство базы. Она создаёт над ними API.
Manager
objects является менеджером модели.
Order.objects
Он служит точкой входа для запросов.
Order.objects.all()
Order.objects.filter(...)
Order.objects.get(...)
Order.objects.create(...)
QuerySet
QuerySet описывает запрос и его результаты.
orders = Order.objects.filter(status="paid")
На момент создания переменной запрос обычно ещё не выполнен.
Это называют ленивым выполнением.
SQL выполняется, когда данные действительно нужны:
- при переборе;
- преобразовании в список;
- выводе;
- срезе с шагом;
- вызове методов, возвращающих готовый результат.
Получение одного объекта
order = Order.objects.get(pk=42)
get() ожидает одну строку.
Возможные результаты:
Одна строка - объект
Ни одной строки - DoesNotExist
Несколько строк - MultipleObjectsReturned
Для представлений часто используется:
from django.shortcuts import get_object_or_404
order = get_object_or_404(Order, pk=order_id)
Если объект не найден, возвращается HTTP 404.
Фильтрация
Order.objects.filter(status="paid")
Условия можно объединять:
Order.objects.filter(
status="paid",
total__gte=1000,
)
Суффиксы после двойного подчёркивания называют lookup.
total__gte - больше или равно
created_at__date - дата
name__icontains - регистронезависимое вхождение
user__email - поле связанной модели
Сортировка
Order.objects.order_by("-created_at")
created_at - по возрастанию
-created_at - по убыванию
select_related и prefetch_related
Связи могут привести к большому числу запросов.
orders = Order.objects.all()
for order in orders:
print(order.user.email)
Если пользователь не был загружен заранее, обращение к order.user может выполнить отдельный запрос для каждого заказа.
Это называют проблемой N+1.
select_related
Подходит для одиночных связей:
ForeignKey;OneToOneField.
orders = Order.objects.select_related("user")
Django получает связанные данные через SQL JOIN.
prefetch_related
Подходит для коллекций:
- обратные внешние ключи;
- many-to-many;
- сложные наборы связанных объектов.
users = User.objects.prefetch_related("orders")
Django выполняет отдельные запросы и объединяет результаты в Python.
select_related - SQL JOIN для одиночной связи
prefetch_related - отдельная загрузка коллекций
Создание и изменение объектов
Создание
order = Order.objects.create(
number="ORD-1042",
total="2500.00",
)
Отдельное сохранение
order = Order(
number="ORD-1042",
total="2500.00",
)
order.save()
Изменение объекта
order.status = "paid"
order.save(update_fields=["status"])
Массовое обновление
Order.objects.filter(
status="new",
).update(
status="processing",
)
Массовое обновление выполняется на уровне SQL и не вызывает save() каждого объекта.
Транзакции
Несколько операций могут составлять одно логическое действие.
Создать заказ
Списать остаток товара
Записать платёж
Изменить статус
Если одна операция завершилась ошибкой, предыдущие изменения не должны оставлять систему в промежуточном состоянии.
Django предоставляет transaction.atomic().
from django.db import transaction
@transaction.atomic
def create_order(...):
...
или:
with transaction.atomic():
...
Успешно - изменения фиксируются
Ошибка - изменения внутри блока откатываются
Транзакция обеспечивает целостность базы, но не откатывает автоматически внешние действия, например отправленное письмо или запрос к стороннему API.
Миграции
Модель в Python и структура базы должны изменяться согласованно.
Миграция описывает переход схемы из одного состояния в другое.
Модель изменена - создан файл миграции - миграция применена к базе
Создание миграций:
python manage.py makemigrations
Применение:
python manage.py migrate
Просмотр SQL:
python manage.py sqlmigrate orders 0001
Что хранит миграция
Миграция может:
- создать таблицу;
- добавить поле;
- изменить поле;
- создать индекс;
- добавить ограничение;
- перенести данные;
- удалить структуру.
Миграции являются историей схемы
Нельзя воспринимать их как временные файлы, которые можно бездумно удалить.
В командной разработке миграции сохраняются в репозитории и применяются в одинаковом порядке.
Шаблоны
Django Template Language формирует текстовый ответ, обычно HTML.
Представление передаёт контекст:
from django.shortcuts import render
def order_detail(request, order_id):
order = get_object_or_404(
Order,
pk=order_id,
)
return render(
request,
"orders/detail.html",
{
"order": order,
},
)
Шаблон:
<h1>Заказ {{ order.number }}</h1>
<p>Сумма: {{ order.total }}</p>
Контекст шаблона
Контекст - словарь значений, доступных шаблону.
Ключ словаря - имя в шаблоне
Значение - объект Python
Условия и циклы
{% if order.is_paid %}
<p>Заказ оплачен</p>
{% endif %}
{% for item in order.items.all %}
<p>{{ item.product.name }}</p>
{% empty %}
<p>Позиции отсутствуют</p>
{% endfor %}
Наследование шаблонов
Базовый шаблон:
<!doctype html>
<html lang="ru">
<head>
<meta charset="utf-8">
<title>{% block title %}Сайт{% endblock %}</title>
</head>
<body>
{% block content %}{% endblock %}
</body>
</html>
Дочерний:
{% extends "base.html" %}
{% block title %}Заказы{% endblock %}
{% block content %}
<h1>Заказы</h1>
{% endblock %}
Автоматическое экранирование
При выводе HTML Django по умолчанию экранирует опасные символы.
Если пользователь ввёл:
<script>alert("xss")</script>
шаблон не должен выполнить этот код как часть страницы.
Автоматическое экранирование снижает риск XSS, но разработчик может отключить его специальными средствами. Делать это следует только для доверенного HTML.
Обычный пользовательский текст - экранировать
Проверенный HTML - разрешать осознанно
Формы
Форма в Django отвечает не только за HTML-поля.
Она связывает:
Входные данные - преобразование типов - валидация - ошибки - очищенные значения - HTML-представление
Пример:
from django import forms
class OrderForm(forms.Form):
number = forms.CharField(max_length=50)
total = forms.DecimalField(
min_value=0,
max_digits=12,
decimal_places=2,
)
Обработка:
def create_order(request):
if request.method == "POST":
form = OrderForm(request.POST)
if form.is_valid():
number = form.cleaned_data["number"]
total = form.cleaned_data["total"]
...
else:
form = OrderForm()
return render(
request,
"orders/create.html",
{
"form": form,
},
)
Связанная и несвязанная форма
Unbound form - данных для проверки ещё нет
Bound form - в форму переданы данные
is_valid():
- преобразует значения;
- запускает валидаторы;
- заполняет
cleaned_data; - формирует ошибки.
ModelForm
ModelForm создаёт форму на основе модели.
from django import forms
from .models import Order
class OrderForm(forms.ModelForm):
class Meta:
model = Order
fields = [
"number",
"total",
]
ModelForm уменьшает повторение, но не отменяет понимание модели и правил валидации.
Модель - всё состояние объекта
Форма - только данные, которые разрешено вводить в этом сценарии
CSRF
CSRF - атака, при которой браузер авторизованного пользователя отправляет изменяющий запрос с чужой страницы.
Django использует CSRF-токен для защиты форм.
<form method="post">
{% csrf_token %}
...
</form>
Токен доказывает, что форма была создана приложением для текущего пользовательского контекста.
CSRF-защита важна для запросов, которые изменяют состояние:
- создание;
- изменение;
- удаление;
- отправка операции.
Она не заменяет проверку прав пользователя.
Административная панель
Django admin - готовый интерфейс управления моделями.
Подключение:
from django.contrib import admin
from .models import Order
admin.site.register(Order)
После регистрации модель доступна в административном интерфейсе пользователям с подходящими правами.
Настройка ModelAdmin
@admin.register(Order)
class OrderAdmin(admin.ModelAdmin):
list_display = [
"number",
"status",
"total",
"created_at",
]
list_filter = [
"status",
"created_at",
]
search_fields = [
"number",
"user__email",
]
Админ-панель полезна для:
- внутренних операций;
- модерации;
- справочников;
- просмотра данных;
- поддержки;
- контент-менеджмента.
Она не обязана быть пользовательским интерфейсом продукта.
Admin - внутренний инструмент
Публичный интерфейс - часть пользовательского продукта
Пользователи и аутентификация
Django включает систему пользователей, групп, прав и сессий.
request.user представляет текущего пользователя.
def profile(request):
if request.user.is_authenticated:
...
Ограничение доступа
from django.contrib.auth.decorators import login_required
@login_required
def order_list(request):
...
Права
Модель обычно получает стандартные разрешения:
add - создание
change - изменение
delete - удаление
view - просмотр
Проверка:
request.user.has_perm(
"orders.change_order"
)
Собственная модель пользователя
Если проекту нужна нестандартная модель пользователя, её лучше определить в начале проекта.
Связи следует строить через:
settings.AUTH_USER_MODEL
а не напрямую через конкретный класс встроенного пользователя.
Сессии и cookie
HTTP сам по себе не хранит состояние между запросами.
Сессия позволяет связать несколько запросов одного клиента.
request.session["cart_id"] = 42
Чтение:
cart_id = request.session.get("cart_id")
Обычно в cookie хранится идентификатор сессии, а сами данные находятся на стороне сервера.
Cookie - идентификатор или небольшие клиентские данные
Session store - состояние сессии
Сессия не должна заменять постоянную базу данных для важных бизнес-объектов.
Middleware
Middleware - компонент, который обрабатывает запрос до представления и ответ после представления.
Запрос - middleware 1 - middleware 2 - view - middleware 2 - middleware 1 - ответ
Middleware подходит для сквозных задач:
- сессии;
- аутентификация;
- безопасность;
- локализация;
- журналирование;
- измерение времени;
- заголовки;
- обработка исключений.
Пример:
class RequestTimeMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
response = self.get_response(request)
response["X-App"] = "example"
return response
Что не следует помещать в middleware
Middleware запускается для большого числа запросов.
Туда не стоит переносить узкую бизнес-логику конкретной страницы или модели.
Сквозная инфраструктура - middleware
Сценарий предметной области - сервис или view
Сигналы
Сигналы позволяют обработчику реагировать на события.
Например:
Модель сохранена
Пользователь вошёл
Миграция завершена
Сигналы полезны для слабосвязанной реакции, но могут скрывать поток выполнения.
Если создание заказа неожиданно запускает несколько обработчиков, код становится сложнее отслеживать и тестировать.
Явная бизнес-операция - обычный вызов функции
Независимая реакция инфраструктуры - возможный сигнал
Не следует использовать сигналы как основной способ организации всей бизнес-логики.
Статические и пользовательские файлы
Django различает два вида файлов.
Static files - CSS, JavaScript, изображения интерфейса
Media files - файлы, загруженные пользователями
Статические файлы
Они принадлежат коду приложения.
Примеры:
- стили;
- клиентский JavaScript;
- логотип;
- иконки.
При развёртывании они собираются отдельной командой.
python manage.py collectstatic
Media
Это пользовательские данные:
- аватары;
- документы;
- фотографии;
- вложения.
Они требуют отдельного хранилища, правил доступа и резервного копирования.
Безопасность
Django предоставляет встроенные механизмы защиты, но безопасность зависит и от кода проекта.
Основные направления:
XSS - экранирование вывода
CSRF - токены для изменяющих запросов
SQL injection - параметризованные запросы ORM
Clickjacking - защитные заголовки
Host header attacks - ALLOWED_HOSTS
Session security - безопасные настройки cookie
Authentication - проверка пользователя
Authorization - проверка разрешений
ORM и SQL injection
ORM параметризует значения в обычных запросах.
Order.objects.filter(
number=user_input,
)
Но небезопасный raw SQL остаётся опасным.
Нельзя собирать запрос конкатенацией строк:
query = f"SELECT * FROM orders WHERE number = '{value}'"
Аутентификация не равна авторизации
Аутентификация - кто пользователь
Авторизация - что ему разрешено
Проверка is_authenticated не означает, что пользователь может видеть любой заказ.
WSGI и ASGI
Django-приложение запускается через серверный интерфейс.
WSGI - традиционный синхронный интерфейс Python-веб-приложений
ASGI - интерфейс с поддержкой асинхронного выполнения
Файлы проекта:
wsgi.py - точка входа WSGI
asgi.py - точка входа ASGI
Команда:
python manage.py runserver
запускает локальный сервер Django.
Синхронный и асинхронный код
Django поддерживает асинхронные представления.
from django.http import JsonResponse
async def status_view(request):
return JsonResponse({
"status": "ok",
})
async полезен, когда обработчик долго ожидает внешние операции, совместимые с асинхронным выполнением.
Но добавление async не ускоряет автоматически:
- тяжёлые вычисления;
- синхронную библиотеку;
- блокирующий запрос;
- плохо спроектированную базу.
I/O ожидание - возможная польза async
CPU-нагрузка - отдельные процессы или другие механизмы
Тестирование
Django предоставляет тестовый клиент, базовые классы и временную тестовую базу.
Пример:
from django.test import TestCase
from django.urls import reverse
class OrderListTests(TestCase):
def test_page_is_available(self):
response = self.client.get(
reverse("orders:list")
)
self.assertEqual(
response.status_code,
200,
)
Тесты могут проверять:
- маршруты;
- представления;
- формы;
- модели;
- права;
- шаблоны;
- запросы;
- транзакции;
- административный интерфейс.
Что тестировать отдельно
Модель - правила данных
Сервис - бизнес-сценарий
View - HTTP-контракт
Форма - ввод и ошибки
Интеграция - взаимодействие компонентов
Не каждый тест должен проходить через полный HTTP-цикл.
Где размещать бизнес-логику
Небольшой обработчик может содержать логику прямо во view.
Но по мере роста проекта представления часто становятся перегруженными.
Плохой сценарий:
View - валидация - несколько запросов - расчёты - внешний API - отправка письма - смена статусов - формирование ответа
Представление лучше оставить координатором HTTP-уровня.
View - принять запрос и вернуть ответ
Form - проверить ввод
Service - выполнить бизнес-операцию
Model - представить данные и локальное поведение
Selector - получить данные
Пример сервиса:
from django.db import transaction
@transaction.atomic
def cancel_order(*, order, user):
if not order.can_be_cancelled_by(user):
raise OrderCannotBeCancelled
order.status = "cancelled"
order.save(update_fields=["status"])
View вызывает операцию и преобразует результат в HTTP-ответ.
Это не обязательный встроенный паттерн Django, а способ сохранить понятные границы в крупном приложении.
Django и API
Django способен возвращать JSON без дополнительного фреймворка.
from django.http import JsonResponse
def order_detail(request, order_id):
return JsonResponse({
"id": order_id,
})
Для большого API обычно требуются дополнительные механизмы:
- сериализация;
- валидация JSON;
- аутентификация API;
- разрешения;
- пагинация;
- фильтрация;
- документация;
- единый формат ошибок.
Их можно реализовать самостоятельно или использовать отдельный API-фреймворк поверх Django.
Django - общий веб-фреймворк
API-слой - способ построения HTTP-интерфейса приложения
Когда Django подходит
Django хорошо подходит, когда проекту нужны несколько связанных возможностей:
- реляционная база данных;
- административный интерфейс;
- пользователи и права;
- серверный HTML;
- формы;
- сложная предметная область;
- миграции;
- много приложений;
- внутренние панели;
- API рядом с веб-интерфейсом.
Примеры:
- CRM;
- интернет-магазин;
- образовательная платформа;
- внутренний портал;
- сервис бронирования;
- система учёта;
- контентный сайт;
- кабинет клиента;
- панель управления.
Когда Django может быть избыточен
Django не обязан быть лучшим выбором для каждого сервиса.
Он может оказаться избыточным, если:
- нужен один небольшой endpoint;
- нет базы данных;
- нет пользователей и административного интерфейса;
- приложение выполняет одну узкую функцию;
- нужен минимальный асинхронный сервис;
- команда не планирует использовать встроенные компоненты.
Но количество файлов само по себе не делает фреймворк тяжёлым. Важно, какую часть инфраструктуры он заменяет.
Типичные ошибки начинающих
Путать проект и приложение
Проект содержит общую конфигурацию, приложение реализует отдельную функциональную область.
Писать всю логику во views.py
Представление быстро становится трудным для тестирования и повторного использования.
Считать ORM заменой знания SQL
Без понимания JOIN, индексов, транзакций и ограничений легко создать медленные или некорректные запросы.
Игнорировать миграции
Изменение модели не изменяет базу автоматически без применения миграции.
Делать запросы в цикле
Это приводит к N+1 и большому числу обращений к базе.
Использовать сигналы для всего
Поток выполнения становится скрытым.
Доверять только проверкам формы
Ограничения целостности должны существовать и в базе.
Использовать admin как публичный интерфейс
Админ-панель предназначена прежде всего для доверенных внутренних пользователей.
Хранить секреты в settings.py
Секретные данные не должны попадать в репозиторий.
Как изучать Django последовательно
Порядок изучения важен, потому что компоненты связаны между собой.
HTTP-запрос и ответ - проект и приложение - URL dispatcher - view - template - model - ORM - миграции - forms - admin - authentication - middleware - тесты
Первый этап
Сделать страницу без базы:
- создать проект;
- создать приложение;
- настроить URL;
- написать view;
- вернуть HTML.
Второй этап
Добавить данные:
- создать модель;
- выполнить миграции;
- добавить записи;
- получить QuerySet;
- вывести данные в шаблоне.
Третий этап
Добавить изменение данных:
- создать форму;
- проверить ввод;
- сохранить объект;
- обработать ошибки;
- настроить CSRF.
Четвёртый этап
Добавить пользователей:
- вход;
- выход;
- ограничение доступа;
- права на объекты.
Пятый этап
Разобрать архитектуру:
- транзакции;
- оптимизацию запросов;
- сервисный слой;
- тесты;
- middleware;
- журналирование;
- настройки окружения.
Полный пример движения запроса
Пусть пользователь открывает:
GET /orders/42/
1. Веб-сервер передаёт запрос Django
Django создаёт HttpRequest.
2. Запрос проходит входящую часть middleware
Могут быть добавлены:
- сессия;
- пользователь;
- языковые настройки;
- защитные проверки.
3. URL dispatcher разбирает путь
Маршрут:
path(
"<int:order_id>/",
views.order_detail,
name="detail",
)
извлекает:
order_id - 42
4. Вызывается представление
def order_detail(request, order_id):
...
5. Представление получает объект
order = get_object_or_404(
Order.objects.select_related("user"),
pk=order_id,
)
ORM формирует SQL, база возвращает данные, Django создаёт объект модели.
6. Проверяются права
Приложение определяет, может ли текущий пользователь видеть заказ.
7. Создаётся HTML
return render(
request,
"orders/detail.html",
{
"order": order,
},
)
Шаблон получает контекст и формирует текст страницы.
8. Создаётся HttpResponse
Django задаёт тело, статус и заголовки.
9. Ответ проходит обратную часть middleware
Могут быть добавлены защитные заголовки, cookie и служебная информация.
10. Клиент получает ответ
HTTP/1.1 200 OK
Content-Type: text/html
Вся цепочка:
Клиент - HttpRequest - middleware - URL - view - ORM - PostgreSQL - model - template - HttpResponse - middleware - клиент
Итог
Django - это не только способ связать URL с Python-функцией.
Он организует серверное приложение как систему взаимосвязанных компонентов.
Project - общая конфигурация
App - функциональная область
URL dispatcher - выбор обработчика
View - обработка HTTP-запроса
Model - представление данных
ORM - построение запросов к базе
Migration - изменение схемы
Template - формирование HTML
Form - ввод и валидация
Admin - внутреннее управление данными
Authentication - определение пользователя
Authorization - проверка прав
Middleware - сквозная обработка запросов
Основной путь запроса:
Запрос - middleware - URL - view - данные - template или JSON - response
Django даёт готовую инфраструктуру, но качество приложения по-прежнему зависит от проектирования:
- сущностей и связей;
- ограничений базы;
- бизнес-операций;
- границ приложений;
- прав доступа;
- запросов ORM;
- обработки ошибок;
- тестов;
- настроек рабочей среды.
Хорошее Django-приложение строится не вокруг количества встроенных возможностей, а вокруг понятного распределения ответственности между ними.