191 votes

Dans Django, comment filtrer un QuerySet avec des recherches de champs dynamiques ?

Étant donné une classe :

from django.db import models

class Person(models.Model):
    name = models.CharField(max_length=20)

Est-il possible, et si oui comment, d'avoir un QuerySet qui filtre sur la base d'arguments dynamiques ? Par exemple :

 # Instead of:
 Person.objects.filter(name__startswith='B')
 # ... and:
 Person.objects.filter(name__endswith='B')

 # ... is there some way, given:
 filter_by = '{0}__{1}'.format('name', 'startswith')
 filter_value = 'B'

 # ... that you can run the equivalent of this?
 Person.objects.filter(filter_by=filter_value)
 # ... which will throw an exception, since `filter_by` is not
 # an attribute of `Person`.

365voto

Daniel Naab Points 9857

L'expansion des arguments de Python peut être utilisée pour résoudre ce problème :

kwargs = {
    '{0}__{1}'.format('name', 'startswith'): 'A',
    '{0}__{1}'.format('name', 'endswith'): 'Z'
}

Person.objects.filter(**kwargs)

Il s'agit d'un idiome Python très courant et utile.

7 votes

Juste un petit conseil : assurez-vous que les chaînes de caractères dans les kwargs sont de type str et non unicode, sinon filter() va râler.

0 votes

Merci Daniel ! Cela m'a aidé. Comment cela s'appelle-t-il en Python ? Expansion d'argument ? Je ne l'ai pas trouvé dans la documentation.

1 votes

@santiagobasulto On parle aussi d'emballage/déballage de paramètres, et de leurs variantes.

8voto

shacker Points 3348

Un exemple simplifié :

Dans une application de sondage Django, je voulais une liste de sélection HTML montrant les utilisateurs enregistrés. Mais comme nous avons 5000 utilisateurs enregistrés, j'avais besoin d'un moyen de filtrer cette liste en fonction de critères de requête (par exemple, seulement les personnes qui ont terminé un certain atelier). Pour que l'élément d'enquête soit réutilisable, il fallait que la personne créant la question d'enquête puisse attacher ces critères à cette question (je ne voulais pas coder en dur la requête dans l'application).

La solution que j'ai trouvée n'est pas entièrement conviviale (elle nécessite l'aide d'un technicien pour créer la requête), mais elle résout le problème. Lors de la création de la question, l'éditeur peut entrer un dictionnaire dans un champ personnalisé, par ex :

{'is_staff':True,'last_name__startswith':'A',}

Cette chaîne est stockée dans la base de données. Dans le code de la vue, elle revient en tant que self.question.custom_query . La valeur de cela est une chaîne de caractères qui mira comme un dictionnaire. Nous le transformons en un réel avec eval() et ensuite l'insérer dans le queryset avec **kwargs :

kwargs = eval(self.question.custom_query)
user_list = User.objects.filter(**kwargs).order_by("last_name")

0 votes

Je me demande ce qu'il faudrait faire pour créer un ModelField/FormField/WidgetField personnalisé qui implémente le comportement permettant à l'utilisateur, du côté de l'interface graphique, de "construire" une requête, sans jamais voir le texte réel, mais en utilisant une interface pour le faire. Cela semble être un projet intéressant...

1 votes

T. Stone - J'imagine qu'il serait facile de construire un tel outil de manière simpliste si les modèles à interroger étaient simples, mais très difficile de le faire de manière approfondie en exposant toutes les options possibles, surtout si les modèles sont complexes.

6 votes

-1 appel eval() lors de l'importation d'un utilisateur est une mauvaise idée, même si vous faites entièrement confiance à vos utilisateurs. Un champ JSON serait une meilleure idée ici.

7voto

Branko Radojevic Points 428

En plus de la réponse qui a fait quelques demandes pour d'autres éléments de code, j'en ajoute un que j'utilise dans mon code en utilisant Q. Disons que dans ma demande je peux avoir ou non des champs comme :

