mirror of
https://github.com/ae-utbm/sith.git
synced 2025-07-11 04:19:25 +00:00
Rewrite documentation with MkDocs
This commit is contained in:
280
docs/howto/querysets.md
Normal file
280
docs/howto/querysets.md
Normal file
@ -0,0 +1,280 @@
|
||||
L'ORM de Django est puissant, très puissant, non par parce qu'il
|
||||
est performant (après tout, ce n'est qu'une interface, le gros du boulot,
|
||||
c'est la db qui le fait), mais parce qu'il permet d'écrire
|
||||
de manière relativement simple un grand panel de requêtes.
|
||||
|
||||
De manière générale, puisqu'un ORM est un système
|
||||
consistant à manipuler avec un code orienté-objet
|
||||
une db relationnelle (c'est-à-dire deux paradigmes
|
||||
qui ne fonctionnent absolument pas pareil),
|
||||
on rencontre un des deux problèmes suivants :
|
||||
|
||||
- soit l'ORM n'offre pas assez d'abstraction,
|
||||
auquel cas, quand on veut faire des requêtes
|
||||
plus complexes qu'un `select` avec un `where`,
|
||||
on s'emmêle les pinceaux et on se dit que
|
||||
ça aurait été plus simple de le faire directement
|
||||
en SQL.
|
||||
- soit l'ORM offre trop d'abstraction,
|
||||
auquel cas, on a tendance à ne pas prêter
|
||||
assez attention aux requêtes envoyées en base
|
||||
de données et on finit par se rendre compte
|
||||
que les temps d'attente explosent
|
||||
parce qu'on envoie trop de requêtes.
|
||||
|
||||
Django est dans ce deuxième cas.
|
||||
|
||||
C'est pourquoi nous ne parlerons pas ici
|
||||
de son fonctionnement exact ni de toutes les fonctions
|
||||
que l'on peut utiliser
|
||||
(la doc officielle fait déjà ça mieux que nous),
|
||||
mais plutôt des pièges courants
|
||||
et des astuces pour les éviter.
|
||||
|
||||
## Les `N+1 queries`
|
||||
|
||||
### Le problème
|
||||
|
||||
Normalement, quand on veut récupérer une liste,
|
||||
on fait une requête et c'est fini.
|
||||
Mais des fois, ça n'est pas si simple.
|
||||
Par exemple, supposons que nous voulons
|
||||
récupérer les 100 utilisateurs les plus riches,
|
||||
avec leurs informations client :
|
||||
|
||||
```python
|
||||
from core.models import User
|
||||
|
||||
for user in User.objects.order_by("-customer__amount")[:100]:
|
||||
print(user.customer.amount)
|
||||
```
|
||||
|
||||
Combien de requêtes le bout de code suivant effectue-t-il ?
|
||||
101\.
|
||||
En deux pauvres lignes de code, nous avons demandé
|
||||
à la base de données d'effectuer 101 requêtes.
|
||||
Une requête toute seule n'est déjà une opération anodine,
|
||||
alors je vous laisse imaginer ce que ça donne pour 101.
|
||||
|
||||
Si vous ne comprenez pourquoi ce nombre, c'est très simple :
|
||||
|
||||
- Une requête pour sélectionner nos 100 utilisateurs
|
||||
- Une requête supplémentaire pour récupérer les informations
|
||||
client de chaque utilisateur, soit 100 requêtes.
|
||||
|
||||
En effet, les informations client sont stockées dans une
|
||||
autre table, mais le fait d'établir un lien de clef
|
||||
étrangère permet de manipuler `customer`
|
||||
comme si c'était un membre à part entière de `User`.
|
||||
|
||||
Il est à noter cependant, que Django n'effectue une requête
|
||||
que pour le premier accès à un membre d'une relation
|
||||
de clef étrangère.
|
||||
Toutes les fois suivantes, l'objet est déjà là,
|
||||
et django le récupère :
|
||||
|
||||
```python
|
||||
from core.models import User
|
||||
|
||||
# l'utilisateur le plus riche
|
||||
user = User.objects.order_by("-customer__amount").first() # <-- requête db
|
||||
print(user.customer.amount) # <-- requête db
|
||||
print(user.customer.account_id) # on a déjà récupéré `customer`, donc pas de requête
|
||||
```
|
||||
|
||||
Ce n'est donc pas gravissime si vous faites cette
|
||||
erreur quand vous manipulez un seul objet.
|
||||
En revanche, quand vous en manipulez plusieurs,
|
||||
il faut régler le problème.
|
||||
Pour ça, il y a plusieurs méthodes, en fonction de votre cas.
|
||||
|
||||
### `select_related`
|
||||
|
||||
La méthode la plus basique consiste à annoter le queryset,
|
||||
avec la méthode `select_related()`.
|
||||
En faisant ça, Django fera une jointure sur l'autre table
|
||||
et demandera des informations en plus
|
||||
à la db lors de la requête.
|
||||
|
||||
De la sorte, lorsque vous appellerez le membre relié,
|
||||
les informations seront déjà là.
|
||||
|
||||
```python
|
||||
from core.models import User
|
||||
|
||||
richest = User.objects.order_by("-customer__amount")
|
||||
for user in richest.select_related("customer")[:100]:
|
||||
print(user.customer)
|
||||
```
|
||||
|
||||
Le code ci-dessus effectue une seule requête.
|
||||
Chaque fois qu'on veut accéder à `customer`, c'est bon,
|
||||
ça a déjà été récupéré à travers le `annotate`.
|
||||
|
||||
### `prefetch_related`
|
||||
|
||||
Maintenant, un cas plus compliqué.
|
||||
Supposons que vous ne vouliez pas récupérer des informations
|
||||
reliées par une relation One-to-One,
|
||||
mais par une relation One-to-Many.
|
||||
|
||||
Par exemple, un utilisateur a un seul compte client,
|
||||
mais il peut avoir plusieurs cotisations à son actif.
|
||||
Et dans ces cas-là, `annotate` ne marche plus.
|
||||
En effet, s'il peut exister plusieurs cotisations,
|
||||
comment savoir laquelle on veut ?
|
||||
|
||||
Il faut alors utiliser un `prefetch_related`.
|
||||
C'est un mécanisme un peu différent :
|
||||
au lieu de faire une jointure et d'ajouter les informations
|
||||
voulues dans la même requête, Django va effectuer
|
||||
une deuxième requête pour récupérer les éléments de l'autre table,
|
||||
puis, à partir de ces éléments, peupler la relation
|
||||
de son côté.
|
||||
|
||||
C'est un mécanisme qui peut être un peu coûteux en mémoire
|
||||
et qui demande une deuxième requête,
|
||||
mais qui reste quand même largement préférable
|
||||
à faire N requêtes en plus.
|
||||
|
||||
```python
|
||||
from core.models import User
|
||||
|
||||
for user in User.objects.prefetch_related("subscriptions")[:100]:
|
||||
# c'est bon, la méthode prefetch a récupéré en avance les `subscriptions`
|
||||
print(user.subscriptions.all())
|
||||
```
|
||||
|
||||
!!! danger
|
||||
|
||||
La méthode `prefetch_related` ne marche que si vous
|
||||
utilisez la méthode `all()` pour accéder au membre.
|
||||
Si vous utilisez une autre méthode (comme `filter` ou `annotate`),
|
||||
alors Django effectuera une nouvelle requête,
|
||||
et vous retomberez dans le problème initial.
|
||||
|
||||
```python
|
||||
from core.models import User
|
||||
from django.db.models import Count
|
||||
|
||||
for user in User.objects.prefetch_related("subscriptions")[:100]:
|
||||
# Le prefetch_related ne marche plus !
|
||||
print(user.subscriptions.annotate(count=Count("*")))
|
||||
```
|
||||
|
||||
### Récupérer ce dont vous avez besoin
|
||||
|
||||
Des fois (souvent, même), penser explicitement
|
||||
à la jointure est le meilleur choix.
|
||||
|
||||
En effet, vous remarquerez que dans tous
|
||||
les exemples précédents, nous n'utilisions
|
||||
qu'une partie des informations
|
||||
(par exemple, nous ne récupérions que la somme
|
||||
d'argent sur les comptes, et éventuellement le numéro de compte).
|
||||
|
||||
Nous pouvons utiliser la méthode `annotate`
|
||||
pour spécifier explicitement les données que l'on veut
|
||||
joindre à notre requête.
|
||||
|
||||
Quand nous voulions récupérer les informations utilisateur,
|
||||
nous aurions tout aussi bien pu écrire :
|
||||
|
||||
```python
|
||||
from core.models import User
|
||||
from django.db.models import F
|
||||
|
||||
richest = User.objects.order_by("-customer__amount")
|
||||
for user in richest.annotate(amount=F("customer__amount"))[:100]:
|
||||
print(user.amount)
|
||||
```
|
||||
|
||||
On aurait même pu réorganiser ça :
|
||||
```python
|
||||
|
||||
from core.models import User
|
||||
from django.db.models import F
|
||||
|
||||
richest = User.objects.annotate(amount=F("customer__amount")).order_by("-amount")
|
||||
for user in richest[:100]:
|
||||
print(user.amount)
|
||||
```
|
||||
|
||||
Ça peut sembler moins bien qu'un `select_related`, comme ça.
|
||||
Des fois, c'est en effet moins bien, et des fois c'est mieux.
|
||||
La comparaison est plus évidente avec le `prefetch_related`.
|
||||
|
||||
En effet, quand nous voulions récupérer
|
||||
le nombre de cotisations des utilisateurs,
|
||||
le `prefetch_related` ne marchait plus.
|
||||
Pourtant, nous voulions récupérer une seule information.
|
||||
|
||||
Il aurait donc été suffisant d'écrire :
|
||||
```python
|
||||
from core.models import User
|
||||
from django.db.models import Count
|
||||
|
||||
for user in User.objects.annotate(nb_subscriptions=Count("subscriptions"))[:100]:
|
||||
# Et là ça marche, en une seule requête.
|
||||
print(user.nb_subscriptions)
|
||||
```
|
||||
|
||||
Faire une jointure, c'est normal en SQL.
|
||||
Et pourtant avec Django on les oublie trop facilement.
|
||||
Posez-vous toujours la question des données que vous pourriez
|
||||
avoir besoin d'annoter, et vous éviterez beaucoup d'ennuis.
|
||||
|
||||
## Les aggrégations manquées
|
||||
|
||||
Il arrive souvent que l'on veuille une information qui
|
||||
porte sur un ensemble d'objets de notre db.
|
||||
|
||||
Imaginons par exemple que nous voulons connaitre
|
||||
la somme totale des ventes faites à un comptoir.
|
||||
|
||||
Nous avons tous suivi nos cours de programmation,
|
||||
nous écrivons donc instinctivement :
|
||||
|
||||
```python
|
||||
from counter.models import Counter
|
||||
|
||||
foyer = Counter.objects.get(name="Foyer")
|
||||
total_amount = sum(
|
||||
sale.amount * sale.unit_price
|
||||
for sale in foyer.sellings.all()
|
||||
)
|
||||
```
|
||||
|
||||
On pourrait penser qu'il n'y a pas de problème.
|
||||
Après tout, on ne fait qu'une seule requête.
|
||||
Eh bien si, il y a un problème :
|
||||
on fait beaucoup de choses en trop.
|
||||
|
||||
Concrètement, on demande à la base de données
|
||||
de renvoyer toutes les informations,
|
||||
ce qui rallonge inutilement la durée
|
||||
de l'échange entre le serveur et la db,
|
||||
puis on perd du temps à convertir ces informations
|
||||
en objets Python (opération qui a un coût également),
|
||||
et enfin on reperd du temps à calculer en Python
|
||||
quelque chose que la db aurait pu calculer
|
||||
à notre plus bien plus vite.
|
||||
|
||||
Nous aurions dû aggréger la requête,
|
||||
avec la méthode `aggregate` :
|
||||
|
||||
```python
|
||||
from counter.models import Counter
|
||||
from django.db.models import Sum, F
|
||||
|
||||
foyer = Counter.objects.get(name="Foyer")
|
||||
total_amount = (
|
||||
foyer.sellings.aggregate(amount=Sum(F("amount") * F("unit_price"), default=0))
|
||||
)["amount__sum"]
|
||||
```
|
||||
|
||||
En effectuant cette requête, la base de données nous renverra exactement
|
||||
l'information dont nous avons besoin.
|
||||
Et de notre côté, nous n'aurons pas à faire de traitement en plus.
|
||||
|
||||
|
Reference in New Issue
Block a user