# Documentation Django 5.2.8

# Écriture d’expressions de requêtes personnalisées

Django offre une large variété d’expressions de recherche intégrées pour filtrer les requêtes (par exemple, `exact` et `icontains`). Cette documentation explique comment écrire des expressions personnalisées et comment modifier le comportement d’expressions existantes. Pour la référence des API de recherche, consultez API de référence des expressions de recherche.

## Un exemple d’expression de recherche

Commençons par une petite expression de recherche. Nous allons écrire une expression personnalisée `ne` fonctionnant à l’inverse de `exact`. `Author.objects.filter(name__ne='Jack')` s’exprime comme ceci en SQL :

    "author"."name" <> 'Jack'

Ce code SQL est indépendant de la base de données, nous n’avons donc pas à nous soucier de la gestion de bases de données différentes.

Deux étapes sont nécessaires pour que cela fonctionne. Nous devons d’abord implémenter la recherche, puis nous devons informer Django de sa disponibilité

    from django.db.models import Lookup

    class NotEqual(Lookup):
        lookup_name = "ne"

        def as_sql(self, compiler, connection):
            lhs, lhs_params = self.process_lhs(compiler, connection)
            rhs, rhs_params = self.process_rhs(compiler, connection)
            params = lhs_params + rhs_params
            return "%s <> %s" % (lhs, rhs), params

Pour inscrire la recherche `NotEqual`, il faut appeler `register_lookup` sur la classe de champ pour lequel nous souhaitons rendre disponible cette recherche. Dans ce cas, l’expression peut s’appliquer à toutes les sous-classes de `Field`, c’est pourquoi nous l’inscrivons directement dans `Field`:

    from django.db.models import Field

    Field.register_lookup(NotEqual)

L’enregistrement d’expression peut aussi se faire par la technique de décoration :

    from django.db.models import Field

    @Field.register_lookup
    class NotEqualLookup(Lookup): ...

Nous pouvons dorénavant écrire `champ__ne` pour n’importe quel `champ`. Il faut s’assurer que l’inscription a lieu avant la création de tout jeu de requête utilisant l’expression. Il est possible de placer cette implémentation dans un fichier `models.py` ou d’enregistrer l’expression dans la méthode `ready()` d’une classe `AppConfig`.

En examinant plus précisément l’implémentation, le premier attribut obligatoire est `lookup_name`. Cela permet à l’ORM de savoir comment interpréter `name__ne` et d’utiliser `NotEqual` pour produire le code SQL. Par convention, ces noms sont toujours en minuscules et ne contiennent que des lettres, mais la seule exigence absolue est l’interdiction d’utiliser la chaîne `__`.

Nous devons ensuite définir la méthode `as_sql`. Elle accepte un objet `SQLCompiler` appelé `compiler` ainsi que la connexion de base de données active. Les objets `SQLCompiler` ne sont pas documentés, mais la seule chose à savoir à leur sujet est qu’ils comportent une méthode `compile()` qui renvoie un tuple contenant une chaîne SQL ainsi que les paramètres à insérer dans cette chaîne. Dans la plupart des cas, il n’est pas nécessaire de l’utiliser directement et il suffit alors de la transmettre à `process_lhs()` et `process_rhs()`.

Un objet `Lookup` fonctionne avec deux valeurs, `lhs` et `rhs`, qui signifient côté gauche (« left-hand side ») et côté droite (« right-hand side »). Le côté gauche est normalement une référence de champ, mais cela peut aussi être n’importe quel objet implémentant l’API d’expression de recherche. Le côté droit correspond à la valeur donnée par l’utilisateur. Dans l’exemple `Author.objects.filter(name__ne='Jack')`, le côté gauche est une référence au champ `name` du modèle `Author`, alors que `'Jack'` correspond au côté droit.

