• Aucun résultat trouvé

Les requêtes complexes avec Q

Django propose un outil très puissant et utile, nommé Q, pour créer des requêtes complexes sur des modèles. Il se peut que vous vous soyez demandé lors de l'introduction aux requêtes comment formuler des requêtes avec la clause « OU » (OR en anglais ; par exemple, la catégorie de l'article que je recherche doit être « Crêpes OU Bretagne »). Eh bien, c'est ici qu'intervient l'objet Q ! Il permet aussi de créer des requêtes de manière plus dynamique.

Avant tout, prenons un modèle simple pour illustrer nos exemples :

Code : Python

class Eleve(models.Model):

nom = models.CharField(max_length=31) moyenne = models.IntegerField(default=10) def __unicode__(self):

return u"Élève {0} ({1}/20 de moyenne)".format(self.nom,

self.moyenne)

Ajoutons quelques élèves dans la console interactive ( manage.py shell ) :

Code : Python

>>> from test.models import Eleve

>>> Eleve(nom="Mathieu",moyenne=18).save()

>>> Eleve(nom="Maxime",moyenne=7).save() # Le vilain petit canard ! >>> Eleve(nom="Thibault",moyenne=10).save()

>>> Eleve(nom="Sofiane",moyenne=10).save()

Pour créer une requête dynamique, rien de plus simple, nous pouvons formuler une condition avec un objet Q ainsi :

Code : Python

>>> from django.db.models import Q

>>> Q(nom="Maxime")

<django.db.models.query_utils.Q object at 0x222f650> # Nous voyons bien que nous possédons ici un objet de la classe Q

>>> Eleve.objects.filter(Q(nom="Maxime")) [<Eleve: Élève Maxime (7/20 de moyenne)>]

>>> Eleve.objects.filter(nom="Maxime") [<Eleve: Élève Maxime (7/20 de moyenne)>]

En réalité, les deux dernières requêtes sont équivalentes.

Quel intérêt d'utiliser Q dans ce cas ?

Comme dit plus haut, il est possible de construire une clause « OU » à partir de Q :

Code : Python

Eleve.objects.filter(Q(moyenne__gt=16) | Q(moyenne__lt=8)) # Nous prenons les moyennes strictement au-dessus de 16 ou en dessous de 8 [<Eleve: Élève Mathieu (18/20 de moyenne)>, <Eleve: Élève Maxime

(7/20 de moyenne)>]

L'opérateur | est généralement connu comme l'opérateur de disjonction (« OU ») dans l'algèbre de Boole, il est repris ici par Django pour désigner cette fois l'opérateur « OR » du langage SQL.

Sachez qu'il est également possible d'utiliser l'opérateur & pour signifier « ET » :

Code : Python

>>> Eleve.objects.filter(Q(moyenne=10) & Q(nom="Sofiane")) [<Eleve: Élève Sofiane (10/20 de moyenne)>]

Néanmoins, cet opérateur n'est pas indispensable, car il suffit de séparer les objets Q avec une virgule, le résultat est identique :

Code : Python

>>> Eleve.objects.filter(Q(moyenne=10),Q(nom="Sofiane")) [<Eleve: Élève Sofiane (10/20 de moyenne)>]

Il est aussi possible de prendre la négation d'une condition. Autrement dit, demander la condition inverse (« NOT » en SQL). Cela se fait en faisant précéder un objet Q dans une requête par le caractère ~.

Code : Python

>>> Eleve.objects.filter(Q(moyenne=10),~Q(nom="Sofiane")) [<Eleve: Élève Thibault (10/20 de moyenne)>]

Pour aller plus loin, construisons quelques requêtes dynamiquement !

Tout d'abord, il faut savoir qu'un objet Q peut se construire de la façon suivante : Q(('moyenne',10)), ce qui est identique à Q(moyenne=10).

Quel intérêt ? Imaginons que nous devions obtenir les objets qui remplissent une des conditions dans la liste suivante :

Code : Python

conditions = [ ('moyenne',15), ('nom','Thibault'), ('moyenne',18) ]

Nous pouvons construire plusieurs objets Q de la manière suivante :

Code : Python

objets_q = [Q(x) for x in conditions] et les incorporer dans une requête ainsi (avec une clause « OU ») :

Code : Python

import operator

Eleve.objects.filter(reduce(operator.or_, objets_q))

(15/20 de moyenne)>]

Que sont reduce et operator.or_ ?

reduce est une fonction par défaut de Python qui permet d'appliquer une fonction à plusieurs valeurs successivement. Petit exemple pour comprendre plus facilement :

reduce(lambda x, y: x+y, [1, 2, 3, 4, 5]) va calculer ((((1+2)+3)+4)+5), donc 15. La même chose sera faite ici, mais avec l'opérateur « OU » qui est accessible depuis operator.or_. En réalité, Python va donc faire :

Code : Python

Eleve.objects.filter(objets_q[0] | objets_q[1] | objets_q[2])

C'est une méthode très puissante et très pratique !

L'agrégation

Il est souvent utile d'extraire une information spécifique à travers plusieurs entrées d'un seul et même modèle. Si nous reprenons nos élèves de la sous-partie précédente, leur professeur aura plus que probablement un jour besoin de calculer la moyenne globale des élèves. Pour ce faire, Django fournit plusieurs outils qui permettent de tels calculs très simplement. Il s'agit de la méthode d'agrégation.

