Django URL Routing¶
Коли HTTP-запит досягає вашого бекенду, Django не просто викликає випадкову функцію. Він використовує URL Dispatcher (диспетчер маршрутів) — сувору систему правил для визначення того, який саме код Python повинен відреагувати на цей конкретний запит.
Ось архітектурний розбір того, як працює ця система.
1. Основний механізм та потік виконання (Execution Flow)¶
Процес маршрутизації відбувається за чітким алгоритмом:
-
Ініціалізація: ROOT_URLCONF. Django зчитує налаштування
ROOT_URLCONF, щоб знайти головний файл маршрутів (зазвичайurls.pyна рівні проєкту). -
Завантаження патернів: urlpatterns. Він шукає змінну
urlpatterns— звичайний список (sequence) об'єктівpath()абоre_path(). -
Пошук зверху вниз: Top-down matching. Django читає цей список суворо зверху вниз і порівнює запитаний URL з кожним шаблоном. Він зупиняється на першому ж збігу.
-
Виклик View: HttpRequest. Як тільки знайдено збіг, Django імпортує відповідну функцію представлення (View), передає їй об'єкт
HttpRequestта будь-які змінні, "захоплені" з URL. -
Помилка 404: Resolver404. Якщо жоден маршрут не підійшов, генерується виняток
Resolver404і викликається обробник помилок (404 Page Not Found).
Ментальна модель: Уявіть листоношу, який йде довгою вулицею (ваш список
urlpatterns) і порівнює адресу на конверті з номерами будинків. Як тільки він бачить будинок, номер якого відповідає адресі на листі, він кидає туди лист (виклик View) і припиняє пошук. Він не йде до кінця вулиці, щоб перевірити, чи є там "кращий" будинок.
2. Архітектура модульного бекенду та include()¶
Для масштабування бекенду Django використовує функцію include(). Вона дозволяє розбити єдиний гігантський список маршрутів на ізольовані модулі для кожного додатку (app).
Коли Django зустрічає include(), він відрізає ту частину URL-адреси, яка вже збіглася, і передає залишок рядка у вкладений файл urls.py для подальшого пошуку.
Архітектурне значення: Це робить додатки "plug-and-play" (незалежними модулями). Додаток polls взагалі не знає, чи знаходиться він за адресою /polls/, чи /api/v1/polls/. Він обробляє лише свій відрізок шляху.
Приклад головного urls.py (Рівень проєкту)¶
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
# Усі запити, що починаються з /users/, передаються в додаток users_app
path('users/', include('users_app.urls')),
# Версіонування API
path('api/v1/', include('api_v1.urls')),
]
3. Два типи динамічних параметрів¶
Django дозволяє передавати дані у View двома різними способами: через структуру самого шляху (Path Converters) та через параметри запиту (Query Strings).
Тип А: Вилучення параметрів зі шляху (Path Converters)¶
Динамічні URL-адреси використовують конвертери шляхів, щоб автоматично перетворювати частини тексту URL у змінні Python. Коли диспетчер знаходить збіг (наприклад <int:id>), він "захоплює" це значення, перетворює на вказаний тип і передає у функцію.
<int:id>: захоплює ціле число і передає його у View якid=5.<slug:post_slug>: захоплює текст (літери, цифри, дефіси).<str:username>: захоплює будь-який рядок до наступного слеша/.
urls.py (Рівень додатку):
from django.urls import path
from . import views
urlpatterns = [
# Збігається з /users/42/
path('<int:id>/', views.user_profile, name='profile'),
# Збігається з /users/johndoe-99/
path('<slug:username_slug>/', views.user_by_slug, name='profile_slug'),
]
views.py:
from django.shortcuts import render, get_object_or_404
from .models import User
def user_profile(request, id):
# Django непомітно виконав: user_profile(request, id=42)
user = get_object_or_404(User, pk=id)
return render(request, 'profile.html', {'user': user})
Тип Б: GET-параметри (Query Strings)¶
На відміну від конвертерів, диспетчер маршрутів повністю ігнорує все, що йде в адресі після знака питання ?. Маршрутизатор розглядає /users/ та /users/?id=42&sort=asc як один і той самий маршрут.
Ці дані автоматично збираються у словник request.GET. Їх зручно використовувати для фільтрації, сортування або пагінації. Це безпечний підхід: якщо параметрів немає, ви просто використовуєте значення за замовчуванням за допомогою методу .get().
views.py:
def users_list(request):
# Шукаємо параметр 'id' (наприклад, ?id=42). Поверне None, якщо його немає.
user_id = request.GET.get('id')
# Шукаємо 'sort'. Якщо його немає, ставимо 'asc' за замовчуванням
sort_by = request.GET.get('sort', 'asc')
# Логіка обробки...
return render(request, 'users_list.html', {'sort_by': sort_by})
4. Поширені помилки (Misconceptions) та Edge Cases¶
- Пастка "Найкращого збігу": Студенти часто думають, що Django шукає найбільш точний збіг. Це не так, він бере перший. Якщо ви поставите маршрут
<str:name>/вище заabout/, запит до/about/буде перехоплено першим маршрутом, і View отримає рядок"about"у зміннуname. Завжди ставте статичні маршрути вище динамічних. - Інтуїція для дебагінгу: Коли ви бачите помилку
404в режимі розробки (DEBUG = True), Django показує вам точний список регулярних виразів та маршрутів, які він перевірив, саме в тому порядку, в якому він це робив. Це ваш головний інструмент для розуміння того, де система "звернула не туди".
Ось навчальний матеріал, який пояснює архітектурну різницю між обробкою GET та POST запитів, а також демонструє, як безпечно передавати дані від користувача до бази даних.
Обробка даних користувача: GET-фільтрація та POST-форми¶
В архітектурі вебдодатків методи GET та POST виконують фундаментально різні ролі. GET використовується виключно для читання та фільтрації даних (не змінює стан сервера). POST використовується для запису або модифікації даних (змінює стан сервера).
Частина 1: Фільтрація бази даних через GET-параметри¶
Коли користувач використовує фільтри на сайті (наприклад, обирає категорію або сортування), URL виглядає так: /books/?category=fiction&sort=-price. Django збирає ці параметри у словник request.GET.
Ваше завдання — витягти ці параметри та безпечно передати їх у методи ORM (.filter(), .order_by()).
Архітектурний патерн: Ланцюгові запити (Chaining)¶
QuerySet у Django є "лінивим" (lazy). Це означає, що ви можете послідовно накладати фільтри один за одним, і Django не буде звертатися до бази даних, доки ви не передасте фінальний результат у шаблон.
Приклад коду: Динамічна фільтрація та сортування¶
from django.shortcuts import render
from .models import Book
def book_list(request):
# 1. Починаємо з базового запиту (БД ще не викликається)
queryset = Book.objects.all()
# 2. Витягуємо GET-параметри
# Якщо параметра немає, поверне None
category_query = request.GET.get('category')
# Якщо параметра немає, за замовчуванням сортуємо за заголовком
sort_query = request.GET.get('sort', 'title')
# 3. Застосовуємо фільтрацію, якщо параметр існує
if category_query:
# Транслюється в SQL: WHERE category = 'fiction'
queryset = queryset.filter(category=category_query)
# 4. Безпечне сортування
# ВАЖЛИВО: Завжди перевіряйте дозволені поля сортування!
# Якщо користувач введе ?sort=invalid_column, БД видасть помилку 500.
allowed_sort_fields = ['title', '-title', 'price', '-price']
if sort_query in allowed_sort_fields:
# Транслюється в SQL: ORDER BY price DESC
queryset = queryset.order_by(sort_query)
# 5. Передаємо готовий QuerySet у шаблон (тут і відбувається SQL-запит)
return render(request, 'books/list.html', {'books': queryset})
Ментальна модель:
GET-запит — це як розмова з бібліотекарем. Ви просите: "Покажіть мені книги з фантастики, відсортовані за ціною" (?category=fiction&sort=price). Бібліотекар (View) застосовує ці фільтри до каталогу (QuerySet) і видає вам список. Стан бібліотеки при цьому не змінюється.
Частина 2: Обробка POST-запитів з HTML-форм¶
Коли користувач заповнює форму (наприклад, реєстрація або додавання товару), дані відправляються методом POST. Ці дані потрапляють у словник request.POST.
Життєвий цикл обробки форми (Патерн PRG)¶
Правильна обробка POST-запиту вимагає дотримання патерну PRG (Post/Redirect/Get). Ви ніколи не повинні повертати HTML-шаблон безпосередньо після успішного POST-запиту, оскільки оновлення сторінки в браузері (F5) призведе до повторної відправки форми (подвійне списання коштів або дублювання запису).
-
GET-запит (Відображення форми): Користувач переходить на сторінку. Сервер бачить, що метод
GET, і повертає порожню HTML-форму. -
Відправка даних: Користувач заповнює поля та натискає "Відправити". Браузер формує
POST-запит. -
POST-запит (Обробка логіки): Сервер отримує
request.POST, витягує дані, перевіряє їх на помилки та зберігає в базу даних. -
Redirect (Перенаправлення): Замість рендерингу шаблону, сервер повертає статус
302 Redirect. Це змушує браузер зробити новий, чистийGET-запит на іншу сторінку (наприклад, на сторінку успіху).
Приклад коду: Обробка "сирих" POST-даних¶
HTML Форма (зверніть увагу на обов'язковий {% csrf_token %}, який захищає від підробки міжсайтових запитів):
<!-- create_book.html -->
<form method="POST" action="{% url 'create_book' %}">
{% csrf_token %}
<input type="text" name="title" placeholder="Назва книги">
<input type="number" name="price" placeholder="Ціна">
<button type="submit">Зберегти</button>
</form>
Обробник у views.py:
from django.shortcuts import render, redirect
from .models import Book
def create_book(request):
# Якщо запит прийшов методом POST (форма відправлена)
if request.method == 'POST':
# 1. Витягуємо дані за атрибутом 'name' з HTML-форми
book_title = request.POST.get('title')
book_price = request.POST.get('price')
# 2. Сира валідація (чи не порожні поля)
if book_title and book_price:
# 3. Зберігаємо в базу даних
Book.objects.create(title=book_title, price=book_price)
# 4. Обов'язковий Redirect (захист від подвійної відправки)
return redirect('book_list')
else:
# Якщо є помилки — можна повернути форму з повідомленням
error_message = "Будь ласка, заповніть усі поля."
return render(request, 'create_book.html', {'error': error_message})
# Якщо метод GET — просто показуємо порожню форму
return render(request, 'create_book.html')
Чому "сирий" request.POST використовують рідко?¶
Витягувати дані через request.POST.get() і писати перевірки вручну (чи є ціна числом, чи не занадто довга назва, чи унікальний email) — це дуже довго і призводить до помилок.
В реальній інженерії для цього використовують систему Django Forms або ModelForms. Вони автоматично генерують HTML, самостійно перевіряють типи даних і гарантують безпеку.
Обробка "сирих" даних через request.POST.get() швидко стає некерованою, коли форма має 10 полів, чекбокси, дати та файли. Вам доводиться вручну перевіряти, чи є рядок числом, чи не перевищує він ліміт символів, і що робити, якщо користувач ввів шкідливий код.
Django Forms (а конкретно ModelForm) — це механізм, який бере вашу модель бази даних і автоматично створює для неї систему валідації та генерації HTML.
1. Ментальна модель: "Розумний секретар"¶
Уявіть, що ModelForm — це розумний секретар у приймальні.
Коли клієнт приходить вперше (GET-запит), секретар видає йому порожній бланк. Коли клієнт повертає заповнений бланк (POST-запит), секретар бере ручку, перевіряє кожен рядок на відповідність правилам (чи тут дійсно дата? чи не закороткий пароль?), і лише якщо все ідеально, передає документ директору (базі даних). Якщо є помилки — секретар повертає бланк клієнту, червоним маркером підкресливши, що саме треба виправити.
2. Крок за кроком: Відмовляємося від ручної обробки¶
Крок 1: Створення файлу forms.py¶
Замість того, щоб описувати правила у views.py, ви створюєте окремий файл, де пов'язуєте форму з вашою моделлю.
# forms.py
from django import forms
from .models import Book
class BookForm(forms.ModelForm):
class Meta:
model = Book
fields = ['title', 'price', 'category'] # Вказуємо, які поля дозволено редагувати
Архітектурна перевага: Форма автоматично прочитає вашу модель Book. Вона знатиме, що title — це текст (з обмеженням довжини), а price — це число, і згенерує відповідні перевірки та HTML-теги (<input type="text">, <input type="number">).
Крок 2: Оптимізація views.py¶
Тепер ми прибираємо весь ручний код розбору словника request.POST.
# views.py
from django.shortcuts import render, redirect
from .forms import BookForm
def create_book(request):
if request.method == 'POST':
# Передаємо всі дані від користувача у форму (Bound Form)
form = BookForm(request.POST)
# Магія відбувається тут: автоматична перевірка всіх правил
if form.is_valid():
# Якщо дані чисті та правильні — зберігаємо їх у базу
form.save()
return redirect('book_list')
else:
# GET-запит: створюємо порожню форму (Unbound Form)
form = BookForm()
# Якщо форма невалідна (є помилки), або це GET-запит —
# ми просто передаємо об'єкт form у шаблон.
return render(request, 'create_book.html', {'form': form})
Крок 3: Спрощення HTML-шаблону¶
Раніше вам доводилося писати кожен <input> вручну. Тепер Django сам рендерить форму.
<!-- create_book.html -->
<form method="POST">
{% csrf_token %}
<!-- Виводить усі поля, ярлики (labels) та повідомлення про помилки -->
{{ form.as_p }}
<button type="submit">Зберегти книгу</button>
</form>
Якщо користувач введе текст у поле ціни, form.is_valid() поверне False, і Django автоматично відрендерить сторінку знову, додавши червоне повідомлення "Введіть дійсне число" поруч із полем ціни. Вам не потрібно писати для цього жодного рядка логіки.
3. Що відбувається під капотом .is_valid()?¶
Виклик form.is_valid() запускає глибокий конвеєр очищення даних:
- To Python (Приведення типів): Всі дані з вебу приходять як текст (рядки). Django намагається перетворити рядок
"15.99"на Python-об'єктDecimal, а"2026-05-19"— на об'єктdatetime. - Перевірка обмежень моделі: Перевіряється
max_length,unique=True, та чи не є порожнім поле, де стоїтьnull=False. - Створення
cleaned_data: Якщо всі перевірки пройдені, Django створює захищений словникform.cleaned_data. Після викликуis_valid()ви ніколи не повинні працювати з "сирим"request.POST, звертайтеся лише доcleaned_data. - Виклик
save(): Метод.save()бере безпечний словникcleaned_dataі генеруєINSERTзапит до бази даних.
5. Завдання на передбачення (Prediction Exercise)¶
Проаналізуйте наступний код диспетчера маршрутів:
# urls.py
urlpatterns = [
path('api/users/', views.all_users), # 1
path('api/<str:username>/', views.user_profile), # 2
path('api/users/active/', views.active_users), # 3
]
4. Максимальна оптимізація: Class-Based Views (CreateView)¶
Ми вже побачили, як ModelForm бере на себе валідацію та генерацію HTML. Але у views.py все ще залишається шаблонний код: перевірка if request.method == 'POST', виклик form.is_valid(), збереження та перенаправлення.
У Django є вбудований інструмент CreateView, який інкапсулює весь цей життєвий цикл (GET, POST, валідацію, збереження і редирект) у кілька рядків коду.
Приклад коду: Перехід на класи¶
Замість функції create_book, ми створюємо клас:
# views.py
from django.urls import reverse_lazy
from django.views.generic.edit import CreateView
from .models import Book
from .forms import BookForm
class BookCreateView(CreateView):
model = Book
form_class = BookForm
template_name = 'create_book.html'
# Куди перенаправити користувача після успішного збереження форми
success_url = reverse_lazy('book_list')
Як це працює під капотом?¶
Клас CreateView містить усю стандартну логіку, яку ви раніше писали вручну. Коли надходить запит, цей клас виконує такий алгоритм:
- Якщо це GET-запит: Клас автоматично створює порожній екземпляр
BookFormі передає його в шаблонcreate_book.htmlяк зміннуform. - Якщо це POST-запит: Клас автоматично зв'язує дані з
request.POSTіз формоюBookForm. - Валідація: Викликається
.is_valid(). - Якщо є помилки, клас автоматично повертає HTML-сторінку з підсвіченими помилками (не потрібно писати блок
else). - Якщо дані чисті, клас викликає
form.save(), записує дані в базу і виконує HTTP 302 Redirect на адресу, вказану вsuccess_url.
Підключення в urls.py¶
Щоб диспетчер маршрутів зрозумів цей клас, його потрібно викликати за допомогою методу .as_view():
# urls.py
from django.urls import path
from .views import BookCreateView, book_list
urlpatterns = [
path('books/', book_list, name='book_list'),
path('books/new/', BookCreateView.as_view(), name='create_book'),
]
Архітектурний підсумок: Використання CBV (
CreateView,UpdateView,DeleteView) дозволяє звести стандартні операції CRUD до конфігурації замість написання імперативного коду. Це мінімізує кількість помилок і робить архітектуру бекенду значно чистішою.
Запитання для роздумів: Що відбудеться, якщо користувач надішле запит на /api/users/active/? Який View виконається і чому?
Відповідь: Виконається маршрут №2 (
user_profile), оскільки він стоїть вище і збігається з рядкомusers/active/, сприймаючиusersяк статичну частину, аactive— як<str:username>. Маршрут №3 ніколи не буде досягнутий. Це класична архітектурна помилка маршрутизації зверху вниз. Маршрут №3 необхідно перемістити вище за маршрут №2.
notes_chat_app — реальні URL patterns¶
Головний router (notes_project/urls.py)¶
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('accounts/', include('django.contrib.auth.urls')), # login, logout, password
path('', include('notes_app.urls', namespace='notes_app')),
]
App router (notes_app/urls.py)¶
app_name = 'notes_app' # ← namespace для reverse()
urlpatterns = [
path('', views.home, name='home'),
# Notes CRUD
path('notes/', views.note_list, name='note_list'),
path('notes/new/', views.note_create, name='note_create'), # ← ПЕРЕД <int:pk>!
path('notes/<int:pk>/', views.note_detail, name='note_detail'),
path('notes/<int:pk>/edit/', views.note_update, name='note_update'),
path('notes/<int:pk>/delete/', views.note_delete, name='note_delete'),
# Notebooks, Tags
path('notebooks/', views.notebook_list, name='notebook_list'),
path('tags/', views.tag_list, name='tag_list'),
# Group chat
path('groups/<int:pk>/chat/', views.group_chat, name='group_chat'),
# Todos, Shopping
path('todos/', views.todo_list, name='todo_list'),
path('shopping/', views.shopping_list_view, name='shopping_list'),
]
Named URLs — reverse() і {% url %}¶
# В views.py — redirect по імені URL
return redirect('notes_app:note_detail', pk=note.pk)
# reverse() — отримати рядок URL
from django.urls import reverse
url = reverse('notes_app:note_detail', kwargs={'pk': 42})
# url == '/notes/42/'
<!-- В шаблоні -->
<a href="{% url 'notes_app:note_detail' pk=note.pk %}">{{ note.title }}</a>
<a href="{% url 'notes_app:note_create' %}">Нова нотатка</a>
Debug — resolve() і show_urls¶
from django.urls import resolve
result = resolve('/notes/42/')
result.url_name # 'note_detail'
result.namespace # 'notes_app'
result.kwargs # {'pk': 42}
result.func # <function note_detail at 0x...>
У книзі¶
- Request Lifecycle — повний шлях запиту крізь Django
- Views — FBV, CBV, декоратори, антипатерни
- Крок 1. Hello Django — практика URL routing
Офіційна документація¶
- Django: URL dispatcher — path(), re_path(), include()
- Django: Reverse resolution of URLs — reverse(), {% url %}
- Django: URL converters — int, str, slug, uuid, path