Nous appelons `process_lhs` et `process_rhs` pour les convertir en valeurs nécessaires pour le code SQL en utilisant l’objet `compiler` décrit précédemment. Ces méthodes renvoient des tuples contenant le code SQL ainsi que les paramètres à insérer dans ce code, exactement comme nous en avons besoin pour la valeur de retour de notre méthode `as_sql`. Dans l’exemple ci-dessus, `process_lhs` renvoie `('"author"."name"', [])` et `process_rhs` renvoie `('"%s"', ['Jack'])`. Dans cet exemple, le côté gauche ne contient aucun paramètre, mais cela dépend de l’objet concerné, il faut donc tout de même les inclure dans les paramètres que nous renvoyons.

Pour terminer, nous combinons les différentes parties dans une expression SQL avec `<>`, et nous fournissons tous les paramètres pour la requête. Nous renvoyons ensuite un tuple contenant la chaîne SQL produite et ses paramètres.

## Un exemple de transformateur

La recherche personnalisée ci-dessus va très bien, mais dans certains cas, il est souhaitable d’enchaîner des recherches les unes après les autres. Par exemple, supposons que nous construisons une application où nous souhaitons exploiter l’opérateur `abs()`. Nous avons un modèle `Experiment` qui enregistre une valeur de départ, une valeur de fin et la modification (début - fin). Nous aimerions trouver toutes les expériences où la modification est égale à une certaine valeur (`Experiment.objects.filter(change__abs=27)`), ou que la valeur ne dépasse pas un seuil limite (`Experiment.objects.filter(change__abs__lt=27)`).

Note

Cet exemple est un peu tiré par les cheveux, mais il démontre bien l’étendue des fonctionnalités possibles de manière indépendante de la base de données, et sans dupliquer une fonctionnalité déjà présente dans Django.

Nous allons commencer par écrire un transformateur `AbsoluteValue`. Il va faire appel à la fonction SQL `ABS()` pour transformer la valeur avant la comparaison :

    from django.db.models import Transform

    class AbsoluteValue(Transform):
        lookup_name = "abs"
        function = "ABS"

Ensuite, nous allons l’inscrire pour le champ `IntegerField`:

    from django.db.models import IntegerField

    IntegerField.register_lookup(AbsoluteValue)

Nous pouvons maintenant exécuter les requêtes que nous avions en tête. `Experiment.objects.filter(change__abs=27)` générera le code SQL suivant :

    SELECT ... WHERE ABS("experiments"."change") = 27

En utilisant `Transform` au lieu de `Lookup`, cela signifie que nous avons la possibilité d’enchaîner d’autres recherches à la suite. Ainsi, `Experiment.objects.filter(change__abs__lt=27)` générera le code SQL suivant :

    SELECT ... WHERE ABS("experiments"."change") < 27

Notez que dans le cas où aucune autre recherche n’a été ajoutée, Django interprète `change__abs=27` comme `change__abs__exact=27`.

Cela permet aussi d’utiliser le résultat dans les clauses `ORDER BY` et `DISTINCT ON`. Par exemple, `Experiment.objects.order_by('change__abs')` produit :

    SELECT ... ORDER BY ABS("experiments"."change") ASC

Et pour les bases de données qui prennent en charge l’instruction `DISTINCT` pour des champs spécifiques (comme PostgreSQL), `Experiment.objects.distinct('change__abs')` produit :

    SELECT ... DISTINCT ON ABS("experiments"."change")

Lorsqu’il recherche quelles sont les expressions de recherche autorisées après l’application de `Transform`, Django se base sur l’attribut `output_field`. Nous n’avons pas eu besoin de le préciser ici car il n’a pas changé, mais si nous avions appliqué `AbsoluteValue` à un champ représentant un type plus complexe (par exemple un point relatif à une origine ou un nombre complexe), nous aurions pu vouloir indiquer que la transformation renvoie un type `FloatField` dans l’optique de recherches subséquentes. Cela peut se faire en ajoutant un attribut `output_field` à la transformation :

    from django.db.models import FloatField, Transform

    class AbsoluteValue(Transform):
        lookup_name = "abs"
        function = "ABS"

        @property
        def output_field(self):
            return FloatField()

Cela garantit que les recherches suivantes comme dans `abs__lte` se comportent comme elles le feraient avec un champ

