[{"content":"On entend souvent \u0026ldquo;on a mis en place une API REST\u0026rdquo; dans les équipes. Mais quand on regarde les réponses JSON, il n\u0026rsquo;y a aucun lien. Juste des données brutes. Ce n\u0026rsquo;est pas du REST, c\u0026rsquo;est du CRUD exposé en HTTP.\nLa différence tient à un principe que la plupart des développeurs ignorent : HATEOAS.\nQu\u0026rsquo;est-ce que HATEOAS ? HATEOAS signifie Hypermedia As The Engine Of Application State. C\u0026rsquo;est l\u0026rsquo;une des contraintes fondamentales du REST, définie par Roy Fielding dans sa thèse de 2000 (la même qui a inventé le terme REST).\nLe principe est simple : un client REST ne doit pas avoir besoin de connaître les routes de l\u0026rsquo;API à l\u0026rsquo;avance. Il démarre depuis un point d\u0026rsquo;entrée, et découvre les actions disponibles en suivant les liens fournis dans chaque réponse.\nExactement comme on navigue sur le Web : on arrive sur une page, on lit les liens disponibles, on clique, on avance. On ne tape pas d\u0026rsquo;URL à la main à chaque étape.\nCRUD over HTTP vs REST réel Sans HATEOAS, une réponse typique ressemble à ça :\n{ \u0026#34;id\u0026#34;: 1, \u0026#34;status\u0026#34;: \u0026#34;pending\u0026#34;, \u0026#34;montant\u0026#34;: 1500.00, \u0026#34;client_id\u0026#34;: 7 } Le client qui reçoit ça doit déjà savoir :\nque pour valider, il faut faire POST /contrats/1/valider/ que pour annuler, c\u0026rsquo;est DELETE /contrats/1/ que le client est accessible via GET /clients/7/ Cette connaissance est codée en dur côté client. Si l\u0026rsquo;API change une route, le client casse. Ce n\u0026rsquo;est pas de l\u0026rsquo;évolutivité, c\u0026rsquo;est du couplage fort déguisé.\nUne réponse HATEOAS réelle Avec HATEOAS, la même réponse devient auto-descriptive :\n{ \u0026#34;id\u0026#34;: 1, \u0026#34;status\u0026#34;: \u0026#34;pending\u0026#34;, \u0026#34;montant\u0026#34;: 1500.00, \u0026#34;client_id\u0026#34;: 7, \u0026#34;_links\u0026#34;: { \u0026#34;self\u0026#34;: { \u0026#34;href\u0026#34;: \u0026#34;/api/contrats/1/\u0026#34;, \u0026#34;method\u0026#34;: \u0026#34;GET\u0026#34; }, \u0026#34;valider\u0026#34;: { \u0026#34;href\u0026#34;: \u0026#34;/api/contrats/1/valider/\u0026#34;, \u0026#34;method\u0026#34;: \u0026#34;POST\u0026#34; }, \u0026#34;annuler\u0026#34;: { \u0026#34;href\u0026#34;: \u0026#34;/api/contrats/1/\u0026#34;, \u0026#34;method\u0026#34;: \u0026#34;DELETE\u0026#34; }, \u0026#34;client\u0026#34;: { \u0026#34;href\u0026#34;: \u0026#34;/api/clients/7/\u0026#34;, \u0026#34;method\u0026#34;: \u0026#34;GET\u0026#34; } } } Le client n\u0026rsquo;a plus besoin de connaître les routes. Il lit les liens disponibles et sait quelles actions sont possibles dans l\u0026rsquo;état actuel de la ressource. Si le contrat est déjà validé, le lien valider n\u0026rsquo;apparaît pas dans la réponse. Le client n\u0026rsquo;a même pas à vérifier.\nHATEOAS en pratique : les liens reflètent l\u0026rsquo;état de la ressource C\u0026rsquo;est là que HATEOAS devient vraiment puissant. Les liens changent en fonction de l\u0026rsquo;état :\n// Contrat en statut \u0026#34;pending\u0026#34; \u0026#34;_links\u0026#34;: { \u0026#34;self\u0026#34;: { \u0026#34;href\u0026#34;: \u0026#34;/api/contrats/1/\u0026#34;, \u0026#34;method\u0026#34;: \u0026#34;GET\u0026#34; }, \u0026#34;valider\u0026#34;: { \u0026#34;href\u0026#34;: \u0026#34;/api/contrats/1/valider/\u0026#34;, \u0026#34;method\u0026#34;: \u0026#34;POST\u0026#34; }, \u0026#34;annuler\u0026#34;: { \u0026#34;href\u0026#34;: \u0026#34;/api/contrats/1/\u0026#34;, \u0026#34;method\u0026#34;: \u0026#34;DELETE\u0026#34; } } // Même contrat, statut \u0026#34;validated\u0026#34; \u0026#34;_links\u0026#34;: { \u0026#34;self\u0026#34;: { \u0026#34;href\u0026#34;: \u0026#34;/api/contrats/1/\u0026#34;, \u0026#34;method\u0026#34;: \u0026#34;GET\u0026#34; }, \u0026#34;resilier\u0026#34;: { \u0026#34;href\u0026#34;: \u0026#34;/api/contrats/1/resilier/\u0026#34;, \u0026#34;method\u0026#34;: \u0026#34;POST\u0026#34; } } Le client ne code pas de logique conditionnelle (if status == \u0026quot;pending\u0026quot;: show_validate_button). Il lit les liens disponibles et construit son interface en conséquence. L\u0026rsquo;API pilote l\u0026rsquo;état de l\u0026rsquo;application, c\u0026rsquo;est exactement ce que le nom HATEOAS signifie.\nImplémentation avec Django REST Framework DRF n\u0026rsquo;inclut pas HATEOAS nativement, mais on peut l\u0026rsquo;implémenter proprement avec un serializer dédié :\nfrom rest_framework import serializers from .models import Contrat, ContratStatus class ContratSerializer(serializers.ModelSerializer): _links = serializers.SerializerMethodField() class Meta: model = Contrat fields = [\u0026#39;id\u0026#39;, \u0026#39;status\u0026#39;, \u0026#39;montant\u0026#39;, \u0026#39;client_id\u0026#39;, \u0026#39;_links\u0026#39;] def get__links(self, obj: Contrat) -\u0026gt; dict[str, dict[str, str]]: request = self.context.get(\u0026#39;request\u0026#39;) base = f\u0026#39;/api/contrats/{obj.pk}/\u0026#39; links: dict[str, dict[str, str]] = { \u0026#39;self\u0026#39;: {\u0026#39;href\u0026#39;: base, \u0026#39;method\u0026#39;: \u0026#39;GET\u0026#39;}, } if obj.status == ContratStatus.PENDING: links[\u0026#39;valider\u0026#39;] = {\u0026#39;href\u0026#39;: f\u0026#39;{base}valider/\u0026#39;, \u0026#39;method\u0026#39;: \u0026#39;POST\u0026#39;} links[\u0026#39;annuler\u0026#39;] = {\u0026#39;href\u0026#39;: base, \u0026#39;method\u0026#39;: \u0026#39;DELETE\u0026#39;} if obj.status == ContratStatus.VALIDATED: links[\u0026#39;resilier\u0026#39;] = {\u0026#39;href\u0026#39;: f\u0026#39;{base}resilier/\u0026#39;, \u0026#39;method\u0026#39;: \u0026#39;POST\u0026#39;} if obj.client_id: links[\u0026#39;client\u0026#39;] = { \u0026#39;href\u0026#39;: f\u0026#39;/api/clients/{obj.client_id}/\u0026#39;, \u0026#39;method\u0026#39;: \u0026#39;GET\u0026#39;, } return links Chaque fois qu\u0026rsquo;un contrat est sérialisé, les liens reflètent son état courant. Aucune logique conditionnelle nécessaire côté client.\nModèle de maturité Richardson : où se situe votre API REST ? Leonard Richardson a formalisé les niveaux de maturité d\u0026rsquo;une API REST :\nNiveau Description Exemple 0 Un seul endpoint, tout en POST SOAP, XML-RPC 1 Ressources distinctes GET /contrats/1 2 Verbes HTTP corrects POST /contrats/, DELETE /contrats/1 3 HATEOAS Réponses avec _links La plupart des APIs en production sont au niveau 2. Elles utilisent correctement les verbes HTTP et les ressources, mais s\u0026rsquo;arrêtent là. Le niveau 3 est ce que Fielding appelle \u0026ldquo;REST\u0026rdquo;.\nFaut-il toujours implémenter HATEOAS ? Honnêtement, non. HATEOAS ajoute de la complexité côté serveur et côté client (qui doit savoir lire et suivre les liens). Il est particulièrement adapté quand :\nL\u0026rsquo;API est publique et consommée par des clients tiers inconnus Le workflow est complexe et évolutif (plusieurs états, transitions conditionnelles) On veut réduire le couplage entre le client et l\u0026rsquo;API Pour une API interne consommée par une seule application front que vous contrôlez, les niveaux 1 et 2 sont souvent suffisants et plus pragmatiques.\nMais comprendre HATEOAS change la façon de concevoir une API. Même sans l\u0026rsquo;implémenter complètement, inclure quelques liens clés dans vos réponses améliore déjà la découvrabilité et réduit la documentation nécessaire.\nTu travailles sur l\u0026rsquo;optimisation des requêtes Django ? Jette un œil à Django in_bulk() : pourquoi c\u0026rsquo;est mieux que filter() en masse.\n","permalink":"https://dev-flow.io/posts/api-rest-hateoas/","summary":"\u003cp\u003eOn entend souvent \u0026ldquo;on a mis en place une API REST\u0026rdquo; dans les équipes. Mais quand on regarde les réponses JSON, il n\u0026rsquo;y a aucun lien. Juste des données brutes. Ce n\u0026rsquo;est pas du REST, c\u0026rsquo;est du CRUD exposé en HTTP.\u003c/p\u003e\n\u003cp\u003eLa différence tient à un principe que la plupart des développeurs ignorent : \u003cstrong\u003eHATEOAS\u003c/strong\u003e.\u003c/p\u003e\n\u003ch2 id=\"quest-ce-que-hateoas-\"\u003eQu\u0026rsquo;est-ce que HATEOAS ?\u003c/h2\u003e\n\u003cp\u003eHATEOAS signifie \u003cstrong\u003eHypermedia As The Engine Of Application State\u003c/strong\u003e. C\u0026rsquo;est l\u0026rsquo;une des contraintes fondamentales du REST, définie par Roy Fielding dans sa thèse de 2000 (la même qui a inventé le terme REST).\u003c/p\u003e","title":"HATEOAS : votre API REST n'est peut-être que du CRUD"},{"content":"Avec Django ORM, il existe deux façons d\u0026rsquo;ajouter une valeur calculée sur un ensemble de lignes : annotate() avec une agrégation classique (Max, Count, Sum\u0026hellip;) ou annotate() avec une Window function. En surface, elles se ressemblent. En pratique, elles ont un comportement fondamentalement différent, et choisir la mauvaise peut bloquer toute la chaîne de filtrage.\nGROUP BY avec annotate() : des lignes qui s\u0026rsquo;écrasent Quand on combine values() et annotate() avec une agrégation, Django génère un GROUP BY en SQL. Le résultat : les lignes se regroupent, et on obtient une ligne par groupe.\nfrom django.db.models import Max def get_latest_dates(self) -\u0026gt; QuerySet: return self.values(\u0026#39;ctr_id\u0026#39;).annotate( latest_date=Max(\u0026#39;evt_end_effect_date\u0026#39;) ) SQL généré :\nSELECT ctr_id, MAX(evt_end_effect_date) AS latest_date FROM events GROUP BY ctr_id Le résultat est un dictionnaire par groupe {'ctr_id': 1, 'latest_date': date(2024, 12, 31)}. Plus d\u0026rsquo;instances de modèle complètes, juste les champs agrégés.\nCe qu\u0026rsquo;il faut comprendre sur la chaînabilité : on peut encore appeler .filter() ou .exclude() après, mais la sémantique change radicalement. Les filtres s\u0026rsquo;appliquent sur les groupes agrégés, pas sur les lignes d\u0026rsquo;origine. On ne filtre plus des événements individuels, on filtre des résultats de groupe.\n# ⚠️ Ce filter s\u0026#39;applique sur les groupes, pas les lignes sources self.values(\u0026#39;ctr_id\u0026#39;).annotate(latest_date=Max(\u0026#39;evt_end_effect_date\u0026#39;)).filter( latest_date__gte=date(2024, 1, 1) ) # SQL : HAVING MAX(evt_end_effect_date) \u0026gt;= \u0026#39;2024-01-01\u0026#39; # Pas de select_related(), pas d\u0026#39;accès aux autres champs de la ligne Window functions : annoter sans toucher aux lignes Une Window function calcule une valeur sur une partition de lignes, mais garde toutes les lignes intactes. Chaque ligne reçoit sa valeur calculée comme une annotation supplémentaire.\nfrom django.db.models import F, Window from django.db.models.functions import FirstValue def with_latest_dates(self) -\u0026gt; QuerySet: return self.annotate( latest_date=Window( expression=FirstValue(\u0026#39;evt_end_effect_date\u0026#39;), partition_by=[\u0026#39;ctr_id\u0026#39;], order_by=F(\u0026#39;evt_end_effect_date\u0026#39;).desc(), ) ) SQL généré :\nSELECT *, FIRST_VALUE(evt_end_effect_date) OVER ( PARTITION BY ctr_id ORDER BY evt_end_effect_date DESC ) AS latest_date FROM events Toutes les lignes sont présentes. Chacune a maintenant latest_date, soit la date la plus récente de son groupe ctr_id. Et le QuerySet reste un QuerySet normal.\n# ✅ Tout reste possible après une Window annotation qs = self.with_latest_dates() qs.filter(status=\u0026#39;active\u0026#39;) # filtre normal (WHERE sur colonne non-window) qs.select_related(\u0026#39;contract\u0026#39;) # jointure normale qs.exclude(latest_date__isnull=True) # filtre sur l\u0026#39;annotation window -\u0026gt; sous-requête qs.order_by(\u0026#39;ctr_id\u0026#39;, \u0026#39;-latest_date\u0026#39;) Django GROUP BY vs Window Function : comparaison visuelle GROUP BY Window function ────────────────────────────────── ────────────────────────────────── ctr_id=1, evt=A → ligne 1 ctr_id=1, evt=A, latest=A → ligne 1 ctr_id=1, evt=B → ctr_id=1, evt=B, latest=A → ligne 2 ctr_id=1, evt=C → MAX(C) ──► C ctr_id=1, evt=C, latest=A → ligne 3 ctr_id=2, evt=D → ligne 2 ctr_id=2, evt=D, latest=D → ligne 4 ctr_id=2, evt=E → MAX(E) ──► E ctr_id=2, evt=E, latest=D → ligne 5 GROUP BY compresse. Window annote.\nCas d\u0026rsquo;usage concret : récupérer la ligne la plus récente par groupe Objectif : pour chaque contrat, récupérer l\u0026rsquo;événement avec la date de fin d\u0026rsquo;effet la plus récente, avec accès à tous ses champs.\nAvec GROUP BY, c\u0026rsquo;est impossible directement : on perd les champs de la ligne. Avec Window + RowNumber :\nfrom django.db.models import F, Window from django.db.models.functions import RowNumber def get_latest_event_per_contract(self) -\u0026gt; QuerySet: return ( self.annotate( row_num=Window( expression=RowNumber(), partition_by=[\u0026#39;ctr_id\u0026#39;], order_by=F(\u0026#39;evt_end_effect_date\u0026#39;).desc(), ) ) .filter(row_num=1) .select_related(\u0026#39;contract\u0026#39;) ) RowNumber() numérote les lignes dans chaque partition, triées par date décroissante. filter(row_num=1) garde uniquement la première, c\u0026rsquo;est-à-dire la plus récente. Django ne peut pas ajouter un WHERE direct sur une Window function (impossible en SQL standard), il génère donc une sous-requête : SELECT * FROM (...) \u0026quot;qualify\u0026quot; WHERE \u0026quot;row_num\u0026quot; = 1.\nWindow functions disponibles dans Django from django.db.models.functions import ( FirstValue, # première valeur de la partition LastValue, # dernière valeur Lag, # valeur N lignes en arrière : Lag(\u0026#39;field\u0026#39;, offset=1) Lead, # valeur N lignes en avant : Lead(\u0026#39;field\u0026#39;, offset=1) NthValue, # nième valeur : NthValue(\u0026#39;field\u0026#39;, nth=2) Rank, # rang avec ex-aequo (1, 1, 3) DenseRank, # rang sans trous (1, 1, 2) RowNumber, # numéro de ligne unique par partition CumeDist, # distribution cumulée (0.0 → 1.0) PercentRank, # rang relatif (0.0 → 1.0) Ntile, # découpage en N buckets : Ntile(num_buckets=4) ) GROUP BY ou Django Window Function : tableau de décision values().annotate(Max(...)) annotate(Window(...)) SQL GROUP BY OVER (PARTITION BY ...) Lignes conservées Une par groupe Toutes Accès aux champs Seulement ceux dans values() Tous Chaînabilité Filtre sur groupes (HAVING) Filtre via sous-requête (WHERE sur les lignes) select_related() ❌ ✅ Cas d\u0026rsquo;usage Compter, sommer, max global Rang, valeur voisine, max par ligne La règle simple : si tu as besoin de garder les lignes complètes et de continuer à filtrer normalement après ton calcul, utilise une Window function. Si tu veux uniquement des résultats agrégés (stats, totaux, max globaux), values().annotate() est plus direct et plus lisible.\nTu travailles sur l\u0026rsquo;optimisation ORM Django ? J\u0026rsquo;ai aussi écrit sur Django in_bulk() : pourquoi c\u0026rsquo;est mieux que filter() en masse.\n","permalink":"https://dev-flow.io/posts/django-window-group-by/","summary":"\u003cp\u003eAvec Django ORM, il existe deux façons d\u0026rsquo;ajouter une valeur calculée sur un ensemble de lignes : \u003ccode\u003eannotate()\u003c/code\u003e avec une agrégation classique (\u003ccode\u003eMax\u003c/code\u003e, \u003ccode\u003eCount\u003c/code\u003e, \u003ccode\u003eSum\u003c/code\u003e\u0026hellip;) ou \u003ccode\u003eannotate()\u003c/code\u003e avec une \u003cstrong\u003eWindow function\u003c/strong\u003e. En surface, elles se ressemblent. En pratique, elles ont un comportement fondamentalement différent, et choisir la mauvaise peut bloquer toute la chaîne de filtrage.\u003c/p\u003e\n\u003ch2 id=\"group-by-avec-annotate--des-lignes-qui-sécrasent\"\u003eGROUP BY avec annotate() : des lignes qui s\u0026rsquo;écrasent\u003c/h2\u003e\n\u003cp\u003eQuand on combine \u003ccode\u003evalues()\u003c/code\u003e et \u003ccode\u003eannotate()\u003c/code\u003e avec une agrégation, Django génère un \u003ccode\u003eGROUP BY\u003c/code\u003e en SQL. Le résultat : les lignes se regroupent, et on obtient \u003cstrong\u003eune ligne par groupe\u003c/strong\u003e.\u003c/p\u003e","title":"Django Window Function vs GROUP BY : QuerySets chaînables"},{"content":"Quand on a une liste d\u0026rsquo;identifiants et qu\u0026rsquo;on veut récupérer les instances correspondantes, le réflexe habituel en Django c\u0026rsquo;est filter(pk__in=[...]). Ça marche, c\u0026rsquo;est une seule requête SQL. Mais in_bulk() est une optimisation ORM souvent ignorée : elle retourne un dictionnaire {id: instance} au lieu d\u0026rsquo;un QuerySet, ce qui change radicalement la façon d\u0026rsquo;accéder aux résultats. Là où filter() force un parcours O(n) pour retrouver un objet par son ID, in_bulk() donne un accès direct O(1).\nSignature Django in_bulk() et comportement QuerySet.in_bulk(id_list=(), *, field_name=\u0026#39;pk\u0026#39;) id_list : liste d\u0026rsquo;identifiants à récupérer. Si omis (appelé sans argument), retourne tous les objets de la table. field_name : champ utilisé comme clé du dictionnaire. Doit obligatoirement avoir unique=True, sinon Django lève une ValueError. La requête SQL générée est une simple clause WHERE pk IN (...), une seule requête peu importe la taille de la liste.\nin_bulk() vs filter() : accès O(1) au lieu de O(n) # filter() classique → QuerySet, accès O(n) contrats: list[Contrat] = list(Contrat.objects.filter(pk__in=[1, 2, 3])) contrat: Contrat | None = next((c for c in contrats if c.pk == 2), None) # in_bulk() → dict, accès O(1) contrats_map: dict[int, Contrat] = Contrat.objects.in_bulk([1, 2, 3]) # → {1: \u0026lt;Contrat pk=1\u0026gt;, 2: \u0026lt;Contrat pk=2\u0026gt;, 3: \u0026lt;Contrat pk=3\u0026gt;} contrat = contrats_map.get(2) # accès direct, None si absent Les IDs absents en base n\u0026rsquo;apparaissent tout simplement pas dans le dictionnaire retourné. Pas d\u0026rsquo;erreur, pas de None : clé manquante = objet inexistant.\nin_bulk() avec field_name : indexer par n\u0026rsquo;importe quel champ unique in_bulk() accepte n\u0026rsquo;importe quel champ unique=True via field_name :\n# Par référence unique refs: list[str] = [\u0026#39;REF-001\u0026#39;, \u0026#39;REF-002\u0026#39;, \u0026#39;REF-003\u0026#39;] contrats_map: dict[str, Contrat] = Contrat.objects.in_bulk( refs, field_name=\u0026#39;reference\u0026#39; ) # → {\u0026#39;REF-001\u0026#39;: \u0026lt;Contrat ...\u0026gt;, \u0026#39;REF-002\u0026#39;: \u0026lt;Contrat ...\u0026gt;, ...} contrat: Contrat | None = contrats_map.get(\u0026#39;REF-002\u0026#39;) C\u0026rsquo;est particulièrement utile lors de synchronisations de données où l\u0026rsquo;identifiant métier n\u0026rsquo;est pas la PK.\nCas d\u0026rsquo;usage Django : quand in_bulk() fait la différence Hydrater plusieurs agrégats en une requête Dans un contexte DDD, quand on doit charger plusieurs agrégats à partir d\u0026rsquo;une liste d\u0026rsquo;IDs :\nids: list[int] = [event.contrat_id for event in evenements] contrats_map: dict[int, Contrat] = Contrat.objects.in_bulk(ids) for evenement in evenements: contrat: Contrat | None = contrats_map.get(evenement.contrat_id) if contrat: contrat.appliquer(evenement) Une seule requête pour tous les contrats, puis accès direct par ID dans la boucle.\nÉviter le N+1 lors d\u0026rsquo;imports from decimal import Decimal def importer_lignes(lignes_csv: list[dict[str, str]]) -\u0026gt; None: references: list[str] = [ligne[\u0026#39;ref\u0026#39;] for ligne in lignes_csv] existants: dict[str, Produit] = Produit.objects.in_bulk( references, field_name=\u0026#39;reference\u0026#39; ) a_creer: list[Produit] = [] a_mettre_a_jour: list[Produit] = [] for ligne in lignes_csv: if ligne[\u0026#39;ref\u0026#39;] in existants: produit = existants[ligne[\u0026#39;ref\u0026#39;]] produit.prix = Decimal(ligne[\u0026#39;prix\u0026#39;]) a_mettre_a_jour.append(produit) else: a_creer.append(Produit(reference=ligne[\u0026#39;ref\u0026#39;], prix=ligne[\u0026#39;prix\u0026#39;])) Produit.objects.bulk_create(a_creer) Produit.objects.bulk_update(a_mettre_a_jour, [\u0026#39;prix\u0026#39;]) Pattern classique import/synchro : une requête in_bulk(), puis bulk_create + bulk_update. Zéro N+1.\nRécupérer tous les objets d\u0026rsquo;une table # Charge toute la table en mémoire — à réserver aux petites tables config: dict[int, ParametreApp] = ParametreApp.objects.in_bulk() valeur: str = config[42].valeur Pratique pour les tables de référence (pays, devises, paramètres) qu\u0026rsquo;on consulte souvent.\nOptimiser in_bulk() sur de grandes listes avec le chunking Pour une liste de plusieurs milliers d\u0026rsquo;IDs, la clause IN(...) peut devenir lourde côté base. La solution : découper en lots.\nfrom collections.abc import Iterator from itertools import islice from typing import Any from django.db.models import QuerySet def chunked(iterable: list[Any], size: int) -\u0026gt; Iterator[list[Any]]: it = iter(iterable) while chunk := list(islice(it, size)): yield chunk def in_bulk_chunked( queryset: QuerySet, ids: list[Any], chunk_size: int = 500, field_name: str = \u0026#39;pk\u0026#39;, ) -\u0026gt; dict[Any, Any]: result: dict[Any, Any] = {} for chunk in chunked(ids, chunk_size): result.update(queryset.in_bulk(chunk, field_name=field_name)) return result # Usage contrats: dict[int, Contrat] = in_bulk_chunked( Contrat.objects, liste_de_5000_ids ) Récapitulatif : in_bulk() vs filter() Django filter(pk__in=[...]) in_bulk([...]) Retour QuerySet (liste) dict {id: instance} Accès par ID O(n) — parcours O(1) — clé directe Requêtes SQL 1 1 IDs absents ignorés silencieusement clé absente du dict field_name non oui (unique=True requis) in_bulk() n\u0026rsquo;est pas un remplacement universel de filter(). C\u0026rsquo;est un outil spécifique : quand on a des IDs et qu\u0026rsquo;on veut un accès direct par clé, c\u0026rsquo;est le bon choix. Pour tout le reste, filter() reste parfaitement adapté.\nTu travailles sur des sujets de performance Django ? Jette un œil à pourquoi l\u0026rsquo;IA rend l\u0026rsquo;apprentissage du code plus essentiel que jamais.\n","permalink":"https://dev-flow.io/posts/django-in-bulk/","summary":"\u003cp\u003eQuand on a une liste d\u0026rsquo;identifiants et qu\u0026rsquo;on veut récupérer les instances correspondantes, le réflexe habituel en Django c\u0026rsquo;est \u003ccode\u003efilter(pk__in=[...])\u003c/code\u003e. Ça marche, c\u0026rsquo;est une seule requête SQL. Mais \u003ccode\u003ein_bulk()\u003c/code\u003e est une optimisation ORM souvent ignorée : elle retourne un \u003cstrong\u003edictionnaire\u003c/strong\u003e \u003ccode\u003e{id: instance}\u003c/code\u003e au lieu d\u0026rsquo;un QuerySet, ce qui change radicalement la façon d\u0026rsquo;accéder aux résultats. Là où \u003ccode\u003efilter()\u003c/code\u003e force un parcours O(n) pour retrouver un objet par son ID, \u003ccode\u003ein_bulk()\u003c/code\u003e donne un accès direct O(1).\u003c/p\u003e","title":"Django in_bulk() : optimiser les requêtes ORM et éviter le N+1"},{"content":"On entend souvent la même promesse ces derniers temps : \u0026ldquo;Plus besoin de savoir coder, l\u0026rsquo;IA s\u0026rsquo;en charge.\u0026rdquo; Et franchement, c\u0026rsquo;est séduisant. On ouvre un agent, on décrit ce qu\u0026rsquo;on veut, et en quelques secondes, du code apparaît. Magique.\nSauf que non. Pas vraiment.\nLe développement agentique, oui mais pour qui ? Le développement assisté par IA est une révolution réelle. Je ne vais pas prétendre le contraire. Pour un développeur senior ou intermédiaire, qui s\u0026rsquo;est déjà heurté à des problématiques complexes, débogué des algos tordus, et livré des systèmes en production, la productivité atteint des sommets inégalés. On délègue les tâches répétitives, on prototypé en heures ce qui prenait des jours, et on reste dans sa zone de haute valeur : l\u0026rsquo;architecture, les décisions critiques, la validation.\nMais il y a un mot clé dans cette phrase : validation.\nCar le développement agentique n\u0026rsquo;est pas parfait. Loin de là. L\u0026rsquo;agent hallucine, il génère du code qui compile mais qui est fondamentalement incorrect, il ignore les conventions du projet, contourne les bonnes pratiques, introduit des failles de sécurité discrètes. Pour exploiter réellement cet outil, il faut pouvoir le guider, le corriger, le challenger.\nEt pour guider un agent de code, il faut savoir coder.\nValider sans comprendre : une illusion dangereuse Imaginez confier la supervision de travaux de gros œuvre à quelqu\u0026rsquo;un qui n\u0026rsquo;a jamais mis les pieds sur un chantier. Il pourra regarder si les murs sont droits, si la peinture est jolie. Mais les fondations ? Les normes parasismiques ? La conformité électrique ? Ça lui échappera complètement.\nC\u0026rsquo;est exactement ce qui se passe quand un développeur sans expérience pratique tente de valider du code généré par une IA. Il peut vérifier que ça \u0026ldquo;fonctionne\u0026rdquo; en surface. Mais la lisibilité, la maintenabilité, la gestion des cas limites, la pertinence algorithmique, les risques sécurité : tout ça lui restera invisible.\nLa validation sera superficielle. Et dans notre métier, le superficiel finit toujours par exploser en production.\nL\u0026rsquo;apprentissage ne peut pas être que théorique On le sait, l\u0026rsquo;apprentissage du développement passe par la pratique. Écrire des lignes de code. Se planter. Déboguer pendant trois heures pour réaliser qu\u0026rsquo;il manquait une virgule. Implémenter un algorithme de zéro pour comprendre pourquoi la complexité temporelle compte. Se battre avec une régression inexplicable jusqu\u0026rsquo;à développer un instinct pour les causes probables.\nCe sont ces cicatrices qui forment un développeur capable de juger, d\u0026rsquo;anticiper, de décider.\nOr, si l\u0026rsquo;IA est là en permanence pour \u0026ldquo;sauver\u0026rdquo; l\u0026rsquo;apprenti du moindre obstacle, la paresse cognitive s\u0026rsquo;installe. Pourquoi réfléchir quand l\u0026rsquo;IA répond ? Pourquoi explorer quand la solution est à portée de prompt ? On cesse de se confronter au problème. On délègue la réflexion. Et sans s\u0026rsquo;en rendre compte, on ne développe jamais les réflexes qui font la différence.\nLa question que personne ne pose encore assez Dans cinq à dix ans, une génération de seniors va partir à la retraite. Ces développeurs qui ont construit des systèmes critiques, qui connaissent les patterns éprouvés, qui peuvent dire \u0026ldquo;j\u0026rsquo;ai déjà vu ça tourner mal\u0026rdquo;\u0026hellip; et ils vont disparaître.\nQui va les remplacer ?\nDes développeurs formés dans un monde où l\u0026rsquo;IA fait le code pour eux, et où l\u0026rsquo;apprentissage pratique a été court-circuité par la commodité ? Des gens capables d\u0026rsquo;écrire de bons prompts, mais incapables d\u0026rsquo;auditer ce que l\u0026rsquo;agent a produit avec rigueur ?\nAujourd\u0026rsquo;hui, le code critique en production est encore validé par des humains compétents. Mais cette compétence, elle n\u0026rsquo;est pas héréditaire. Elle se construit, difficilement, par l\u0026rsquo;expérience.\nEt si l\u0026rsquo;IA devenait parfaite demain ? Peut-être. Les avancées sont réelles et s\u0026rsquo;accélèrent. Il est possible qu\u0026rsquo;un jour, les agents soient capables d\u0026rsquo;autovalidation qualitative : vérifier eux-mêmes que le code qu\u0026rsquo;ils produisent respecte les bonnes pratiques, est sécurisé, performant, maintenable.\nMais d\u0026rsquo;après mon expérience, même aujourd\u0026rsquo;hui avec les modèles les plus avancés, si on veut du code propre, il faut le guider. Lui donner du contexte. Lui imposer des contraintes. Lui corriger le cap. Et pour faire tout ça, il faut une vision. Une expertise. Un jugement.\nL\u0026rsquo;IA est un outil extraordinaire. Mais comme tout outil, son efficacité dépend entièrement de la main qui le tient.\nConclusion : apprendre à coder n\u0026rsquo;a jamais été aussi important Paradoxalement, l\u0026rsquo;essor de l\u0026rsquo;IA dans le développement rend l\u0026rsquo;apprentissage du code plus essentiel, pas moins. Non pas pour écrire chaque ligne soi-même, c\u0026rsquo;est une vision du passé, mais pour garder la capacité de comprendre, évaluer, et orienter ce que les machines produisent.\nLes nouveaux développeurs qui investiront dans cet apprentissage difficile et pratique seront ceux qui tireront le meilleur parti de l\u0026rsquo;IA. Les autres seront au mieux des opérateurs de surface, compétents tant que tout va bien, dépassés dès que ça coince.\nLe code, ça s\u0026rsquo;apprend encore. Et ça s\u0026rsquo;apprend en le vivant.\nTu viens d\u0026rsquo;arriver sur DevFlow ? Découvre pourquoi ce blog parle de Python, Django et FastAPI.\n","permalink":"https://dev-flow.io/posts/ia-apprentissage-code/","summary":"\u003cp\u003eOn entend souvent la même promesse ces derniers temps : \u003cem\u003e\u0026ldquo;Plus besoin de savoir coder, l\u0026rsquo;IA s\u0026rsquo;en charge.\u0026rdquo;\u003c/em\u003e Et franchement, c\u0026rsquo;est séduisant. On ouvre un agent, on décrit ce qu\u0026rsquo;on veut, et en quelques secondes, du code apparaît. Magique.\u003c/p\u003e\n\u003cp\u003eSauf que non. Pas vraiment.\u003c/p\u003e\n\u003ch2 id=\"le-développement-agentique-oui-mais-pour-qui-\"\u003eLe développement agentique, oui mais pour qui ?\u003c/h2\u003e\n\u003cp\u003eLe développement assisté par IA est une révolution réelle. Je ne vais pas prétendre le contraire. Pour un développeur senior ou intermédiaire, qui s\u0026rsquo;est déjà heurté à des problématiques complexes, débogué des algos tordus, et livré des systèmes en production, la productivité atteint des sommets inégalés. On délègue les tâches répétitives, on prototypé en heures ce qui prenait des jours, et on reste dans sa zone de haute valeur : l\u0026rsquo;architecture, les décisions critiques, la validation.\u003c/p\u003e","title":"L'IA ne remplace pas l'apprentissage du code"},{"content":"Ce blog, c\u0026rsquo;est avant tout un espace de partage : des découvertes, des réflexions, des choses qui m\u0026rsquo;ont été utiles et qui pourraient l\u0026rsquo;être pour d\u0026rsquo;autres.\nPython, Django, FastAPI et DRF : le cœur du blog Le cœur du blog, c\u0026rsquo;est le développement Python, et plus précisément les frameworks qui structurent mon quotidien : Django, FastAPI et Flask. Chacun a ses forces, ses cas d\u0026rsquo;usage, ses pièges. On rentrera dans le détail.\nMais au-delà du code qui tourne, ce qui m\u0026rsquo;intéresse, c\u0026rsquo;est le code qui dure. Donc on parlera aussi de méthodes et de pratiques :\nTDD : écrire les tests avant le code, pourquoi ça change vraiment la façon de penser SOLID : les principes derrière le code maintenable DDD : modéliser le métier, pas juste la base de données Et puis il y a tout ce qu\u0026rsquo;on accumule avec l\u0026rsquo;expérience : les patterns qu\u0026rsquo;on adopte, ceux qu\u0026rsquo;on abandonne, les erreurs qu\u0026rsquo;on arrête de faire, les raccourcis qu\u0026rsquo;on apprend à éviter.\nGo, Lua, JavaScript et d\u0026rsquo;autres langages De temps en temps, on sortira du périmètre. Go pour ce qu\u0026rsquo;il apporte en termes de performance et de simplicité dans certains contextes. Lua pour ses cas d\u0026rsquo;usage inattendus. D\u0026rsquo;autres langages si l\u0026rsquo;occasion se présente, pas par exhaustivité, mais par curiosité.\nBienvenue sur DevFlow.\nSi la question du développement à l\u0026rsquo;ère de l\u0026rsquo;IA t\u0026rsquo;intéresse, j\u0026rsquo;ai écrit un article sur pourquoi l\u0026rsquo;IA rend l\u0026rsquo;apprentissage du code plus essentiel que jamais.\n","permalink":"https://dev-flow.io/posts/bienvenue-sur-devontheflow/","summary":"\u003cp\u003eCe blog, c\u0026rsquo;est avant tout un espace de partage : des découvertes, des réflexions, des choses qui m\u0026rsquo;ont été utiles et qui pourraient l\u0026rsquo;être pour d\u0026rsquo;autres.\u003c/p\u003e\n\u003ch2 id=\"python-django-fastapi-et-drf--le-cœur-du-blog\"\u003ePython, Django, FastAPI et DRF : le cœur du blog\u003c/h2\u003e\n\u003cp\u003eLe cœur du blog, c\u0026rsquo;est le développement \u003cstrong\u003ePython\u003c/strong\u003e, et plus précisément les frameworks qui structurent mon quotidien : \u003cstrong\u003eDjango\u003c/strong\u003e, \u003cstrong\u003eFastAPI\u003c/strong\u003e et \u003cstrong\u003eFlask\u003c/strong\u003e. Chacun a ses forces, ses cas d\u0026rsquo;usage, ses pièges. On rentrera dans le détail.\u003c/p\u003e","title":"Pourquoi ce blog sur Python, Django et FastAPI ?"}]