Chainer plusieurs filtres () dans Django, est-ce un bug?

103

J'ai toujours supposé que le chaînage de plusieurs appels filter () dans Django était toujours le même que leur collecte en un seul appel.

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

mais j'ai rencontré un jeu de requêtes compliqué dans mon code 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 SQL généré est

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 jeu de requêtes avec les filter()appels enchaînés rejoint le modèle d'inventaire deux fois en créant effectivement un OU entre les deux conditions, tandis que le second jeu de requêtes ETs les deux conditions ensemble. Je m'attendais à ce que la première requête soit également ET les deux conditions. Est-ce le comportement attendu ou s'agit-il 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 requêtes devraient être équivalents.

Gerdemb
la source

Réponses:

117

La façon dont je comprends les choses est qu'ils sont subtilement différents de par leur conception (et je suis certainement ouvert à la correction): filter(A, B)filtrera d'abord selon A puis sous-filtrera selon B, tandis que filter(A).filter(B)retournera une ligne qui correspond à A 'et' un potentiellement différent ligne qui correspond à B.

Regardez l'exemple ici:

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

particulièrement:

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 à filter () restreignent davantage l'ensemble des objets

...

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

Timmy O'Mahony
la source
18
Ce comportement, bien que documenté, semble violer le principe du moindre étonnement. Plusieurs filtres () ET ensemble lorsque les champs sont sur le même modèle, mais ensuite OU ensemble lorsque des relations s'étendent.
gerdemb
3
Je pense que vous l'avez mal choisi dans le premier paragraphe - filter (A, B) est la situation AND ('lennon' AND 2008 dans la documentation), tandis que filter (A) .filter (B) est la situation OR ( 'lennon' OU 2008). Cela a du sens lorsque vous regardez les requêtes générées dans la question - le cas .filter (A) .filter (B) crée les jointures deux fois, ce qui donne un OU.
Sam
17
filter (A, B) est le filtre ET (A) .filter (B) est OU
WeizhongTu
3
ça further restrictveut dire less restrictive?
boh
7
Cette réponse est incorrecte. Ce n'est pas "OU". Cette phrase «Le deuxième filtre restreint l'ensemble des blogs à ceux qui sont également (B)». mentionne clairement «qui sont aussi (B)». Si vous observez un comportement similaire à OR dans cet exemple spécifique, cela ne signifie pas nécessairement que vous pouvez généraliser votre propre interprétation. Veuillez regarder les réponses de «Kevin 3112» et «Johnny Tsang». Je pense que ce sont les bonnes réponses.
1man le
66

Ces deux styles de filtrage sont équivalents dans la plupart des cas, mais lorsque la requête sur des objets est basée sur ForeignKey ou ManyToManyField, ils sont légèrement différents.

Exemples tirés de la documentation .

Le modèle
Blog to Entry est une relation un-à-plusieurs.

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.
entrez la description de l'image ici

requêtes

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), elle ne correspond qu'à blog1.

Pour la deuxième requête (filtre en chaîne un), il filtre blog1 et blog2.
Le premier filtre restreint l'ensemble de requêtes à blog1, blog2 et blog5; le deuxième filtre restreint l'ensemble des blogs à blog1 et blog2.

Et tu devrais réaliser que

Nous filtrons les éléments du blog avec chaque instruction de filtre, pas les éléments d'entrée.

Donc, ce n'est pas la même chose, car Blog et Entrée sont des relations à valeurs multiples.

Référence: https://docs.djangoproject.com/en/1.8/topics/db/queries/#spanning-multi-valued-relationships
S'il y a quelque chose qui ne va pas, veuillez me corriger.

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

Kevin_wyx
la source
3
Vous semblez être confondu entre «correspondances» et «filtres sortants». Si vous vous en teniez à "cette requête renvoie", ce serait beaucoup plus clair.
OrangeDog
7

Comme vous pouvez le voir dans les instructions SQL générées, la différence n'est pas le "OU" comme certains peuvent le soupçonner. C'est ainsi que sont placés les WHERE et JOIN.

Exemple1 (même table jointe):

(exemple tiré 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)

Cela vous donnera tous les blogs qui ont une entrée avec à la fois (entry_ headline _contains = 'Lennon') ET (entry__pub_date__year = 2008), ce que vous attendez de cette requête. Résultat: Réservez avec {entry.headline: 'Life of Lennon', entry.pub_date: '2008'}

Exemple 2 (chaî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_ headline _contains = 'Lennon') puis à partir des filtres de résultats (entry__pub_date__year = 2008).

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

Dans ton cas

Je pense que c'est celui-ci dont vous avez besoin:

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

Et si vous souhaitez utiliser OU, veuillez lire: https://docs.djangoproject.com/en/dev/topics/db/queries/#complex-lookups-with-q-objects

Johnny Tsang
la source
Le deuxième exemple n'est pas vraiment vrai. Tous les filtres chaînés sont appliqués aux objets interrogés, c'est-à-dire qu'ils sont associés ensemble dans la requête.
Janne
Je pense que l'exemple 2 est correct et qu'il s'agit en fait d'une explication tirée des documents officiels de Django, comme indiqué. Je ne suis peut-être pas le meilleur explicateur et je vous en pardonne. L'exemple 1 est un ET direct comme on peut s'y attendre dans une écriture SQL normale. L'exemple 1 donne quelque chose comme ceci: 'SELECT blog JOIN entry WHERE entry.head_line LIKE " Lennon " AND entry.year == 2008 L'exemple 2 donne quelque chose comme ceci:' SELECT blog JOIN entry WHERE entry.head_list LIKE " Lennon " UNION SELECT blog JOIN entry WHERE entry.head_list LIKE " Lennon " '
Johnny Tsang
Monsieur, vous avez tout à fait raison. En hâte, j'ai manqué le fait que nos critères de filtrage pointent vers une relation un-à-plusieurs, pas vers le blog lui-même.
Janne
0

Parfois, vous ne voulez pas joindre plusieurs filtres ensemble comme ceci:

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

Et le code suivant ne renverrait en fait pas la bonne chose.

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))
)

Ensuite, vous pouvez filtrer par annotation.

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

Cette solution est également moins chère sur les grands ensembles de requêtes.

J'espère que cela t'aides.

Tobias Ernst
la source