# Питання на співбесіді з Django: ORM, Middleware та DRF -- поглиблений розбір > Питання на співбесіді з Django: оптимізація ORM з select_related та prefetch_related, архітектура middleware, продуктивність серіалізаторів Django REST Framework, дозволи та пагінація. - Published: 2026-04-03 - Updated: 2026-04-03 - Author: SharpSkill - Tags: django, python, interview, orm, middleware, drf, rest-api - Reading time: 9 min --- Питання на співбесіді з Django залишаються одним із найскладніших етапів відбору Python-розробників у 2026 році. Три ключові напрямки, що відрізняють досвідчених кандидатів від решти -- оптимізація ORM, архітектура middleware та паттерни проєктування в Django REST Framework (DRF). Цей матеріал розбирає конкретні запитання, які ставлять технічні інтерв'юери, з прикладами production-рівня на базі Django 5.2 LTS та DRF 3.17. > **Що насправді перевіряють на співбесідах** > > Сучасні Django-співбесіди рідко зводяться до базового CRUD. Фокус змістився на оптимізацію QuerySet (select_related проти prefetch_related), розробку кастомного middleware та продуктивність DRF-серіалізаторів. Кандидати, здатні пояснити проблему N+1 запитів та написати ефективні ViewSet, стабільно випереджають тих, хто знає лише generic views. ## Оптимізація Django ORM: select_related та prefetch_related Найпоширеніше запитання з Django ORM на технічних співбесідах стосується проблеми N+1 запитів. За наявності моделі зі зв'язками типу ForeignKey та ManyToMany кандидат повинен продемонструвати, коли застосовувати `select_related`, а коли -- `prefetch_related`. ```python # models.py from django.db import models class Company(models.Model): name = models.CharField(max_length=200) founded_year = models.IntegerField() class Developer(models.Model): name = models.CharField(max_length=200) company = models.ForeignKey(Company, on_delete=models.CASCADE, related_name="developers") skills = models.ManyToManyField("Skill", related_name="developers") class Skill(models.Model): name = models.CharField(max_length=100) category = models.CharField(max_length=50) ``` Різниця між цими двома методами визначається типом зв'язку, який потрібно завантажити. ```python # queries.py — Correct approach for ForeignKey (single object) developers = Developer.objects.select_related("company").all() # Generates ONE SQL query with JOIN # SELECT developer.*, company.* FROM developer INNER JOIN company ... # Correct approach for ManyToMany (multiple objects) developers = Developer.objects.prefetch_related("skills").all() # Generates TWO SQL queries: # 1. SELECT * FROM developer # 2. SELECT * FROM skill INNER JOIN developer_skills WHERE developer_id IN (...) ``` `select_related` виконує SQL JOIN і працює з ForeignKey та OneToOneField -- результат потрапляє в один SQL-запит. `prefetch_related` виконує окремий запит і підходить для ManyToManyField та зворотних ForeignKey-зв'язків. Плутанина між ними призводить або до надлишкових JOIN на великих наборах даних, або до класичної проблеми N+1. ## Кастомні менеджери та ланцюжкові QuerySet-запити Інтерв'юери часто просять написати кастомний менеджер, що інкапсулює бізнес-логіку. Мета -- переконатися, що кандидат розуміє паттерн Manager/QuerySet в Django та вміє створювати повторно використовувані інтерфейси запитів. ```python # managers.py from django.db import models from django.utils import timezone class ActiveDeveloperQuerySet(models.QuerySet): def active(self): """Filter developers who logged in within the last 30 days.""" cutoff = timezone.now() - timezone.timedelta(days=30) return self.filter(last_login__gte=cutoff) def senior(self): """Filter developers with 5+ years of experience.""" return self.filter(years_experience__gte=5) def by_skill(self, skill_name): """Filter developers by a specific skill.""" return self.filter(skills__name=skill_name) class ActiveDeveloperManager(models.Manager): def get_queryset(self): return ActiveDeveloperQuerySet(self.model, using=self._db) def active(self): return self.get_queryset().active() ``` Типове уточнювальне запитання: "Чому варто використовувати кастомний QuerySet замість простого Manager?" Відповідь -- можливість ланцюжкового виклику (chaining). Методи кастомного QuerySet можна поєднувати в ланцюжки, тоді як методи Manager не підтримують ланцюжкування після першого виклику. ```python # usage.py — QuerySet chaining in action # This works because each method returns a QuerySet senior_python_devs = ( Developer.active_objects # custom manager .active() # ActiveDeveloperQuerySet method .senior() # chains another QuerySet method .by_skill("Python") # chains a third method .select_related("company") # standard QuerySet method still works ) ``` > **Django 5.2 LTS: складені первинні ключі** > > Django 5.2 запровадив CompositePrimaryKey -- довгоочікувану можливість для моделей, яким потрібні багатоколонкові первинні ключі. Запитання про цю функцію на співбесідах стають дедалі частішими, особливо для кандидатів, що працюють з legacy-базами даних або схемами сховищ даних (data warehouse). ## Middleware-конвеєр Django: обробка запитів та відповідей Запитання про middleware перевіряють розуміння кандидатом життєвого циклу запит-відповідь у Django. Стандартне запитання: "Поясніть порядок, у якому middleware обробляє запит та відповідь." Відповідь має чітку структуру. Під час обробки запиту middleware-класи виконуються зверху вниз, у порядку визначення в `MIDDLEWARE`. Під час обробки відповіді -- знизу вверх. Ця багатошарова архітектура (onion-layer) означає, що перший middleware у списку огортає все інше. ```python # middleware.py import time import logging from django.http import JsonResponse logger = logging.getLogger(__name__) class RequestTimingMiddleware: """Logs the time taken to process each request.""" def __init__(self, get_response): self.get_response = get_response # next middleware or view def __call__(self, request): start_time = time.monotonic() response = self.get_response(request) # passes to next layer duration_ms = (time.monotonic() - start_time) * 1000 logger.info( "method=%s path=%s status=%d duration=%.2fms", request.method, request.path, response.status_code, duration_ms, ) return response ``` Поширене додаткове запитання: "Як перервати ланцюжок middleware?" Повернення HttpResponse з `__call__` до виклику `self.get_response(request)` повністю зупиняє ланцюжок. Решта middleware та сама view-функція ніколи не виконуються. ```python # middleware.py — Rate limiting with short-circuit from django.core.cache import cache class RateLimitMiddleware: """Blocks requests exceeding 100 per minute per IP.""" def __init__(self, get_response): self.get_response = get_response def __call__(self, request): ip = request.META.get("REMOTE_ADDR") cache_key = f"rate_limit:{ip}" request_count = cache.get(cache_key, 0) if request_count >= 100: return JsonResponse( # short-circuits — view never executes {"error": "Rate limit exceeded. Try again in 60 seconds."}, status=429, ) cache.set(cache_key, request_count + 1, timeout=60) return self.get_response(request) ``` ## Хуки middleware: process_view, process_exception та process_template_response Окрім `__call__`, middleware в Django підтримує три необов'язкові методи-хуки. Інтерв'юери використовують їх для оцінки глибини знань кандидата. ```python # middleware.py — Full middleware with all hooks class AuditMiddleware: def __init__(self, get_response): self.get_response = get_response def __call__(self, request): return self.get_response(request) def process_view(self, request, view_func, view_args, view_kwargs): """Called after URL resolution, before the view executes.""" logger.info("Calling view: %s", view_func.__name__) return None # returning None continues normal processing def process_exception(self, request, exception): """Called only if the view raises an exception.""" logger.error("View exception: %s", exception, exc_info=True) return None # returning None lets Django's default handling proceed def process_template_response(self, request, response): """Called if the response has a render() method (TemplateResponse).""" response.context_data["audit_timestamp"] = time.time() return response # must return a response with render() ``` `process_view` спрацьовує після розв'язання URL, але до виконання view. Повернення `None` продовжує виконання; повернення `HttpResponse` перериває ланцюжок. `process_exception` спрацьовує лише при необроблених винятках у view. `process_template_response` викликається лише для об'єктів `TemplateResponse`, а не для звичайних `HttpResponse`. ## DRF-серіалізатори: вкладена серіалізація та продуктивність Запитання про серіалізатори [Django REST Framework](https://www.django-rest-framework.org/) зосереджені на вкладеній серіалізації та її впливі на продуктивність. Найчастіше запитання: "Як обробляти вкладені зв'язки, не створюючи N+1 запитів?" ```python # serializers.py from rest_framework import serializers from .models import Developer, Company, Skill class SkillSerializer(serializers.ModelSerializer): class Meta: model = Skill fields = ["name", "category"] class CompanySerializer(serializers.ModelSerializer): class Meta: model = Company fields = ["name", "founded_year"] class DeveloperSerializer(serializers.ModelSerializer): company = CompanySerializer(read_only=True) # nested FK skills = SkillSerializer(many=True, read_only=True) # nested M2M class Meta: model = Developer fields = ["id", "name", "company", "skills"] ``` Сам по собі серіалізатор не вирішує проблему продуктивності. ViewSet повинен оптимізувати queryset. ```python # views.py from rest_framework import viewsets from .models import Developer from .serializers import DeveloperSerializer class DeveloperViewSet(viewsets.ReadOnlyModelViewSet): serializer_class = DeveloperSerializer def get_queryset(self): return ( Developer.objects .select_related("company") # JOIN for FK .prefetch_related("skills") # separate query for M2M .order_by("-id") ) ``` Без `select_related` та `prefetch_related` у ViewSet кожен серіалізований об'єкт Developer спричинює окремі запити для company та skills. На списку з 50 розробників це означає 1 + 50 + 50 = 101 запит замість 3. ## Кастомні дозволи та автентифікація в DRF Типове запитання з DRF на співбесіді: "Напишіть кастомний permission, який обмежує доступ на основі ролі користувача та власника об'єкта." ```python # permissions.py from rest_framework.permissions import BasePermission class IsOwnerOrAdmin(BasePermission): """ Object-level permission: - Admin users can access any object - Regular users can only access objects they own """ message = "Access restricted to the object owner or admin users." def has_object_permission(self, request, view, obj): if request.user.is_staff: return True # Assumes the model has an 'owner' field pointing to User return obj.owner == request.user ``` ```python # views.py — Applying custom permissions from rest_framework import viewsets, permissions from .permissions import IsOwnerOrAdmin class ProjectViewSet(viewsets.ModelViewSet): permission_classes = [permissions.IsAuthenticated, IsOwnerOrAdmin] def get_queryset(self): # Non-admin users only see their own projects if self.request.user.is_staff: return Project.objects.all() return Project.objects.filter(owner=self.request.user) def perform_create(self, serializer): serializer.save(owner=self.request.user) # auto-assign owner ``` Важливий нюанс, на який звертають увагу інтерв'юери: `has_permission` виконується при кожному запиті (рівень списку), тоді як `has_object_permission` -- лише при виклику `get_object()` (рівень деталей). Якщо не перевизначити `get_queryset` для list-ендпоінту, виникає прогалина в безпеці: користувачі зможуть бачити об'єкти, що їм не належать. > **Поширена помилка безпеки в DRF** > > Покладатися виключно на has_object_permission без фільтрації queryset означає залишити list-ендпоінт без захисту. Завжди слід поєднувати object-level permissions із відфільтрованим get_queryset, щоб забезпечити контроль доступу на обох рівнях -- і для списку, і для деталей. ## Троттлінг та пагінація в DRF Троттлінг і пагінація -- стандартні додаткові запитання на Django-співбесідах. Інтерв'юери хочуть переконатися, що кандидат розуміє production-конфігурацію API. ```python # settings.py — Production DRF configuration REST_FRAMEWORK = { "DEFAULT_THROTTLE_CLASSES": [ "rest_framework.throttling.AnonRateThrottle", "rest_framework.throttling.UserRateThrottle", ], "DEFAULT_THROTTLE_RATES": { "anon": "20/minute", # unauthenticated users "user": "200/minute", # authenticated users }, "DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.CursorPagination", "PAGE_SIZE": 25, } ``` ```python # pagination.py — Custom cursor pagination for consistent ordering from rest_framework.pagination import CursorPagination class CreatedAtCursorPagination(CursorPagination): page_size = 25 ordering = "-created_at" # must be a unique, sequential field cursor_query_param = "cursor" # ?cursor=abc123 ``` Cursor-пагінація перевершує offset-пагінацію на великих наборах даних, оскільки не потребує підрахунку загальної кількості рядків. Компроміс: клієнт не може перейти на довільну сторінку. Саме таку відповідь очікують інтерв'юери на запитання "Чому ви обрали б cursor-пагінацію замість пагінації за номером сторінки?" ## Висновок Ключові тези для підготовки до Django-співбесіди: - **Оптимізація ORM**: метод запиту має відповідати типу зв'язку. `select_related` -- для ForeignKey/OneToOne, `prefetch_related` -- для ManyToMany та зворотних FK. Кастомні QuerySet забезпечують ланцюжкову, повторно використовувану бізнес-логіку. - **Архітектура middleware**: запит проходить зверху вниз, відповідь -- знизу вверх. Переривання ланцюжка через повернення відповіді до `get_response()` -- фундаментальний паттерн для rate limiting, перевірки автентифікації та валідації запитів. - **Продуктивність DRF-серіалізаторів**: вкладені серіалізатори вимагають явної оптимізації queryset у ViewSet. Без `select_related`/`prefetch_related` серіалізація спричинює N+1 запити на масштабі. - **Дозволи DRF**: `has_object_permission` необхідно поєднувати з відфільтрованим `get_queryset` для контролю доступу на обох рівнях -- list та detail. Object-level permissions самі по собі залишають list-ендпоінт без захисту. - **Троттлінг та пагінація**: cursor-пагінація масштабується краще за offset на великих таблицях. Ліміти запитів слід конфігурувати окремо для анонімних та автентифікованих користувачів. Для практичного закріплення цих паттернів варто пройти [запитання зі співбесід з Django ORM](/technologies/django/interview-questions/django-orm-advanced) та [запитання з Django middleware](/technologies/django/interview-questions/django-middleware) на SharpSkill. --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/uk/blog/django/django-interview-orm-middleware-drf