Django & DRF 101, partie 1 : ORM et accès à la donnée
Tags: django, drf, orm
Published: 2020-01-21 14:02:26

---
title: Django & DRF 101, partie 1 : ORM et accès à la donnée
published: true
description: Stack technique Django / DRF / SPA
tags: django, drf, orm
series: Django DRF
---

## Sommaire
  * [Préambule](#preambule)
  * [Django](#django)
    * [ORM] (#orm)
      * [Entités] (#entites)
      * [Requêtage] (#requetage)
        * [Création d'objets] (#creation)
        * [Interrogations] (#get)
        * [Filtrage avec conditions] (#filter)
        * [Optimisations] (#optimisations)


# Préambule : changement de _stack_ <a name="preambule"></a>

J'ai changé de stack technique depuis bientôt 2 ans. Avec un _background_ principalement .NET (C# / ASPNET Webforms - MVC - WebApi / EF - NHibernate) depuis 15 ans, j'avais envie de changer, après être passé par d'autres langages / frameworks avant ou pendant C# / .NET : C, PHP, Perl, Ruby, Java / Spring.

En voici les raisons.

## .NET 

La plateforme .NET est une plateforme polyvalente, je développais principalement avec pour des applications Web et plus particulièrement sur une architecture "REST" (frontend SPA / API REST) avec MVC puis WebAPI pour coder des API. 

Le [langage C#](https://docs.microsoft.com/fr-fr/dotnet/csharp/) est très bien, devenu opensource à partir de 2018, MS arrive à enrichir le langage pour répondre aux besoins des développeurs, en revanche, j'ai souvent trouvé "complexe" (entendre beaucoup de lignes de code ou [code _boilerplate_](https://en.wikipedia.org/wiki/Boilerplate_code)) pour développer des API ou du MVC (et je ne suis pas le seul, à l'image de ce [tweet](https://twitter.com/davidfowl/status/1225996008544460800)) :

- avoir des compétences **[Entity framework](https://docs.microsoft.com/fr-fr/ef/)** qui est devenu le standard de fait pour l'accès aux données (j'utilisais [NHibernate](https://nhibernate.info/) avant qu'EF devienne mûr). Comme tout ORM, il vaut mieux bien connaitre le framework pour ne pas faire de bêtises ou que cela soit performant, en connaître les subtilités pour éviter des effets de bords indésirables, 
- C# fonctionne souvent avec beaucoup d'[**attributs**](https://docs.microsoft.com/fr-fr/dotnet/csharp/programming-guide/concepts/attributes/) (HttpGet, permissions, attributs dans les méthodes HttpBody et j'en passe, classes sérializables, etc), ce qui alourdit le code et le rend spécialisé,
- devoir avoir des **couches** (N tiers) entre la couche données et les APIs, ce qui oblige souvent à avoir des couches intermédiaires (POCO / DTO) et donc à _mapper_ les objets, à avoir des "services" pour abstraire la logique via des interfaces notamment, ce qui, sur un projet, génère encore une fois beaucoup de code technique

On retrouvera ces points négatifs sur l'écosystème Java ou d'autres plateformes.

Cette **complexité technique** empêche ou du moins réduit fortement de se concentrer sur les aspects **métiers** d'une application, qui pour moi, est bien plus intéressant. 

Le **nombre de ligne de code** induit également _a fortiori_ une augmentation automatique de bugs potentiels, effets de bords ou régressions et de générer un système à termes instable pour la **maintenabilité** de l'application, d'autant plus si plusieurs développeurs interviennent sur cette dernière, j'aime appeler cela l’**entropie** d'une application ("Il caractérise le niveau de désorganisation, ou d'imprédictibilité du contenu en information d'un système").

Je bossais chez un éditeur de solution [ITSM](https://fr.wikipedia.org/wiki/Gestion_des_services_informatiques), et on m'avait parlé de Django / python, tandis qu'on développait, en équipe de 5, la solution en .NET / C# / [ASPNET](https://dotnet.microsoft.com/apps/aspnet). 

[.NET Core](https://dotnet.microsoft.com/) était aussi en train de mûrir, ce qui signifie aussi quelques changements malgré tout, et donc de ré-apprendre  certains éléments, autant changer je me suis dit.

J'ai eu l'occasion d'avoir une opportunité de changer de société, pour aller vers une _stack_ **[Django](https://www.djangoproject.com/)** / **[Django Rest Framework](https://www.django-rest-framework.org/)** / python pour le _backend_ et **[Angular](https://angular.io/)** pour la partie _frontend_, et, après 2 ans, j'ai pu fortement monter en compétences sur cette pile technique et j'en suis très content : plus simple, moins de code, une productivité bien plus haute, le plaisir de retrouver un langage dynamique tel que python (qui me fait penser fortement à Javascript que j'aime) : typage dynamique, _closure_, fonctionnel, ... et bien entendu développer objet (POO). 

Associé à [Django](https://www.djangoproject.com/) / [DRF](https://www.django-rest-framework.org/), on est bien plus productif et surtout, on se concentre sur le métier et non plus sur du code technique (même s'il y en a bien entendu ;-) )

# Django et DRF

Après s'être intéressé au pourquoi avoir basculé de _stack_ technique, intéressons-nous aux frameworks utilisés pour en faire une introduction, avec comme 1ère partie, un focus sur l'ORM de Django.

## Django <a name="django"></a>

Django est un framework Web "MVC" (MVT pour être exact), opensource (c'est une [fondation](https://www.djangoproject.com/foundation/) derrière), écrit en python. 

En tant que **framework**, il englobe tout un tas de fonctionnalités, avec, notamment :

- gestion multi sites (voir ça comme une plateforme applicative avec dessus des applications) : Django peut avoir un mode "site" (projet) dans lequel l'on peut créer des applications, ce qui a l'avantage de mutualiser tout ce qui relatif à la configuration, modules, logs, etc
- l'aspect Web (urls / routes, les requêtes Web, sessions, authentification, utilisateur, scaffolding "admin" pour le développement, ...)
- possibilité de middlewares pour étendre le framework,
- un moteur de template,
- un mode "admin" pour les entités pour du *scaffolding* rapide pour des besoins en mode développement,
- une gestion de configuration, de logs, etc
- un ORM (connexions, requêtage, sécurité des requêtes, multi-bases, etc)

...et c'est très bien [documenté](https://docs.djangoproject.com/).

Dans le cadre d'API, on lui adjoint [DRF](https://www.django-rest-framework.org/) que nous verrons dans un futur article. Avec DRF, je n'utilise pas le modèle MVC / MVT et son moteur de template de Django mais uniquement le reste.

### ORM <a name="orm"></a>

Venant d'[Entity framework](https://docs.microsoft.com/fr-fr/ef/) / [Nhibernate](https://nhibernate.info/), mon passage à à l'ORM de Django a été le plus perturbant pour le requêtage, mais on y vient sans trop de difficulté et quelques principes restent les mêmes (exécution des requêtes différée à la "IQueryable" avec les querysets,  lazy / eager loading, migrations), définition des entités, même si [LINQ](https://docs.microsoft.com/en-us/previous-versions/dotnet/articles/bb308959(v=msdn.10)?redirectedfrom=MSDN) était très appréciable. Un [excellent article sur les ORM](http://sametmax.com/les-critiques-des-orm-sont-a-cote-de-la-plaque/) ou plus particulièrement l'ORM de Django. 

L'**ORM** suit le modèle "**code / model first**" : on définit ses entités, la génération en base vient ensuite grâce aux [migrations](https://docs.djangoproject.com/fr/2.2/topics/migrations/) (que cela soit le schéma de données ou les données), tout est donc porté par le code : versionning des entités que l'on pourra partager dans Git par exemple.

L'ORM propose ce que l'on peut retrouver dans tout ORM : gestion des connexions, typage des champs, relations (1 -- 1, 1 -- *, * -- *), contraintes, requêtage, etc

Pour une configuration d'une station de travail pour Django, voir l'article dédié : https://dev.to/zorky/django-drf-101-initialisation-1e39

#### Entités <a name="entites"></a>

Par exemple, on veut représenter le schéma suivant sous forme UML : 

**Task * ---- 1 TodoList** : pouvoir créer des listes de tâches à faire.

J'utilise une classe utilitaire "abstraite" **TimeStampedModel** pour ajouter automatiquement 2 champs aux entités : dates de création ou de modification d'un enregistrement dans la table, les entités hériteront de cette classe.

```python
class TimeStampedModel(models.Model):
    """
    Classe utilitaire, champs dt_created / dt_updated
    """
    dt_created = models.DateTimeField(auto_now_add=True)
    dt_updated = models.DateTimeField(auto_now=True)

    class Meta:
        abstract = True


class TodoList(TimeStampedModel):
   """
   Une liste de trucs à faire 
   """
   label = models.CharField(max_length=100, help_text='liste de tâches')
    

class Task(TimeStampedModel):
    """
    Une tâche à faire
    """
    label = models.CharField(max_length=300, db_index=True, 
                             null=False, blank=False,
                             help_text='libellé de la tâche')
    done = models.BooleanField(default=False, help_text='fait ou non ?')
    list = models.ForeignKey(TodoList, null=False, 
                             related_name='tasks', 
                             on_delete=models.CASCADE, 
                             help_text='liste associée, list.tasks pour avoir la liste des tâches pour cette liste')

```
Il est nul besoin de préciser l'_id_ (clé primaire), par défaut, Django en ajoute un en auto-incrément. Il est à noter qu'il y a une **restriction** quant aux clés primaires : on ne peut avoir de **clé composée** de plusieurs champs de la table.

_db_index_ crée un index sur _label_, contraintes sur NULL ou vide avec _null=False_ et _blank=False_
le _related_name_ représente la relation inverse (le *many* du *one to many*), côté TodoList, pour avoir les tâches associée à une liste, il suffira alors de faire : list.tasks

#### Migrations <a name="migrations"></a>

Nos entités sont créées, maintenant, générons ce qu'il faut pour la prise en compte en base en utilisant la commande django **[manage.py](https://docs.djangoproject.com/fr/2.2/ref/django-admin/)** et à la commande **[makemigrations](https://docs.djangoproject.com/fr/2.2/ref/django-admin/#makemigrations)** (le script python pour générer ce qu'il faudra en base). Django tient à jour les migrations et les versions des différentes migrations créées (dans une table _django_migration_ qu'il ne faut absolument pas toucher), avantage : on pourra revenir en arrière (grâce à la commande **[migrate](https://docs.djangoproject.com/fr/2.2/ref/django-admin/#django-admin-migrate) <numéro_migration>**) si on le souhaite. 

```
$ python3 manage.py makemigrations
```
Cela génère un fichier dans le répertoire **migrations** de la forme _numero___nom_.py (ici, c'est la 1ère, on aura alors 001_initial.py)

On pourrait aussi nommer la migration pour plus de clarté avec l'option *--name*

```
$ python3 manage.py makemigrations --name 'todolist_task'
```

cela va créer un fichier de migration de type _numero__todolist_task

Lorsque tout est bon, il reste à passer les fichiers de migrations avec la commande *migrate*, cela va créer les tables en base. Par défaut, la convention de nommage des tables est _projet_entite_, ici, si notre projet s'appelle todolist, on aura _todolist_task_ et _todolist_todolist_

```
$ python3 manage.py migrate
``` 

#### Requêtage <a name="requetage"></a>

Pour écrire des requêtes, nous utilisons les **[Queryset](https://docs.djangoproject.com/fr/2.2/ref/models/querysets/)**. Une queryset n'est évaluée (on dira qu'elle est *lazy*) uniquement lorsqu'elle est explicitement appelée (par un print ou une itération par exemple), ce qui permettra de construire sa requête dynamiquement sans l'exécuter, comme nous le verrons plus tard.

Maintenant que nous avons nos 2 entités, on va pouvoir les manipuler. Django propose un utilitaire en ligne de commande : **shell** qui ouvre une session python pour taper des commandes, pour ma part, je lui préfère **shell_plus** qui a le mérite de charger toutes les entités ou autre besoins pour le requêtage, inclut dans le module [django-extensions](https://django-extensions.readthedocs.io/en/latest/shell_plus.html)

```
$ python3 manage.py shell_plus
```

##### Création <a name="creation"></a>

**Créons** une liste de tâches et au moins une tâche dans cette liste :

```python
>>> todolist = TodoList(label='urgente')
>>> todolist.save()

>>> task1 = Task(label='chercher le pain', list=todolist)
>>> task1.save()
>>> task2 = Task(label='prendre les enfants', list=todolist)
>>> task2.save()
```
##### Interrogations <a name="get"></a>

**Interrogeons** en base nos 2 entités :

```python
>>> Task.objects.all()
<QuerySet [<Task: Task object (1)>, <Task: Task object (2)>]>
>>> TodoList.objects.all()
<QuerySet [<TodoList: TodoList object (1)>]>
```

**objects** est le _[manager](https://docs.djangoproject.com/fr/3.0/topics/db/managers/#django.db.models.Manager)_ par défaut des entités, il contient l'API pour requêter les objets, ici, _all()_ qui renvoie l'ensemble des lignes de chaque entité. 

On retrouvera dans **objects** pelle-mêle les fonctions suivantes : _filter()_, _get()_, _order_by()_, _count()_, _exists()_, _exclude()_, _first()_, _last()_, _annotate()_ et bien d'autres.

Par défaut la liste affichée est la liste des objets avec leur _id_, si on veut que cela soit plus parlant, il suffit de surcharge pour chaque entité la fonction __str__ pour préciser quel valeur de champ à afficher, par exemple :

```python
class TodoList(TimeStampedModel):
   """
   Une liste de trucs à faire 
   """
   label = models.CharField(max_length=100, help_text='liste de tâches')
    
   def __str__(self):
      return self.label

class Task(TimeStampedModel):
    """
    Une tâche à faire
    """
    label = models.CharField(max_length=300, db_index=True, 
                             null=False, blank=False,
                             help_text='libellé de la tâche')
    done = models.BooleanField(default=False, help_text='fait ou non ?')
    list = models.ForeignKey(TodoList, null=False, 
                             related_name='tasks', 
                             on_delete=models.CASCADE, 
                             help_text='liste associée, list.tasks pour avoir la liste des tâches pour cette liste')

    def __str__(self):
       return self.label
```

en exécutant la même requête (Task.objects.all()), on aura alors le _label_ et non plus l'_id_ d'affiché :

```
>>> Task.objects.all()
<QuerySet [<Task: chercher le pain>, <Task: prendre les enfants>]>
```

Maintenant, je souhaite recherche la tâche identifiée d'id 2

```python
>>> task = Task.objects.get(id=2)
>>> task
<Task: prendre les enfants>
```

on aurait aussi pu utiliser le mot clé _pk_ (primary key) que je préfère, cela abstrait la dénomination de la clé primaire, par défaut c'est _id_ mais cela pourrait être autre chose si on l'avait défini autrement (uid, object_id, etc) :
```python
>>> task = Task.objects.get(pk=2)
>>> task
<Task: prendre les enfants>
```

ou par son nom :

```python
>>> task = Task.objects.get(label='chercher le pain')
>>> task
<Task: chercher le pain>

```

**Attention** si la fonction _get()_ peut lever des exceptions dans au moins 2 cas :

* si le get() ne trouve pas la ligne (exception Entite.[DoesNotExist](https://docs.djangoproject.com/en/2.2/ref/models/instances/#django.db.models.Model.DoesNotExist))
* si le get() renvoie plus d'une ligne (exception Entite.[MultipleObjectsReturned](https://docs.djangoproject.com/en/2.2/ref/exceptions/#django.core.exceptions.MultipleObjectsReturned)

```python
>>> task = Task.objects.get(label='chercher le pai')
Traceback (most recent call last):
  File "/usr/local/lib/python3.7/code.py", line 90, in runcode
    exec(code, self.locals)
  File "<console>", line 1, in <module>
  File "/usr/local/lib/python3.7/site-packages/django/db/models/manager.py", line 82, in manager_method
    return getattr(self.get_queryset(), name)(*args, **kwargs)
  File "/usr/local/lib/python3.7/site-packages/django/db/models/query.py", line 408, in get
    self.model._meta.object_name
api_projets.models.DoesNotExist: Task matching query does not exist.
``` 

##### Filtrage avec des condition(s) <a name="filter"></a>

Des **filtres** un peu plus évolués avec la fonction _filter()_ : représente le *WHERE* d'une requête SQL, renvoie les lignes correspondent au critère ou [] si rien n'est trouvé

Les tâches qui contient "pain" dans leur _label_

```python
>>> tasks = Task.objects.filter(label__contains='pain')
>>> tasks
<QuerySet [<Task: chercher le pain>]>
``` 

Affichons la requête qui est générée par l'ORM 

```python
>>> tasks = Task.objects.filter(label__contains='pain')
>>> print(tasks.query)
SELECT "api_projets_task"."id", "api_projets_task"."dt_created", "api_projets_task"."dt_updated", "api_projets_task"."label", "api_projets_task"."done", "api_projets_task"."list_id" FROM "api_projets_task" WHERE "api_projets_task"."label"::text LIKE %pain%
```

il est toujours intéressant de voir la requête générée pour des requêtes complexes ou voir si on a bien ce qu'il faudrait.

Quelques **mots clés** sur les champs textes :

* **startswith** : correspond à _champ_ LIKE '%valeur'
* **contains** ou **icontains** : correspond à _champ_ LIKE '%valeur%', le **i** signifie de ne pas prendre en compte la casse ('Pain' et 'pain' renverront la même chose)
* _champ_='valeur' ou **exact** ou **iexact** : correspond à _champ_='valeur', le **i** permet de ne pas prendre en compte la casse
* on peut rajouter **unaccent** pour que la recherche ne soit pas sensible aux accents, par exemple :

```python
>>> tasks = Task.objects.filter(label__unaccent__icontains='elec')
>>> tasks
<QuerySet [<Task: élection du pain>]>
```

la requête générée :

```python
>>> print(tasks.query)
SELECT "api_projets_task"."id", "api_projets_task"."dt_created", "api_projets_task"."dt_updated", "api_projets_task"."label", "api_projets_task"."done", "api_projets_task"."list_id" FROM "api_projets_task" WHERE UPPER(UNACCENT("api_projets_task"."label"
)::text) LIKE '%' || UPPER(REPLACE(REPLACE(REPLACE((UNACCENT(elec)), E'\\', E'\\\\'), E'%', E'\\%'), E'_', E'\\_')) || '%'
```

On peut cumuler les filtres, par défaut, cela correspondra à un **ET**, par exemple les tâches commençant par 'elec' et ayant l'id = 1, on affiche la requête SQL générée

```python
>>> tasks = Task.objects.filter(label__unaccent__istartswith='elec', pk=1)
>>> print(tasks.query)
SELECT "api_projets_task"."id", "api_projets_task"."dt_created", "api_projets_task"."dt_updated", "api_projets_task"."label", "api_projets_task"."do
ne", "api_projets_task"."list_id" FROM "api_projets_task" WHERE (UPPER(UNACCENT("api_projets_task"."label")::text) LIKE UPPER(REPLACE(REPLACE(REPLAC
E((UNACCENT(elec)), E'\\', E'\\\\'), E'%', E'\\%'), E'_', E'\\_')) || '%' AND "api_projets_task"."id" = 1)
```

si l'on souhaite faire des **OU** ou des **not** (n'est pas égal) , on utilisera les [Q expressions](https://docs.djangoproject.com/en/3.0/topics/db/queries/#complex-lookups-with-q-objects), par exemple, les tâches commençant par "elec" ou d'id 2, il suffira d'utiliser l'opérateur **|** dans la fonction filter()

```python
>>> commencepar = Q(label__unaccent__istartswith='label')
>>> id = Q(pk=2)
>>> tasks = Task.objects.filter(commencepar | id)
>>> print(tasks.query)
SELECT "api_projets_task"."id", "api_projets_task"."dt_created", "api_projets_task"."dt_updated", "api_projets_task"."label", "api_projets_task"."do
ne", "api_projets_task"."list_id" FROM "api_projets_task" WHERE (UPPER(UNACCENT("api_projets_task"."label")::text) LIKE UPPER(REPLACE(REPLACE(REPLAC
E((UNACCENT(label)), E'\\', E'\\\\'), E'%', E'\\%'), E'_', E'\\_')) || '%' OR "api_projets_task"."id" = 2)
```

la même requête mais avec **n'ayant pas** (le ~ est alors utilisé) l'id 2

```python
>>> tasks = Task.objects.filter(commencepar | ~Q(id=2))
>>> print(tasks.query)
SELECT "api_projets_task"."id", "api_projets_task"."dt_created", "api_projets_task"."dt_updated", "api_projets_task"."label", "api_projets_task"."do
ne", "api_projets_task"."list_id" FROM "api_projets_task" WHERE (UPPER(UNACCENT("api_projets_task"."label")::text) LIKE UPPER(REPLACE(REPLACE(REPLAC
E((UNACCENT(label)), E'\\', E'\\\\'), E'%', E'\\%'), E'_', E'\\_')) || '%' OR NOT ("api_projets_task"."id" = 2))

```

##### Optimisations <a name="optimisations"></a>

Comme tout ORM, on retrouve sur l'ORM de Django, le problème du **SELECT N+1** qu'il faut se soucier pour ne pas se trouver avec des centaines de requêtes inutilement exécutées.

Le SELECT N+1 c'est quoi ? par défaut, les queryset sont différées et leurs attributs vers les entités rattachées aussi : autrement dit, dans notre exemple, si nous avons une liste de *Task* et que nous souhaitons afficher la *TodoList* à laquelle elle est rattachée, sur une itération d'une liste de Task, le fait d'afficher task.list.label , cela déclenchera une requête à chaque fois pour interroger la liste associée.

Par exemple, le code suivant déclenche au print un "SELECT" pour joindre Task et TodoList et obtenir l'objet, ce qui peut vite devenir une catastrophe sur un schéma de données bien plus complexe.

```python
>>> tasks = Task.objects.all()
>>> for task in tasks:
>>>     print(tasks.list.label)
```

en activant l'affichage des requêtes de **shell_plus** (*python3 manage.py shell_plus --print-sql*), on aura le résultat suivant qui montre qu'une requête est exécutée vers TodoList pour chaque Task itérée :

```python
SELECT "api_projets_task"."id",
       "api_projets_task"."dt_created",
       "api_projets_task"."dt_updated",
       "api_projets_task"."label",
       "api_projets_task"."done",
       "api_projets_task"."list_id"
  FROM "api_projets_task"

Execution time: 0.000945s [Database: default]

SELECT "api_projets_todolist"."id",
       "api_projets_todolist"."dt_created",
       "api_projets_todolist"."dt_updated",
       "api_projets_todolist"."label"
  FROM "api_projets_todolist"
 WHERE "api_projets_todolist"."id" = 1

Execution time: 0.000498s [Database: default]

Liste 1
SELECT "api_projets_todolist"."id",
       "api_projets_todolist"."dt_created",
       "api_projets_todolist"."dt_updated",
       "api_projets_todolist"."label"
  FROM "api_projets_todolist"
 WHERE "api_projets_todolist"."id" = 2

Execution time: 0.000527s [Database: default]

Liste 2
SELECT "api_projets_todolist"."id",
       "api_projets_todolist"."dt_created",
       "api_projets_todolist"."dt_updated",
       "api_projets_todolist"."label"
  FROM "api_projets_todolist"
 WHERE "api_projets_todolist"."id" = 1

Execution time: 0.000489s [Database: default]

Liste 1
SELECT "api_projets_todolist"."id",
       "api_projets_todolist"."dt_created",
       "api_projets_todolist"."dt_updated",
       "api_projets_todolist"."label"
  FROM "api_projets_todolist"
 WHERE "api_projets_todolist"."id" = 1

Execution time: 0.000445s [Database: default]

Liste 1
SELECT "api_projets_todolist"."id",
       "api_projets_todolist"."dt_created",
       "api_projets_todolist"."dt_updated",
       "api_projets_todolist"."label"
  FROM "api_projets_todolist"
 WHERE "api_projets_todolist"."id" = 2

Execution time: 0.000495s [Database: default]

Liste 2
```

Pour "précharger" ou du moins avoir une requête qui nous retournera directement les TodoList associées, Django propose 2 fonctions suivant la relation :

* [select_related](https://docs.djangoproject.com/fr/3.0/ref/models/querysets/#select-related) : pour les relations à 1 (ForeignKey)
* [prefetch-related] (https://docs.djangoproject.com/fr/3.0/ref/models/querysets/#prefetch-related) : pour les relations * (définies dans le related_name)

Reprenons notre exemple avec la liste des Task et la TodoList associée (en ForeignKey de Task) en utilisant le select_related()

```python
>>> tasks = Task.objects.select_related('list').all()
>>> for task in tasks:
      print(task.list.label)
```

**LA** requête générée lors de l'accès aux entités pour les afficher :

```python
SELECT "api_projets_task"."id",
       "api_projets_task"."dt_created",
       "api_projets_task"."dt_updated",
       "api_projets_task"."label",
       "api_projets_task"."done",
       "api_projets_task"."list_id",
       "api_projets_todolist"."id",
       "api_projets_todolist"."dt_created",
       "api_projets_todolist"."dt_updated",
       "api_projets_todolist"."label"
  FROM "api_projets_task"
 INNER JOIN "api_projets_todolist"
    ON ("api_projets_task"."list_id" = "api_projets_todolist"."id")

Execution time: 0.001652s [Database: default]

Liste 1
Liste 1
Liste 1
Liste 2
Liste 2
```

C'est déjà mieux. Django a rajouté un INNER JOIN vers TodoList et les attributs de cette dernière dans le SELECT.

Maintenant, passons par les TodoList : liste des todolist et pour chacune, obtenir la liste des tâches associées, on aura alors ce type de requêtes :

```python
>>> todolists = TodoList.objects.all()
>>> for list in todolists:
      for task in list.tasks.all():
         print(task.label)
```

qui va créer comme requêtes, une pour obtenir les TodoList et ensuite une par TodoList trouvée pour obtenir ses Tasks :

```python
SELECT "api_projets_todolist"."id",
       "api_projets_todolist"."dt_created",
       "api_projets_todolist"."dt_updated",
       "api_projets_todolist"."label"
  FROM "api_projets_todolist"

Execution time: 0.000305s [Database: default]

SELECT "api_projets_task"."id",
       "api_projets_task"."dt_created",
       "api_projets_task"."dt_updated",
       "api_projets_task"."label",
       "api_projets_task"."done",
       "api_projets_task"."list_id"
  FROM "api_projets_task"
 WHERE "api_projets_task"."list_id" = 1

Execution time: 0.000573s [Database: default]

Tache 4
Tache 3
Tache 1
SELECT "api_projets_task"."id",
       "api_projets_task"."dt_created",
       "api_projets_task"."dt_updated",
       "api_projets_task"."label",
       "api_projets_task"."done",
       "api_projets_task"."list_id"
  FROM "api_projets_task"
 WHERE "api_projets_task"."list_id" = 2

Execution time: 0.000660s [Database: default]

Tache 5
Tache 2

```

Maintenant avec le prefetch_related() :

```python
>>> todolists = TodoList.objects.prefetch_related('tasks').all()
>>> for list in todolists:
      for task in list.tasks.all():
         print(task.label)
```

qui génère comme requêtes, une pour la liste des TodoList et une pour la liste tâches pour les TodoList (le **in**) :

```python
SELECT "api_projets_todolist"."id",
       "api_projets_todolist"."dt_created",
       "api_projets_todolist"."dt_updated",
       "api_projets_todolist"."label"
  FROM "api_projets_todolist"

Execution time: 0.000574s [Database: default]

SELECT "api_projets_task"."id",
       "api_projets_task"."dt_created",
       "api_projets_task"."dt_updated",
       "api_projets_task"."label",
       "api_projets_task"."done",
       "api_projets_task"."list_id"
  FROM "api_projets_task"
 WHERE "api_projets_task"."list_id" IN (1, 2)

Execution time: 0.000646s [Database: default]

Tache 1
Tache 3
Tache 4
Tache 2
Tache 5
```

ce qui est encore une fois bien bien mieux : moins de requêtes, moins de temps d'exécution.

Dans une prochaine partie, nous nous intéresserons à des aspects un peu plus avancés pour le requêtage (managers, agrégations, fonctions F(), Prefetch et des fonctions utiles) ainsi qu'une introduction à DRF.