# Django 5.2: Власна Middleware та Обробка Сигналів для Технічних Співбесід > Повний посібник з Django 5.2: побудова власної middleware, використання сигналів post_save та pre_save, асинхронна middleware, користувацькі сигнали та типові питання для технічних співбесід з прикладами коду. - Published: 2026-04-29 - Updated: 2026-04-29 - Author: SharpSkill - Tags: django, python, middleware, signals, interview - Reading time: 8 min --- Django 5.2 LTS надає розробникам зрілий та перевірений набір інструментів для побудови веб-застосунків промислового рівня. Серед ключових архітектурних компонентів фреймворку два механізми заслуговують на особливу увагу при підготовці до технічних співбесід: middleware та сигнали. Middleware контролює потік HTTP-запитів та відповідей, тоді як сигнали забезпечують слабкозв'язану комунікацію між компонентами застосунку. Глибоке розуміння обох механізмів є обов'язковою компетенцією для Django-розробників будь-якого рівня. > **Коротка довідка для співбесіди** > > Middleware обробляє кожен HTTP-запит до та після представлення (view). Сигнали реагують на внутрішні події моделей (збереження, видалення). Middleware працює на рівні HTTP, сигнали — на рівні ORM. Обидва механізми підтримують розширення без зміни базового коду. ## Як працює middleware у Django Middleware у Django реалізується як клас із двома обов'язковими методами: `__init__` та `__call__`. Конструктор викликається один раз при запуску сервера та отримує посилання на наступний обробник у ланцюжку. Метод `__call__` викликається при кожному запиті та відповідає за передачу управління далі по ланцюжку. Ланцюжок middleware працює за принципом вкладеності. Кожна middleware огортає наступну, формуючи структуру, подібну до матрьошки. Код перед викликом `get_response` виконується на шляху запиту до представлення, а код після виклику — на шляху відповіді назад до клієнта. Ця модель забезпечує повний контроль над обома фазами обробки HTTP-запиту. ```python # middleware.py - Basic middleware structure class SimpleMiddleware: def __init__(self, get_response): self.get_response = get_response # One-time configuration happens here at server start def __call__(self, request): # Code executed BEFORE the view (and later middleware) response = self.get_response(request) # Code executed AFTER the view (on the way back) return response ``` Метод `__init__` приймає `get_response` — виклик наступної middleware або самого представлення, якщо це остання ланка ланцюжка. Ініціалізація відбувається лише один раз при старті сервера, тому тут доцільно розміщувати конфігурацію, ініціалізацію логерів чи підключення до зовнішніх сервісів. ## Побудова middleware для логування запитів Практичне застосування middleware демонструє клас для структурованого логування HTTP-запитів. Кожен запит отримує мітку часу на вході, а на виході фіксується тривалість обробки, HTTP-метод, шлях, статус відповіді та IP-адреса клієнта. Особливу увагу варто звернути на метод `process_exception`. Django викликає його автоматично, коли представлення генерує необроблений виняток. Цей хук дозволяє зафіксувати помилку з повним контекстом запиту до того, як стандартний обробник Django перетворить виняток на HTTP-відповідь 500. ```python # apps/core/middleware.py import logging import time logger = logging.getLogger('django.request') class RequestLoggingMiddleware: def __init__(self, get_response): self.get_response = get_response def __call__(self, request): start_time = time.monotonic() # Attach metadata before view processing request.start_time = start_time response = self.get_response(request) duration = time.monotonic() - start_time logger.info( 'method=%s path=%s status=%d duration=%.3fs ip=%s', request.method, request.get_full_path(), response.status_code, duration, request.META.get('REMOTE_ADDR'), ) return response def process_exception(self, request, exception): # Called only when the view raises an exception duration = time.monotonic() - getattr(request, 'start_time', 0) logger.error( 'method=%s path=%s exception=%s duration=%.3fs', request.method, request.get_full_path(), str(exception), duration, ) return None # Let Django's default exception handling continue ``` Використання `time.monotonic()` замість `time.time()` є принциповим рішенням. Монотонний годинник не залежить від корекцій системного часу (NTP-синхронізація, переведення годинників), що гарантує коректне вимірювання інтервалів навіть на серверах із нестабільним системним часом. ## Порядок middleware та налаштування MIDDLEWARE Порядок middleware у списку `MIDDLEWARE` визначає послідовність обробки. Для запитів middleware виконуються зверху вниз, для відповідей — знизу вгору. Це означає, що middleware, оголошена першою, є зовнішнім шаром матрьошки: вона першою отримує запит і останньою обробляє відповідь. Власну middleware рекомендовано розміщувати після стандартного стеку Django. `SecurityMiddleware` повинна залишатися першою для забезпечення HTTPS-перенаправлень та заголовків безпеки. `AuthenticationMiddleware` має передувати будь-якій middleware, що звертається до `request.user`. ```python # settings.py - Middleware ordering matters 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', # Custom middleware placed after Django's core stack 'apps.core.middleware.RequestLoggingMiddleware', ] ``` Порушення порядку є поширеною причиною помилок на продакшні. Наприклад, розміщення `CsrfViewMiddleware` перед `SessionMiddleware` призведе до некоректної роботи CSRF-захисту, оскільки CSRF-токен зберігається у сесії. На технічних співбесідах кандидатів часто просять пояснити наслідки зміни порядку конкретних middleware. ## Асинхронна middleware у Django 5.2 Django 5.2 повноцінно підтримує асинхронну middleware через атрибути `async_capable` та `sync_capable`. Позначення middleware як виключно асинхронної (`sync_capable = False`) гарантує, що Django використовуватиме асинхронний контекст виконання, що усуває накладні витрати на перемикання між синхронним та асинхронним режимами. Асинхронна middleware є особливо ефективною для операцій, що включають введення-виведення: запити до зовнішніх API, перевірка кешу, взаємодія з Redis. У синхронному режимі такі операції блокують робочий потік, тоді як у асинхронному — потік повертається в пул для обробки інших запитів. ```python # apps/core/middleware.py import asyncio import time class AsyncTimingMiddleware: # Mark this middleware as async-capable async_capable = True sync_capable = False def __init__(self, get_response): self.get_response = get_response async def __call__(self, request): start = time.monotonic() # get_response is awaitable in async context response = await self.get_response(request) duration = time.monotonic() - start response['X-Request-Duration'] = f'{duration:.4f}s' return response ``` Атрибут `async_capable = True` повідомляє Django, що middleware підтримує асинхронний виклик. `sync_capable = False` вказує, що middleware не може працювати у синхронному контексті. При такій конфігурації Django гарантовано виконуватиме `__call__` як корутину, і `get_response` повертатиме awaitable об'єкт. ## Сигнали Django: патерн publish-subscribe Сигнали реалізують патерн publish-subscribe (видавець-підписник) у Django ORM. Коли модель зберігається, видаляється або ініціалізується, Django генерує відповідний сигнал, на який можуть підписатися довільні функції-обробники. Це дозволяє реагувати на події моделей без зміни коду самих моделей. Вбудовані сигнали Django включають `pre_save` (перед збереженням), `post_save` (після збереження), `pre_delete` (перед видаленням), `post_delete` (після видалення) та `m2m_changed` (зміна зв'язків many-to-many). Кожен сигнал передає набір аргументів, специфічний для типу події: `sender`, `instance`, `created` (для `post_save`), `using` (база даних). Ключова перевага сигналів полягає у розділенні відповідальності. Модель `User` не повинна знати про створення профілю, надсилання вітального листа чи ініціалізацію налаштувань. Кожна з цих дій оформлюється як окремий обробник сигналу у відповідному модулі застосунку. ## post_save для автоматичного створення профілю Сигнал `post_save` є одним з найчастіше використовуваних у Django-проєктах. Типовий приклад — автоматичне створення пов'язаного об'єкта при збереженні нового запису. Аргумент `created` дозволяє розрізнити операцію INSERT (створення нового запису) від UPDATE (оновлення існуючого). Декоратор `@receiver` реєструє функцію як обробник конкретного сигналу від конкретного відправника. Параметр `sender=User` обмежує спрацювання лише збереженнями моделі `User`, ігноруючи інші моделі. ```python # apps/accounts/signals.py from django.db.models.signals import post_save from django.dispatch import receiver from django.contrib.auth import get_user_model from apps.accounts.models import UserProfile User = get_user_model() @receiver(post_save, sender=User) def create_user_profile(sender, instance, created, **kwargs): # 'created' is True only on INSERT, False on UPDATE if created: UserProfile.objects.create( user=instance, display_name=instance.get_full_name() or instance.username, ) ``` Важливо завжди приймати `**kwargs` у обробниках сигналів. Django може розширити набір аргументів у майбутніх версіях, і відсутність `**kwargs` призведе до помилки `TypeError` при оновленні фреймворку. Ця деталь часто перевіряється на технічних співбесідах як індикатор досвіду роботи з Django у продакшні. ## pre_save для валідації та трансформації даних Сигнал `pre_save` спрацьовує безпосередньо перед записом у базу даних, що робить його ідеальним для автоматичної трансформації даних: генерація слагів, нормалізація полів, обчислення похідних значень. У наведеному прикладі обробник автоматично генерує URL-сумісний слаг з назви статті, якщо слаг не було встановлено вручну. Цикл `while` забезпечує унікальність слагу, додаючи числовий суфікс при виявленні конфлікту. Виключення поточного об'єкта через `exclude(pk=instance.pk)` запобігає хибному спрацюванню при оновленні існуючого запису. ```python # apps/blog/signals.py from django.db.models.signals import pre_save from django.dispatch import receiver from django.utils.text import slugify from apps.blog.models import Article @receiver(pre_save, sender=Article) def auto_generate_slug(sender, instance, **kwargs): if not instance.slug: base_slug = slugify(instance.title) slug = base_slug counter = 1 # Ensure slug uniqueness while Article.objects.filter(slug=slug).exclude(pk=instance.pk).exists(): slug = f'{base_slug}-{counter}' counter += 1 instance.slug = slug ``` Модифікація `instance` у `pre_save` автоматично відображається у збереженому записі, оскільки Django використовує той самий об'єкт для запису у базу. Немає потреби викликати `instance.save()` — це спричинило б рекурсію, оскільки `save()` знову ініціює `pre_save`. ## Реєстрація сигналів у AppConfig.ready() Обробники сигналів повинні бути імпортовані для реєстрації у диспетчері Django. Рекомендований підхід — імпорт модуля сигналів у методі `ready()` класу `AppConfig`. Цей метод викликається один раз після повної ініціалізації застосунку. Коментар `# noqa: F401` вказує лінтеру ігнорувати попередження про невикористаний імпорт. Імпорт необхідний для побічного ефекту — реєстрації обробників через декоратори `@receiver`, а не для прямого використання модуля у коді. ```python # apps/accounts/apps.py from django.apps import AppConfig class AccountsConfig(AppConfig): default_auto_field = 'django.db.models.BigAutoField' name = 'apps.accounts' def ready(self): # Import signals module to register receivers import apps.accounts.signals # noqa: F401 ``` Поширеною помилкою є реєстрація сигналів безпосередньо у `models.py` або на верхньому рівні модуля. Це може спричинити проблеми з циклічними імпортами та подвійною реєстрацією обробників. Метод `ready()` гарантує, що імпорт відбувається у правильний момент життєвого циклу застосунку. ## Побудова користувацьких сигналів для доменних подій Вбудовані сигнали покривають події ORM, але бізнес-логіка часто вимагає власних подій. Django дозволяє створювати користувацькі сигнали через клас `Signal`. Це забезпечує слабке зв'язування між модулями: сервіс замовлень не залежить від сервісу сповіщень, а лише генерує подію, на яку підписуються зацікавлені обробники. Такий підхід спрощує тестування (обробники можна відключити в тестах), розширення (нові реакції додаються без зміни базового коду) та підтримку (кожен обробник виконує одну функцію). ```python # apps/orders/signals.py from django.dispatch import Signal # Define custom signals with documentation order_completed = Signal() # Sent after payment confirmation order_refunded = Signal() # Sent after refund processing # apps/orders/services.py from apps.orders.signals import order_completed def complete_order(order): order.status = 'completed' order.save() # Dispatch signal with relevant context order_completed.send( sender=order.__class__, order=order, total=order.total_amount, ) # apps/notifications/receivers.py from django.dispatch import receiver from apps.orders.signals import order_completed @receiver(order_completed) def send_order_confirmation_email(sender, order, **kwargs): # Email logic decoupled from order processing from apps.notifications.services import send_email send_email( to=order.customer.email, template='order_confirmation', context={'order': order}, ) ``` Метод `send()` є синхронним та виконує всі обробники послідовно у тому самому потоці. Для ресурсомістких операцій (надсилання email, виклик зовнішніх API) обробник повинен делегувати роботу черзі завдань (Celery, Django-Q) замість виконання безпосередньо у сигналі. ## Типові помилки на технічних співбесідах Досвідчені інтерв'юери часто перевіряють знання граничних випадків, а не лише загальне розуміння механізмів. Нижче наведено дві найпоширеніші пастки. > **QuerySet.update() не викликає сигнали** > > Методи `QuerySet.update()` та `QuerySet.delete()` виконують SQL-операції безпосередньо на рівні бази даних, обходячи ORM-методи `save()` та `delete()` окремих об'єктів. Як наслідок, сигнали `pre_save`, `post_save`, `pre_delete` та `post_delete` не генеруються. Це критично важливо для коректності бізнес-логіки, побудованої на сигналах. > **Django 5.2 LTS: підтримка до квітня 2028** > > Django 5.2 є версією з довгостроковою підтримкою (LTS), що гарантує оновлення безпеки та виправлення помилок до квітня 2028 року. Для продакшн-проєктів рекомендовано використовувати саме LTS-версії. Серед ключових покращень: повноцінна підтримка асинхронної middleware, покращена продуктивність ORM та розширені можливості міграцій. ## Порівняння middleware та сигналів | Характеристика | Middleware | Сигнали | |---|---|---| | Рівень роботи | HTTP-запит/відповідь | Внутрішні події ORM та застосунку | | Виклик | Кожен HTTP-запит | Подія моделі (save, delete) або користувацький сигнал | | Порядок виконання | Визначений списком MIDDLEWARE | Невизначений (залежить від порядку реєстрації) | | Асинхронна підтримка | async_capable / sync_capable | Лише синхронні обробники | | Доступ до request | Повний доступ | Відсутній (працює на рівні моделей) | | Типове використання | Логування, аутентифікація, CORS, кешування | Автоматичне створення пов'язаних об'єктів, аудит змін, нотифікації | | Вплив на продуктивність | Виконується при кожному запиті | Виконується лише при конкретних подіях | ## Підсумки Middleware та сигнали є фундаментальними механізмами розширення Django, кожен з яких обслуговує чітко визначений архітектурний рівень. Основні висновки для підготовки до технічних співбесід: - Middleware обробляє кожен HTTP-запит за принципом вкладеності: код перед `get_response` виконується на шляху запиту, після — на шляху відповіді - Порядок middleware у `MIDDLEWARE` є критичним: `SecurityMiddleware` першою, `AuthenticationMiddleware` перед будь-якою middleware, що потребує `request.user` - Django 5.2 підтримує повноцінну асинхронну middleware через атрибути `async_capable` та `sync_capable` - Сигнали `post_save` та `pre_save` дозволяють реагувати на збереження моделей без зміни коду моделі - Обробники сигналів реєструються у `AppConfig.ready()` для уникнення циклічних імпортів та подвійної реєстрації - `QuerySet.update()` та `QuerySet.delete()` обходять сигнали, оскільки працюють безпосередньо на рівні SQL - Користувацькі сигнали забезпечують слабке зв'язування між модулями застосунку за патерном publish-subscribe - Middleware працює на рівні HTTP, сигнали — на рівні ORM; обидва механізми доповнюють один одного, але не замінюють --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/uk/blog/django/django-5-2-custom-middleware-signals