web-dev-qa-db-fra.com

Enchaînant plusieurs filtres () dans Django, s'agit-il d'un bogue?

J'ai toujours supposé que l'enchaînement de plusieurs appels filter () dans Django était toujours le même que de les collecter 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 queryset 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 ensemble de requêtes avec les appels filter() enchaînés rejoint le modèle d'inventaire à deux reprises, créant ainsi un OR entre les deux conditions, tandis que le second ensemble de requêtes AND combine les deux conditions. Je m'attendais à ce que La première requête serait également ET les deux conditions: s'agit-il du comportement attendu ou s'agit-il d'un bogue dans Django?

La réponse à une question connexe Y at-il un inconvénient à utiliser ".filter (). Filter (). Filter () ..." dans Django? semble indiquer que les deux ensembles de requêtes doivent être équivalents.

77
user27478

D'après ce que je comprends, ils sont légèrement différents par conception (et je suis certainement ouvert à la correction): filter(A, B) va d'abord filtrer en fonction de A, puis sous-filtre en fonction de B, tandis que filter(A).filter(B) retournera une ligne qui correspond à A 'et' une ligne potentiellement différente 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 est contenu 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 d'objets

...

Dans ce deuxième exemple (filtre (A) .filter (B)), le premier filtre limitait le jeu de requêtes à (A). Le second filtre a limité l’ensemble des blogs à ceux qui sont aussi (B). Les entrées sélectionnées par le deuxième filtre peuvent être ou non identiques aux entrées du premier filtre.

86
Timmy O'Mahony

Ces deux styles de filtrage sont équivalents dans la plupart des cas, mais lorsqu'ils interrogent des objets sur ForeignKey ou ManyToManyField, ils sont légèrement différents.

Exemples de la documentation .

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 ici des objets de blog et d'entrée.
enter image description here

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 2ème requête (filtres chaînés), il filtre les blogues1 et blog2.
Le premier filtre limite le jeu de requêtes à blog1, blog2 et blog5; le second filtre restreint l'ensemble des blogs à blog1 et blog2.

Et vous devriez réaliser que

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

Donc, ce n'est pas la même chose, car Blog et Entry sont des relations à plusieurs valeurs.

Référence: https://docs.djangoproject.com/fr/1.8/topics/db/queries/#spanning-multi-valued-relationships
Si quelque chose ne va pas, corrigez-moi.

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

50
Kevin_wyx

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

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

Cela vous donnera tous les blogs qui ont une entrée avec les deux (entrée _ titre _ contient = 'Lennon') ET (entry__pub_date__year = 2008), ce à quoi vous pouvez vous attendre 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ésultat. Puisqu'il filtre d'abord tous les blogs avec (entrée _ titre _ contient = 'Lennon'), puis à partir des filtres de résultat (entrée__pub_date__year = 2008).

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

Dans ton cas

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

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

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

5
Johnny Tsang

Parfois, vous ne voulez pas joindre plusieurs filtres 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 réalité pas la chose correcte.

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

Ce que vous pouvez faire maintenant est d’utiliser un filtre de comptage d’annotation.

Dans ce cas, nous comptons tous les quarts de travail 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.

0
Tobias Ernst