164 votes

Enchaînement de plusieurs filter() dans Django, est-ce un bug ?

J'ai toujours pensé que l'enchaînement de plusieurs appels à filter() dans Django était toujours la même chose que de les rassembler en un seul appel.

# Equivalent
Model.objects.filter(foo=1).filter(bar=2)
Model.objects.filter(foo=1,bar=2)

mais j'ai rencontré dans mon code un queryset compliqué où ce n'est pas le cas

class Inventory(models.Model):
    book = models.ForeignKey(Book)

class Profile(models.Model):
    user = models.OneToOneField(auth.models.User)
    vacation = models.BooleanField()
    country = models.CharField(max_length=30)

# Not Equivalent!
Book.objects.filter(inventory__user__profile__vacation=False).filter(inventory__user__profile__country='BR')
Book.objects.filter(inventory__user__profile__vacation=False, inventory__user__profile__country='BR')

Le code SQL généré est le suivant

SELECT "library_book"."id", "library_book"."asin", "library_book"."added", "library_book"."updated" FROM "library_book" INNER JOIN "library_inventory" ON ("library_book"."id" = "library_inventory"."book_id") INNER JOIN "auth_user" ON ("library_inventory"."user_id" = "auth_user"."id") INNER JOIN "library_profile" ON ("auth_user"."id" = "library_profile"."user_id") INNER JOIN "library_inventory" T5 ON ("library_book"."id" = T5."book_id") INNER JOIN "auth_user" T6 ON (T5."user_id" = T6."id") INNER JOIN "library_profile" T7 ON (T6."id" = T7."user_id") WHERE ("library_profile"."vacation" = False  AND T7."country" = BR )
SELECT "library_book"."id", "library_book"."asin", "library_book"."added", "library_book"."updated" FROM "library_book" INNER JOIN "library_inventory" ON ("library_book"."id" = "library_inventory"."book_id") INNER JOIN "auth_user" ON ("library_inventory"."user_id" = "auth_user"."id") INNER JOIN "library_profile" ON ("auth_user"."id" = "library_profile"."user_id") WHERE ("library_profile"."vacation" = False  AND "library_profile"."country" = BR )

Le premier queryset avec le chaînage filter() calls joint deux fois le modèle d'inventaire, créant ainsi un OU entre les deux conditions, tandis que le deuxième jeu de questions ET réunit les deux conditions. Je m'attendais à ce que la première requête fasse également un ET entre les deux conditions. S'agit-il du comportement attendu ou d'un bogue dans Django ?

La réponse à une question connexe Y a-t-il un inconvénient à utiliser ".filter().filter().filter()..." dans Django ? semble indiquer que les deux ensembles de questions devraient être équivalents.

155voto

Timmy O'Mahony Points 23071

D'après ce que j'ai compris, ils sont subtilement différents de par leur conception (et je suis tout à fait ouvert à toute correction) : filter(A, B) filtrera d'abord selon A puis sous-filtrera selon B, tandis que filter(A).filter(B) renverra une ligne correspondant à A "et" une ligne potentiellement différente correspondant à B.

Regardez l'exemple ici :

https://docs.djangoproject.com/en/dev/topics/db/queries/#spanning-multi-valued-relationships

en particulier :

Tout ce qui se trouve dans un seul appel à filter() est appliqué simultanément pour filtrer les éléments correspondant à toutes ces exigences. Des appels successifs à filter() restreignent davantage l'ensemble des objets

...

Dans ce deuxième exemple (filter(A).filter(B)), le premier filtre a restreint l'ensemble des questions à (A). Le second filtre restreint l'ensemble des blogs à ceux qui sont également (B). Les entrées sélectionnées par le second filtre peuvent ou non être les mêmes que celles du premier filtre.

110voto

Kevin_wyx Points 933

Ces deux types de filtrage sont équivalents dans la plupart des cas, mais lorsqu'il s'agit d'interroger des objets basés sur des champs ForeignKey ou ManyToManyField, ils sont légèrement différents.

Exemples de la documentation .

modèle
Blog to Entry est une relation de type "one-to-many".

from django.db import models

class Blog(models.Model):
    ...

class Entry(models.Model):
    blog = models.ForeignKey(Blog)
    headline = models.CharField(max_length=255)
    pub_date = models.DateField()
    ...

objets
En supposant qu'il y ait des objets de blog et d'entrée ici.
enter image description here

interrogations

Blog.objects.filter(entry__headline_contains='Lennon', 
    entry__pub_date__year=2008)
Blog.objects.filter(entry__headline_contains='Lennon').filter(
    entry__pub_date__year=2008)  

Pour la 1ère requête (filtre unique), il ne correspond qu'à blog1.

Pour la 2ème requête (filtres enchaînés), il filtre blog1 et blog2.
Le premier filtre restreint l'ensemble de questions à blog1, blog2 et blog5 ; le second filtre restreint l'ensemble de blogs à blog1 et blog2.

Et vous devez vous rendre compte que

Nous filtrons les articles du blog à chaque déclaration de filtre, et non les articles de l'entrée.

Ce n'est donc pas la même chose, car Blog et Entry sont des relations à valeurs multiples.

Référence : https://docs.djangoproject.com/en/1.8/topics/db/queries/#spanning-multi-valued-relationships
Si quelque chose ne va pas, merci de me corriger.

