Questions d'entretien Django : ORM, Middleware et DRF en profondeur

Questions d'entretien Django couvrant l'optimisation de l'ORM avec select_related et prefetch_related, l'architecture middleware et les performances des serializers Django REST Framework, permissions et pagination.

Questions d'entretien Django : ORM, Middleware et DRF

Les questions d'entretien Django se concentrent sur trois piliers qui distinguent les candidats expérimentés des autres : la maîtrise de l'ORM, l'architecture middleware et les design patterns de Django REST Framework (DRF). Ce guide détaille les questions exactes posées par les recruteurs en 2026, avec des exemples de code prêts pour la production utilisant Django 5.2 LTS et DRF 3.17.

Ce que les recruteurs évaluent réellement

Les entretiens Django posent rarement des questions sur le CRUD basique. L'accent s'est déplacé vers l'optimisation des QuerySets (select_related vs prefetch_related), la conception de middleware personnalisés et les performances des serializers DRF. Les candidats capables d'expliquer les requêtes N+1 et d'écrire des viewsets efficaces surpassent systématiquement ceux qui ne connaissent que les vues génériques.

Questions d'entretien sur l'ORM Django : optimisation des QuerySets

La question la plus courante sur l'ORM Django cible le problème N+1. Face à un modèle avec des relations de clé étrangère, le candidat doit démontrer quand utiliser select_related par rapport à 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)

La différence entre les deux méthodes tient au type de relation traversée.

python
# queries.py — Approche correcte pour ForeignKey (objet unique)
developers = Developer.objects.select_related("company").all()
# Génère UNE seule requête SQL avec JOIN
# SELECT developer.*, company.* FROM developer INNER JOIN company ...

# Approche correcte pour ManyToMany (objets multiples)
developers = Developer.objects.prefetch_related("skills").all()
# Génère DEUX requêtes SQL :
# 1. SELECT * FROM developer
# 2. SELECT * FROM skill INNER JOIN developer_skills WHERE developer_id IN (...)

select_related effectue un JOIN SQL et fonctionne avec les champs ForeignKey et OneToOneField. prefetch_related exécute une requête séparée et fonctionne avec les ManyToManyField et les relations ForeignKey inverses. Confondre les deux provoque soit des JOINs inutiles sur de grands jeux de données, soit le redouté problème N+1.

ORM avancé : Managers personnalisés et chaînage de QuerySets

Les recruteurs demandent souvent aux candidats d'écrire un manager personnalisé qui encapsule la logique métier. L'objectif est de vérifier que le candidat comprend le pattern manager de Django et sait écrire des interfaces de requêtes réutilisables.

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

La question de suivi clé : « Pourquoi utiliser un QuerySet personnalisé plutôt qu'un simple Manager ? » La réponse tient au chaînage. Les méthodes d'un QuerySet personnalisé peuvent être enchaînées, alors que les méthodes d'un Manager ne peuvent pas l'être après le premier appel.

python
# usage.py — Chaînage de QuerySets en action
# Cela fonctionne car chaque méthode retourne un QuerySet
senior_python_devs = (
    Developer.active_objects  # manager personnalisé
    .active()                 # méthode ActiveDeveloperQuerySet
    .senior()                 # chaîne une autre méthode QuerySet
    .by_skill("Python")       # chaîne une troisième méthode
    .select_related("company")  # les méthodes standard QuerySet fonctionnent toujours
)
Django 5.2 LTS : clés primaires composites

Django 5.2 a introduit CompositePrimaryKey, une fonctionnalité attendue de longue date pour les modèles nécessitant des clés primaires multi-colonnes. Les questions d'entretien sur cette fonctionnalité deviennent de plus en plus fréquentes, en particulier pour les candidats travaillant avec des bases de données héritées ou des schémas d'entrepôt de données.

Questions d'entretien sur le middleware Django : pipeline requête-réponse

Les questions sur le middleware testent la compréhension du cycle de vie requête-réponse de Django. La question standard : « Expliquez l'ordre dans lequel le middleware traite une requête et une réponse. »

La réponse suit un schéma strict. Pendant une requête, les classes middleware s'exécutent de haut en bas tel que défini dans MIDDLEWARE. Pendant une réponse, elles s'exécutent de bas en haut. Cette architecture en couches d'oignon signifie que le premier middleware de la liste enveloppe tout le reste.

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

Une question de suivi courante : « Comment court-circuiter la chaîne middleware ? » Retourner une HttpResponse depuis __call__ avant d'appeler self.get_response(request) arrête entièrement la chaîne. Les middleware restants et la vue ne s'exécutent jamais.

