Питання на співбесіді з Django: ORM, Middleware та DRF -- поглиблений розбір

Питання на співбесіді з Django: оптимізація ORM з select_related та prefetch_related, архітектура middleware, продуктивність серіалізаторів Django REST Framework, дозволи та пагінація.

Підготовка до співбесіди з Django: ORM, Middleware та Django REST Framework

Питання на співбесіді з 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 на технічних співбесідах стосується проблеми 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.

Готовий до співбесід з Django?

Практикуйся з нашими інтерактивними симуляторами, flashcards та технічними тестами.

DRF-серіалізатори: вкладена серіалізація та продуктивність

Запитання про серіалізатори Django REST Framework зосереджені на вкладеній серіалізації та її впливі на продуктивність. Найчастіше запитання: "Як обробляти вкладені зв'язки, не створюючи 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 та запитання з Django middleware на SharpSkill.

Починай практикувати!

Перевір свої знання з нашими симуляторами співбесід та технічними тестами.

Теги

#django
#python
#interview
#orm
#middleware
#drf
#rest-api

Поділитися

Пов'язані статті