Edit : Changement de la v1.6 en v1.8 car les liens de la 1.6 ne sont plus disponibles.

10voto

Johnny Tsang Points 319

Comme vous pouvez le voir dans les instructions SQL générées, la différence ne réside pas dans le "OR", comme certains pourraient le soupçonner. Il s'agit de la façon dont le WHERE et le JOIN sont placés.

Exemple 1 (même table jointe) :

(exemple de https://docs.djangoproject.com/en/dev/topics/db/queries/#spanning-multi-valued-relationships )

Blog.objects.filter(entry__headline__contains='Lennon', entry__pub_date__year=2008)

Vous obtiendrez ainsi tous les blogs qui ont un avec les deux (entry_ titre _contains='Lennon') AND (entry__pub_date__year=2008), ce qui correspond à ce que l'on peut attendre de cette requête. Résultat : Livre avec {entry.headline : 'Life of Lennon', entry.pub_date : '2008'}

Exemple 2 (enchaîné)

Blog.objects.filter(entry__headline__contains='Lennon').filter(entry__pub_date__year=2008)

Cela couvrira tous les résultats de l'exemple 1, mais générera un peu plus de résultats. Parce qu'il filtre d'abord tous les blogs avec (entry_ titre _contains='Lennon') et ensuite à partir des filtres de résultats (entry__pub_date__year=2008).

La différence est qu'il vous donnera également des résultats comme : Livre avec {entry.headline : ' Lennon ', entry.pub_date : 2000}, {entry.headline : 'Bill', entry.pub_date : 2008 }

Dans votre cas

Je pense que c'est celui-ci qu'il vous faut :

Book.objects.filter(inventory__user__profile__vacation=False, inventory__user__profile__country='BR')

Et si vous voulez utiliser le bloc opératoire, lisez ce qui suit : https://docs.djangoproject.com/en/dev/topics/db/queries/#complex-lookups-with-q-objects

10voto

lbris Points 702

De Documentation sur Django :

Pour gérer ces deux situations, Django dispose d'une méthode cohérente pour traiter les appels à filter(). Tout ce qui se trouve dans un seul appel filter() est appliqué simultanément pour filtrer les éléments correspondant à toutes ces exigences. Les appels successifs de filter() restreignent davantage l'ensemble des objets, mais pour les relations à valeurs multiples, ils s'appliquent à tout objet lié au modèle primaire, et pas nécessairement aux objets qui ont été sélectionnés par un appel de filter() antérieur.

  • Il est clairement dit que des conditions multiples dans une seule et même filter() sont appliquées simultanément. Cela signifie qu'en faisant :

    objs = Mymodel.objects.filter(a=True, b=False)

renverra un ensemble de requêtes contenant les données du modèle Mymodel donde a=True ET b=False .

  • Successif filter() Dans certains cas, le résultat sera le même. Faire :

    objs = Mymodel.objects.filter(a=True).filter(b=False)

renverra un ensemble de requêtes contenant les données du modèle Mymodel donde a=True ET b=False trop. Puisque vous obtenez "d'abord" un queryset avec des enregistrements qui ont a=True et il est réservé aux personnes qui ont b=False en même temps.

  • La différence de chaînage filter() vient lorsqu'il y a multi-valued relations ce qui signifie que vous passez par d'autres modèles (comme l'exemple donné dans la documentation, entre les modèles Blog et Entry). Il est dit que dans ce cas (...) they apply to any object linked to the primary model, not necessarily those objects that were selected by an earlier filter() call.

Ce qui signifie qu'il applique les successifs filter() sur le modèle cible directement, et non sur les filter()

Si je prends l'exemple de la documentation :

Blog.objects.filter(entry__headline__contains='Lennon').filter(entry__pub_date__year=2008)

n'oubliez pas que c'est le modèle Blog qui est filtrée, et non la Entry . Il traitera donc les 2 filter() indépendamment.

Il renverra, par exemple, un ensemble de requêtes contenant des blogs dont les entrées contiennent "Lennon" (même si elles ne datent pas de 2008) et des entrées datant de 2008 (même si leur titre ne contient pas "Lennon").

CETTE RÉPONSE va encore plus loin dans l'explication. Et la question initiale est similaire.

1voto

Tobias Ernst Points 713

Parfois, il n'est pas nécessaire d'assembler plusieurs filtres de cette manière :

def your_dynamic_query_generator(self, event: Event):
    qs \
    .filter(shiftregistrations__event=event) \
    .filter(shiftregistrations__shifts=False)

Le code suivant ne renverrait pas la bonne information.

def your_dynamic_query_generator(self, event: Event):
    return Q(shiftregistrations__event=event) & Q(shiftregistrations__shifts=False)

Ce que vous pouvez faire maintenant, c'est utiliser un filtre de comptage d'annotations.

Dans ce cas, nous comptons toutes les équipes qui appartiennent à un certain événement.

qs: EventQuerySet = qs.annotate(
    num_shifts=Count('shiftregistrations__shifts', filter=Q(shiftregistrations__event=event))
)

Vous pouvez ensuite filtrer par annotation.

def your_dynamic_query_generator(self):
    return Q(num_shifts=0)

Cette solution est également moins coûteuse pour les grands ensembles de questions.

J'espère que cela vous aidera.

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