python
# middleware.py — Rate limiting avec court-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)

Hooks middleware : process_view, process_exception et process_template_response

Au-delà de __call__, le middleware Django prend en charge trois méthodes hook optionnelles. Les recruteurs les utilisent pour évaluer la profondeur des connaissances.

python
# middleware.py — Middleware complet avec tous les 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 se déclenche après la résolution d'URL mais avant la vue. Retourner None continue l'exécution ; retourner une HttpResponse court-circuite. process_exception se déclenche uniquement sur les exceptions non gérées. process_template_response se déclenche uniquement pour les objets TemplateResponse, pas pour les HttpResponse classiques.

Prêt à réussir tes entretiens Django ?

Entraîne-toi avec nos simulateurs interactifs, fiches express et tests techniques.

Questions d'entretien DRF : performances des serializers

Les questions sur les serializers de Django REST Framework se concentrent sur la sérialisation imbriquée et les implications de performance des différentes approches. La question la plus posée : « Comment gérer les relations imbriquées sans provoquer de requêtes 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"]

Le serializer seul ne résout pas les problèmes de performance. Le ViewSet doit optimiser le 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")
        )

Sans select_related et prefetch_related dans le ViewSet, chaque développeur sérialisé déclenche des requêtes individuelles pour son entreprise et ses compétences. Sur une liste de 50 développeurs, cela signifie 1 + 50 + 50 = 101 requêtes au lieu de 3.

Permissions et authentification personnalisées DRF

Une question fréquente en entretien DRF : « Écrire une permission personnalisée qui restreint l'accès en fonction du rôle de l'utilisateur et du propriétaire de l'objet. »

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

La nuance recherchée par les recruteurs : has_permission s'exécute à chaque requête (niveau liste), tandis que has_object_permission ne s'exécute que lorsque get_object() est appelé (niveau détail). Oublier de surcharger get_queryset pour les vues liste crée une faille de sécurité où les utilisateurs peuvent voir des objets qui ne leur appartiennent pas.

Erreur de sécurité DRF courante

Se fier uniquement à has_object_permission sans filtrer le queryset laisse l'endpoint de liste sans protection. Il faut toujours combiner les permissions au niveau objet avec un get_queryset filtré pour appliquer le contrôle d'accès sur les vues liste et détail.

Throttling et patterns de pagination DRF

Le throttling et la pagination sont des questions de suivi standard. Les recruteurs veulent voir que le candidat comprend la configuration d'API prête pour la production.

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

La pagination par curseur surpasse la pagination par offset sur les grands jeux de données car elle ne nécessite pas de compter le nombre total de lignes. Le compromis : les clients ne peuvent pas sauter à une page arbitraire. C'est la réponse attendue lorsque les recruteurs demandent « Pourquoi choisiriez-vous la pagination par curseur plutôt que la pagination par numéro de page ? »

Conclusion

Points clés pour la préparation aux entretiens Django :

  • Optimisation de l'ORM : toujours associer la méthode de requête au type de relation. select_related pour ForeignKey/OneToOne, prefetch_related pour ManyToMany et FK inversée. Les QuerySets personnalisés permettent une logique métier chaînable et réutilisable.
  • Architecture middleware : la requête circule de haut en bas, la réponse de bas en haut. Court-circuiter en retournant une réponse avant get_response() est un pattern fondamental pour le rate limiting, les vérifications d'authentification et la validation des requêtes.
  • Performances des serializers DRF : les serializers imbriqués nécessitent une optimisation explicite du queryset dans le ViewSet. Sans select_related/prefetch_related, la sérialisation provoque des requêtes N+1 à grande échelle.
  • Permissions DRF : combiner has_object_permission avec un get_queryset filtré pour appliquer le contrôle d'accès sur les endpoints liste et détail. Les permissions au niveau objet seules laissent les vues liste sans protection.
  • Throttling et pagination : la pagination par curseur évolue mieux que l'offset sur les grandes tables. Configurer des taux de throttling distincts pour les utilisateurs anonymes et authentifiés.

Pour consolider ces connaissances, il est recommandé de pratiquer avec des questions d'entretien sur l'ORM Django et des questions sur le middleware Django sur SharpSkill avant le véritable entretien.

Passe à la pratique !

Teste tes connaissances avec nos simulateurs d'entretien et tests techniques.

Tags

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

Partager

Articles similaires