Pytania rekrutacyjne Django: ORM, Middleware i DRF -- szczegółowa analiza

Pytania rekrutacyjne Django obejmujące optymalizację ORM z select_related i prefetch_related, architekturę middleware oraz wydajność serializerów Django REST Framework, uprawnienia i paginację.

Przygotowanie do rozmowy kwalifikacyjnej z Django obejmujące ORM, Middleware i Django REST Framework

Pytania rekrutacyjne Django dotyczące ORM, middleware i Django REST Framework stanowią fundament każdej rozmowy kwalifikacyjnej na stanowisko Python-developera w 2026 roku. Niniejszy artykuł omawia zaawansowane zagadnienia -- od optymalizacji zapytań przez select_related i prefetch_related, przez architekturę potoku middleware, aż po serializery, uprawnienia i throttling w DRF. Każda sekcja zawiera praktyczne przykłady kodu gotowe do zastosowania w realnych projektach opartych na Django 5.2 LTS i DRF 3.17.

Wskazówka rekrutacyjna

Rekruterzy szczególnie cenią kandydatów, którzy potrafią wyjaśnić różnice między select_related a prefetch_related na konkretnych przykładach SQL. Umiejętność uzasadnienia wyboru jednej metody nad drugą w kontekście wydajności daje wyraźną przewagę podczas rozmowy technicznej.

Jednym z najczęściej zadawanych pytań na rozmowach jest problem N+1 i sposoby jego rozwiązywania w Django ORM. Aby zrozumieć to zagadnienie, warto zacząć od modeli danych ilustrujących typowe relacje: klucz obcy (ForeignKey) oraz relacje wiele-do-wielu (ManyToManyField).

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)

Model Developer posiada relację ForeignKey do Company oraz ManyToMany do Skill. Bez odpowiedniej optymalizacji iteracja po developerach i odwoływanie się do powiązanych obiektów generuje oddzielne zapytanie SQL dla każdego wiersza -- to właśnie problem N+1.

Rozwiązaniem jest zastosowanie odpowiedniego mechanizmu prefetchowania w zależności od typu relacji:

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 (...)

Zasada jest prosta: select_related stosuje się do relacji ForeignKey i OneToOneField, ponieważ generuje pojedyncze zapytanie z JOIN. Natomiast prefetch_related obsługuje relacje ManyToMany i odwrotne klucze obce, wykonując dwa oddzielne zapytania i łącząc wyniki w Pythonie. Znajomość tej różnicy eliminuje większość problemów wydajnościowych w aplikacjach Django.

Niestandardowe Managery i łączenie QuerySetów

Podczas rozmów rekrutacyjnych często pada pytanie o Custom Managery -- mechanizm enkapsulacji często używanych zapytań na poziomie modelu. Zamiast powtarzać logikę filtrowania w wielu widokach, można zdefiniować niestandardowy QuerySet i Manager.

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()

Kluczowa zaleta tego podejścia polega na możliwości łączenia metod QuerySet w łańcuch (chaining). Każda metoda zwraca nowy QuerySet, co pozwala budować złożone zapytania w czytelny, deklaratywny sposób:

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
)

Standardowe metody QuerySet, takie jak select_related, pozostają dostępne nawet przy użyciu niestandardowego Managera. To podejście jest zgodne z zasadą DRY i centralizuje logikę zapytań w jednym miejscu.

Django 5.2 LTS -- klucze kompozytowe

Django 5.2 LTS wprowadza natywne wsparcie dla kluczy kompozytowych (composite primary keys) poprzez pole CompositePrimaryKey. Eliminuje to konieczność stosowania obejść z unikalnymi ograniczeniami (unique_together) w tabelach pośredniczących. W kontekście rekrutacyjnym warto wspomnieć o tej funkcji jako przykładzie ewolucji ORM.

Potok middleware -- architektura i kolejność wykonywania

Middleware w Django to łańcuch warstw opakowujących (wrapper layers), przez które przechodzi każde żądanie HTTP. Każdy middleware otrzymuje referencję do następnego elementu w łańcuchu poprzez get_response. Wywołanie self.get_response(request) przekazuje sterowanie dalej -- do następnego middleware lub do widoku.

Poniższy przykład ilustruje middleware mierzący czas przetwarzania każdego żądania:

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

Kod wykonany przed wywołaniem get_response działa w fazie żądania (request phase), a kod po nim -- w fazie odpowiedzi (response phase). To fundamentalny wzorzec, który rekruterzy oczekują od kandydatów.

Middleware może również skrócić łańcuch przetwarzania, zwracając odpowiedź bez przekazywania żądania dalej. Typowy przykład to rate limiting:

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)

Gdy RateLimitMiddleware zwraca JsonResponse bezpośrednio, widok nigdy nie zostaje wywołany. To skrócenie (short-circuit) jest kluczowym mechanizmem ochrony przed nadmiernym obciążeniem serwera.

Hooki middleware -- process_view, process_exception i process_template_response

Oprócz standardowej metody __call__, middleware w Django może implementować dodatkowe hooki pozwalające na interwencję w różnych fazach cyklu przetwarzania. Pytanie o te hooki pojawia się regularnie na rozmowach o zaawansowanych zagadnieniach 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()