En effet, si nous voulons obtenir la moyenne des moyennes de nos élèves (pour rappel, Mathieu (moyenne de 18), Maxime (7), Thibault (10) et Sofiane(10)), nous pouvons procéder à partir de la méthode aggregate :

Code : Python

from django.db.models import Avg

>>> Eleve.objects.aggregate(Avg('moyenne')) {'moyenne__avg': 11.25}

En effet, (18+7+10+10)/4 = 11,25 !

Cette méthode prend à chaque fois une fonction spécifique fournie par Django, comme Avg (pour Average, signifiant « moyenne » en anglais) et s'applique sur un champ du modèle. Cette fonction va ensuite parcourir toutes les entrées du modèle et effectuer les calculs propres à celle-ci.

Notons que la valeur retournée par la méthode est un dictionnaire, avec à chaque fois une clé générée automatiquement à partir du nom de la colonne utilisée et de la fonction appliquée (nous avons utilisé la fonction Avg dans la colonne 'moyenne', Django renvoie donc 'moyenne__avg'), avec la valeur calculée correspondante (ici 11,25 donc).

Il existe d'autres fonctions comme Avg, également issues de django.db.models, dont notamment : Max : prend la plus grande valeur ;

Min : prend la plus petite valeur ; Count : compte le nombre d'entrées.

Il est même possible d'utiliser plusieurs de ces fonctions en même temps :

Code : Python

>>> Eleve.objects.aggregate(Avg('moyenne'), Min('moyenne'), Max('moyenne'))

Si vous souhaitez préciser une clé spécifique, il suffit de la faire précéder de la fonction :

Code : Python

>>> Eleve.objects.aggregate(Moyenne=Avg('moyenne'), Minimum=Min('moyenne'), Maximum=Max('moyenne')) {'Minimum': 7, 'Moyenne': 11.25, 'Maximum': 18}

Bien évidemment, il est également possible d'appliquer une agrégation sur un QuerySet obtenu par la méthode filter par exemple :

Code : Python

>>>

Eleve.objects.filter(nom__startswith="Ma").aggregate(Avg('moyenne'), Count('moyenne'))

{'moyenne__count': 2, 'moyenne__avg': 12.5}

Étant donné qu'il n'y a que Mathieu et Maxime comme prénoms qui commencent par « Ma », uniquement ceux-ci seront sélectionnés, comme l'indique moyenne__count.

En réalité, la fonction Count est assez inutile ici, d'autant plus qu'une méthode pour obtenir le nombre d'entrées dans un QuerySet existe déjà :

Code : Python

>>> Eleve.objects.filter(nom__startswith="Ma").count()

2

Cependant, cette fonction peut se révéler bien plus intéressante lorsque nous l'utilisons avec des liaisons entre modèles. Pour ce faire, ajoutons un autre modèle :

Code : Python

class Cours(models.Model):

nom = models.CharField(max_length=31) eleves = models.ManyToManyField(Eleve) def __unicode__(self):

return self.nom

Créons deux cours :

Code : Python

>>> c1 = Cours(nom="Maths")

>>> c1.save()

>>> c1.eleves.add(*Eleve.objects.all())

>>> c2 = Cours(nom="Anglais")

>>> c2.save()

>>> c2.eleves.add(*Eleve.objects.filter(nom__startswith="Ma"))

Il est tout à fait possible d'utiliser les agrégations depuis des liaisons comme une ForeignKey, ou comme ici avec un ManyToManyField :

Code : Python

>>> Cours.objects.aggregate(Max("eleves__moyenne")) {'eleves__moyenne__max': 18}

Nous avons été chercher la meilleure moyenne parmi les élèves de tous les cours enregistrés. Il est également possible de compter le nombre d'affiliations à des cours :

Code : Python

>>> Cours.objects.aggregate(Count("eleves")) {'eleves__count': 6}

En effet, nous avons 6 « élèves », à savoir 4+2, car Django ne vérifie pas si un élève est déjà dans un autre cours ou non. Pour terminer, abordons une dernière fonctionnalité utile. Il est possible d'ajouter des attributs à un objet selon les objets auxquels il est lié. Nous parlons d'annotation. Exemple :

Code : Python

>>>

Cours.objects.annotate(Avg("eleves__moyenne"))[0].eleves__moyenne__avg

11.25

Un nouvel attribut a été créé. Au lieu d'être retournées dans un dictionnaire, les valeurs sont désormais directement ajoutées à l'objet lui-même. Il est bien évidemment possible de redéfinir le nom de l'attribut comme vu précédemment :

Code : Python

>>>

Cours.objects.annotate(Moyenne=Avg("eleves__moyenne"))[1].Moyenne

12.5

Et pour terminer en beauté, il est même possible d'utiliser l'attribut créé dans des méthodes du QuerySet comme filter, exclude ou order_by ! Par exemple :

Code : Python

>>>

Cours.objects.annotate(Moyenne=Avg("eleves__moyenne")).filter(Moyenne__gte=12) [<Cours: Anglais>]

En définitive, l'agrégation et l'annotation sont des outils réellement puissants qu'il ne faut pas hésiter à utiliser si l'occasion se présente !