Django et le pouvoir du Q

Ce billet date de plusieurs années, ses informations peuvent être devenues obsolètes.

Non, ne me remerciez pas pour ce titre.

Voici quelques notes en guise de mémo sur des points que j'oublie tout le temps au sujet des objets Q, par ailleurs utilisés très souvent implicitement.

Requêtes complexes avec des objets Q

La documentation de Django nous dit qu'on peut utiliser les opérateurs bit à bit (OMG) avec les objets Q (OMFG) :

Q objects can be combined using the & and | operators. When an operator is used on two Q objects, it yields a new Q object.

Pour obtenir une requête du type (A AND B AND C) OR (D), on peut donc écrire à la main :

from django.db.models import Q
q = (Q(name__startswith='p') & Q(name__endswith='o') & Q(color__equal='red')) | Q(color__equal='green')

Ceci peut devenir très vite prolixe et poser problème si on doit construire la requête de façon dynamique pour des recherches à facettes par exemple.

Voici une façon d'obtenir la même chose dynamiquement sur la base d'une structure lookup_groups qui doit, dans mon cas, se traduire en pseudo-code de la façon suivante [[A, B, C], [D]] => (A AND B AND C) OR (D) :

from functools import reduce

lookup_groups = [
    [('name__startswith', 'p'), ('name__endswith', 'o'), ('color__equal', 'red')],
    [('color__equal', 'green')]
]

def get_q_query(lookup_groups):
    q_query = Q()
    for lookup_group in lookup_groups:
        q_sub_query = reduce(operator.and_, [Q(lookup) for lookup in lookup_group])
        q_query |= q_sub_query
    return q_query

Faisons fi de reduce() et d'operator.and_

reduce(), mais cette merde fonctionnelle est illisible ;)

Je cite Guido (en 2005 !) :

So now reduce(). This is actually the one I've always hated most, because, apart from a few examples involving + or *, almost every time I see a reduce() call with a non-trivial function argument, I need to grab pen and paper to diagram what's actually being fed into that function before I understand what the reduce() is supposed to do. So in my mind, the applicability of reduce() is pretty much limited to associative operators, and in all other cases it's better to write out the accumulation loop explicitly.

Récrivons donc cette ligne pour tendre vers l'explicite :

q_sub_query = Q()
for lookup in lookup_group:
    q_sub_query = operator.and_(q_sub_query, Q(lookup))

On peut même se passer de operator.and_ (qui fournit l'équivalent de l'opérateur & sous forme de fonction) en se servant de Q.add() en mode KISS :

q_sub_query = Q()
for lookup in lookup_group:
    q_sub_query &= Q(lookup)

*args dans la construction d'un QuerySet

L'utilisation des **kwargs est dans les mœurs lors de la construction d'un QuerySet, mais on peut aussi utiliser les *args.

Ces syntaxes sont équivalentes et produisent quasiment le même SQL (l'ordre des clauses peut varier) :

In [1]: from models import MyModel
In [2]: qs1 = MyModel.objects.filter(words__contains='yo', is_online=True)
In [3]: qs2 = MyModel.objects.filter(('words__contains', 'yo'), ('is_online', True))
In [4]: assert list(qs1) == list(qs2)

Ce qui nous permet de répéter le même lookup avec des valeurs différentes sans devoir utiliser le chaînage en évitant une SyntaxError: keyword argument repeated :

In [1]: from models import MyModel
In [2]: qs1 = MyModel.objects.filter(('words__contains', 'le'), ('words__contains', 'la'))

Ressources

Avant Réflexions sur le Viewport Après Peupler un FileField Django par programmation

Tag Kemar Joint