## Écriture d’une recherche `abs__lt` efficace

Lors de l’utilisation de l’expression `abs` écrite ci-dessus, le code SQL produit n’utilise pas les index de manière efficace dans certains cas. En particulier, quand on écrit `change__abs__lt=27`, c’est équivalent à `change__gt=-27` AND `change__lt=27` (dans le cas de `lte`, nous pourrions utiliser l’expression SQL `BETWEEN`).

Nous aimerions donc que `Experiment.objects.filter(change__abs__lt=27)` produise le code SQL suivant :

    SELECT .. WHERE "experiments"."change" < 27 AND "experiments"."change" > -27

L’implémentation est :

    from django.db.models import Lookup

    class AbsoluteValueLessThan(Lookup):
        lookup_name = "lt"

        def as_sql(self, compiler, connection):
            lhs, lhs_params = compiler.compile(self.lhs.lhs)
            rhs, rhs_params = self.process_rhs(compiler, connection)
            params = lhs_params + rhs_params + lhs_params + rhs_params
            return "%s < %s AND %s > -%s" % (lhs, rhs, lhs, rhs), params

    AbsoluteValue.register_lookup(AbsoluteValueLessThan)

Plusieurs éléments méritent d’être signalés. Tout d’abord, `AbsoluteValueLessThan` n’appelle pas `process_lhs()`. Au lieu de cela, il omet la transformation de `lhs` effectuée par `AbsoluteValue` et utilise la variable `lhs` originale. C’est-à-dire que nous voulons obtenir `"experiments"."change"` et non `ABS("experiments"."change")`. La référence directe à `self.lhs.lhs` est sûre car `AbsoluteValueLessThan` ne peut être accédée qu’au travers de l’expression `AbsoluteValue`, ce qui veut dire que `lhs` est toujours une instance de `AbsoluteValue`.

Remarquez aussi que comme les deux côtés sont utilisés plusieurs fois dans la requête, les paramètres doivent contenir aussi plusieurs fois `lhs_params` et `rhs_params`.

La requête finale procède à l’inversion (`27` to `-27`) directement dans la base de données. La raison de faire cela est que si `self.rhs` représente autre chose qu’une valeur entière simple (par exemple une référence `F()`), nous ne pouvons pas faire la transformation dans le code Python.

Note

En fait, la plupart des recherches avec `__abs` pourraient être implémentées comme des requêtes d’intervalle comme celle-ci, et pour la plupart des moteurs de base de données, il serait probablement plus judicieux de le faire afin de tirer parti au maximum des index. Cependant, avec PostgreSQL, il est possible d’ajouter un index sur `abs(change)` ce qui permettrait à ces requêtes d’être très efficaces.

## Un exemple de transformateur bilatéral

L’exemple `AbsoluteValue` que nous avons abordé précédemment est une transformation qui s’applique au côté gauche de l’expression. Il peut arriver que l’on veuille appliquer la transformation à la fois aux côtés gauche et droit de l’expression. Par exemple, si vous souhaitez filtrer une requête sur la base de l’égalité des côtés gauche et droit après leur avoir appliqué une fonction SQL quelconque.

Examinons ici les transformations non sensibles à la casse. Cette transformation n’est pas très utile en réalité car Django contient déjà un certain nombre d’expressions de recherche non sensibles à la casse, mais cela fera une excellente démonstration des transformations bilatérales et neutres en terme de moteur de base de données.

Nous définissons une transformation `UpperCase` qui utilise la fonction SQL `UPPER()` pour transformer les valeurs avant leur comparaison. Nous définissons `bilateral = True` pour indiquer que cette transformation doit s’appliquer aux deux côtés `lhs` et `rhs`:

    from django.db.models import Transform

    class UpperCase(Transform):
        lookup_name = "upper"
        function = "UPPER"
        bilateral = True

Puis, occupons-nous de l’enregistrement :

    from django.db.models import CharField, TextField

    CharField.register_lookup(UpperCase)
    TextField.register_lookup(UpperCase)