Trzy kluczowe hooki działają w następujący sposób:

  • process_view -- wywoływany po rozwiązaniu URL, ale przed wykonaniem widoku. Zwrócenie None kontynuuje normalne przetwarzanie; zwrócenie HttpResponse pomija widok.
  • process_exception -- wywoływany wyłącznie gdy widok rzuci wyjątek. Służy do logowania, alertów lub niestandardowej obsługi błędów.
  • process_template_response -- wywoływany gdy odpowiedź posiada metodę render() (czyli jest instancją TemplateResponse). Umożliwia modyfikację kontekstu szablonu przed renderowaniem.

Kolejność middleware w ustawieniu MIDDLEWARE jest istotna: żądanie przechodzi przez middleware od góry do dołu, a odpowiedź -- od dołu do góry.

Gotowy na rozmowy o Django?

Ćwicz z naszymi interaktywnymi symulatorami, flashcards i testami technicznymi.

Serializery DRF -- zagnieżdżone relacje i wydajność

Django REST Framework wykorzystuje serializery do konwersji między obiektami Python a formatami takimi jak JSON. Na rozmowach kwalifikacyjnych częstym pytaniem jest obsługa zagnieżdżonych relacji bez generowania problemu 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"]

Parametr read_only=True oznacza, że zagnieżdżone dane są dostępne wyłącznie przy odczycie (GET). Aby uniknąć problemu N+1 w kontekście DRF, ViewSet musi odpowiednio optymalizować 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")
        )

Bez select_related i prefetch_related w ViewSecie każdy serializowany developer generuje oddzielne zapytania dla company i skills. Dla listy 50 developerów oznacza to 1 + 50 + 50 = 101 zapytań zamiast 3.

Uprawnienia w DRF -- kontrola dostępu na poziomie obiektu

System uprawnień w DRF działa na dwóch poziomach: globalnym (view-level) i obiektowym (object-level). Na rozmowach rekrutacyjnych często pada pytanie o implementację niestandardowych klas uprawnień.

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

Metoda has_object_permission jest wywoływana przez DRF automatycznie przy operacjach retrieve, update, partial_update i destroy. Zastosowanie niestandardowych uprawnień w ViewSecie wygląda następująco:

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

Kluczowy niuans, na który zwracają uwagę rekruterzy: has_permission działa przy każdym żądaniu (poziom listy), natomiast has_object_permission -- tylko przy wywołaniu get_object() (poziom szczegółów). Brak nadpisania get_queryset dla widoków listowych tworzy lukę bezpieczeństwa.

Częsty błąd bezpieczeństwa w DRF

Poleganie wyłącznie na has_object_permission bez filtrowania queryset pozostawia endpoint listowy bez ochrony. Zawsze należy łączyć uprawnienia na poziomie obiektu z filtrowanym get_queryset, aby wymusić kontrolę dostępu zarówno na widokach listowych, jak i szczegółowych.

Throttling i paginacja w DRF -- konfiguracja produkcyjna

Throttling (ograniczanie liczby żądań) i paginacja to dwa mechanizmy, których poprawna konfiguracja jest warunkiem wdrożenia API na produkcję. Rekruterzy często pytają o różnice między typami paginacji i strategie throttlingu.

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,
}

Oddzielne limity dla użytkowników anonimowych i uwierzytelnionych są standardową praktyką. W kontekście paginacji, CursorPagination oferuje najlepszą wydajność dla dużych zbiorów danych, ponieważ nie używa OFFSET:

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

Pole ordering musi wskazywać na kolumnę z indeksem i unikalnymi, sekwencyjnymi wartościami. Cursor pagination koduje pozycję w ciągu znaków (cursor), co uniemożliwia przeskakiwanie do dowolnej strony, ale zapewnia stałą wydajność niezależnie od numeru strony.

Podsumowanie

Przygotowanie do rozmowy rekrutacyjnej z Django wymaga solidnego opanowania trzech filarów: ORM, middleware i Django REST Framework. Kluczowe zagadnienia do zapamiętania:

  • Optymalizacja ORM: select_related stosuje się do ForeignKey/OneToOne (SQL JOIN), a prefetch_related do ManyToMany i odwrotnych relacji (oddzielne zapytania). Niestandardowe QuerySety centralizują logikę zapytań i umożliwiają czytelne łączenie metod (chaining).
  • Architektura middleware: żądanie przepływa od góry do dołu, odpowiedź od dołu do góry. Skrócenie łańcucha (short-circuit) przez zwrócenie odpowiedzi przed get_response() to fundamentalny wzorzec dla rate limitingu, walidacji i kontroli dostępu.
  • Wydajność serializerów DRF: zagnieżdżone serializery wymagają optymalizacji QuerySet w ViewSecie. Bez select_related/prefetch_related serializacja generuje N+1 zapytań na skali.
  • Uprawnienia DRF: has_object_permission należy łączyć z filtrowanym get_queryset, aby wymusić kontrolę dostępu na obu poziomach. Same uprawnienia obiektowe pozostawiają widoki listowe bez ochrony.
  • Throttling i paginacja: cursor pagination skaluje się lepiej niż offset na dużych tabelach. Limity żądań należy konfigurować oddzielnie dla użytkowników anonimowych i uwierzytelnionych.

Aby przećwiczyć te wzorce, warto skorzystać z pytań rekrutacyjnych z Django ORM oraz pytań o Django middleware na platformie SharpSkill.

Zacznij ćwiczyć!

Sprawdź swoją wiedzę z naszymi symulatorami rozmów i testami technicznymi.

Tagi

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

Udostępnij

Powiązane artykuły