publisher_id
date_from
date_until

Ces champs peuvent apparaître dans la requête mais ils peuvent aussi être manqués. Voici comment je construis des filtres basés sur ces champs dans une requête agrégée qui ne peut pas être filtrée davantage après l'exécution de la requête initiale :

# prepare filters to apply to queryset
filters = {}
if publisher_id:
    filters['publisher_id'] = publisher_id
if date_from:
    filters['metric_date__gte'] = date_from
if date_until:
    filters['metric_date__lte'] = date_until

filter_q = Q(**filters)

queryset = Something.objects.filter(filter_q)...

J'espère que cela vous aidera, car j'ai passé pas mal de temps à déterrer tout ça.

5voto

Brent81 Points 400

Django.db.models.Q est exactement ce que vous voulez à la manière de Django.

12 votes

Pourriez-vous (ou quelqu'un d'autre) fournir un exemple de l'utilisation des objets Q dans l'utilisation des noms de champs dynamiques ?

4 votes

C'est la même chose que dans La réponse de Daniel Naab La seule différence est que vous passez les arguments dans le constructeur de l'objet Q. Q(**filters) Si vous voulez construire dynamiquement des objets Q, vous pouvez les placer dans une liste et utiliser la fonction .filter(*q_objects) ou utiliser les opérateurs bit à bit pour combiner les objets Q.

11 votes

Cette réponse devrait vraiment inclure un exemple d'utilisation de Q pour résoudre le problème du PO.

-3voto

S.Lott Points 207588

Un formulaire de recherche très complexe indique généralement qu'un modèle plus simple tente de s'échapper.

Comment, exactement, pensez-vous obtenir les valeurs du nom de la colonne et de l'opération ? Où obtenez-vous les valeurs de 'name' un 'startswith' ?

 filter_by = '%s__%s' % ('name', 'startswith')
  1. Un formulaire de "recherche" ? Vous allez quoi ? choisir le nom dans une liste de noms ? Choisir l'opération dans une liste d'opérations ? Bien qu'il soit ouvert, la plupart des gens trouvent cela déroutant et difficile à utiliser.

    Combien de colonnes disposent de tels filtres ? 6 ? 12 ? 18 ?

    • Quelques-uns ? Une liste de sélection complexe n'a pas de sens. Quelques champs et quelques instructions "si" suffisent.
    • Un grand nombre ? Votre modèle ne semble pas correct. Il semble que le "champ" soit en fait une clé pour une ligne d'une autre table, et non une colonne.
  2. Boutons de filtre spécifiques. Attendez... C'est comme ça que fonctionne l'administration de Django. Les filtres spécifiques sont transformés en boutons. Et la même analyse que ci-dessus s'applique. Quelques filtres ont du sens. Un grand nombre de filtres signifie généralement une sorte de violation de la première forme normale.

Un grand nombre de champs similaires signifie souvent qu'il aurait dû y avoir plus de rangées et moins de champs.

0 votes

Merci pour la réponse. Le modèle de données donné en exemple n'est que cela, pour faciliter l'illustration. Je préfère ne pas imaginer que quelqu'un mette quelque chose d'aussi odieux dans un projet réel ;) Je veux découpler les relations génériques et factoriser une logique réutilisable.

0 votes

Django déjà est générique. Écrire des choses plus génériques au dessus de Django est un peu trop générique. Ma recommandation est d'implémenter simplement votre application, en évitant de sur-généraliser un framework déjà générique.

13 votes

Sauf votre respect, il est présomptueux de faire des recommandations sans rien savoir de la conception. La "simple mise en œuvre" de cette application nécessiterait des fonctions astronomiques (>200 applications ^21 foos) pour répondre aux exigences. Vous interprétez l'objectif et l'intention dans l'exemple ; vous ne devriez pas le faire.)

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X