Dorénavant, la requête `Author.objects.filter(name__upper="doe")` va produire une expression non sensible à la casse comme ceci :

    SELECT ... WHERE UPPER("author"."name") = UPPER('doe')

## Écriture d’implémentations alternatives pour des recherches existantes

Il peut arriver que le code SQL doive être différent d’une base de données à l’autre pour la même opération. Pour cet exemple, nous allons réécrire une implémentation personnalisée pour MySQL de l’opérateur NotEqual. Au lieu de `<>`, nous utiliserons l’opérateur `!=` (en réalité, presque toutes les bases de données acceptent les deux syntaxes, ce qui est le cas pour toutes les bases de données prises en charge officiellement par Django).

Nous pouvons modifier le comportement pour un moteur spécifique en créant une sous-classe de `NotEqual` avec une méthode `as_mysql`:

    class MySQLNotEqual(NotEqual):
        def as_mysql(self, compiler, connection, **extra_context):
            lhs, lhs_params = self.process_lhs(compiler, connection)
            rhs, rhs_params = self.process_rhs(compiler, connection)
            params = lhs_params + rhs_params
            return "%s != %s" % (lhs, rhs), params

    Field.register_lookup(MySQLNotEqual)

Nous pouvons ensuite l’inscrire dans `Field`. Elle prend la place de la classe `NotEqual` d’origine car elle possède le même attribut `lookup_name`.

Lors de la compilation d’une requête, Django cherche d’abord les méthodes `as_%s % connection.vendor` et se rabat ensuite sur `as_sql`. Les noms « vendor » pour les moteurs intégrés sont `sqlite`, `postgresql`, `oracle` et `mysql`.

## Comment Django détermine les recherches et les transformations qui sont utilisées

Dans certains cas, il serait souhaitable de changer dynamiquement l’objet `Transform` ou `Lookup` renvoyé en fonction du nom passé, plutôt que de le corriger. Par exemple, un champ pourrait stocker les coordonnées ou une dimension arbitraire, et vous souhaiteriez autoriser une syntaxe telle que `.filter(coords__x7=4)` pour renvoyer les objets où la 7ème coordonnée a la valeur 4. Pour cela, vous pourriez étendre `get_lookup` avec quelque chose comme :

    class CoordinatesField(Field):
        def get_lookup(self, lookup_name):
            if lookup_name.startswith("x"):
                try:
                    dimension = int(lookup_name.removeprefix("x"))
                except ValueError:
                    pass
                else:
                    return get_coordinate_lookup(dimension)
            return super().get_lookup(lookup_name)

Vous pourriez ensuite définir `get_coordinate_lookup` de manière à ce qu’elle renvoie une sous-classe de `Lookup` gérant la valeur applicable de `dimension`.

Il existe une méthode similaire appelée `get_transform()`. `get_lookup()` doit toujours renvoyer une sous-classe de `Lookup`, et `get_transform()` une sous-classe de `Transform`. Il est important de se rappeler que les objets `Transform` peuvent ensuite être filtrés, alors que les objets `Lookup` ne le peuvent pas.

Lors du filtrage, s’il n’y a qu’un seul nom de recherche restant à résoudre, c’est un `Lookup` qui est recherché. S’il y a plusieurs noms, la recherche visera un `Transform`. Dans la situation où il n’y a qu’un seul nom et qu’un `` Lookup`` n’est pas trouvé, un `Transform` est recherché, puis l’expression de recherche `exact` est appliquée sur ce `Transform`. Toutes les séquences d’appels se terminent toujours par un `Lookup`. Pour clarifier :

- `.filter(myfield__mylookup)` appelle `myfield.get_lookup('mylookup')`.

- `.filter(myfield__mytransform__mylookup)` appelle `myfield.get_transform('mytransform')`, puis `mytransform.get_lookup('mylookup')`.

- `.filter(myfield__mytransform)` appelle d’abord `myfield.get_lookup('mytransform')` qui échouera, il appelle donc en dernier recours `myfield.get_transform('mytransform')` puis `mytransform.get_lookup('exact')`.