Concatenare più filtri () in Django, è un bug?


103

Ho sempre pensato che concatenare più chiamate filter () in Django fosse sempre lo stesso che raccoglierle in una singola chiamata.

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

ma mi sono imbattuto in un complesso di query nel mio codice in cui questo non è il caso

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

L'SQL generato è

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 )

Il primo set di query con le filter()chiamate concatenate si unisce al modello Inventory due volte creando effettivamente un OR tra le due condizioni, mentre il secondo set di query esegue insieme AND le due condizioni. Mi aspettavo che la prima query avrebbe anche E le due condizioni. È questo il comportamento previsto o è un bug in Django?

La risposta a una domanda correlata C'è uno svantaggio nell'usare ".filter (). Filter (). Filter () ..." in Django? sembra indicare che i due set di query dovrebbero essere equivalenti.

Risposte:


117

Per come la intendo io è che sono sottilmente diversi in base alla progettazione (e sono certamente disponibile per la correzione): filter(A, B)prima filtreranno secondo A e poi subfilteranno secondo B, mentre filter(A).filter(B)restituiranno una riga che corrisponde ad A 'e' potenzialmente diversa riga che corrisponde a B.

Guarda l'esempio qui:

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

in particolar modo:

Tutto all'interno di una singola chiamata filter () viene applicato simultaneamente per filtrare gli elementi che soddisfano tutti quei requisiti. Le successive chiamate filter () restringono ulteriormente l'insieme di oggetti

...

In questo secondo esempio (filter (A) .filter (B)), il primo filtro ha limitato il set di query a (A). Il secondo filtro ha limitato ulteriormente il set di blog a quelli che sono anche (B). Le voci selezionate dal secondo filtro possono o non possono essere le stesse delle voci nel primo filtro. »


18
Questo comportamento, sebbene documentato, sembra violare il principio del minimo stupore. Più AND di filter () insieme quando i campi si trovano sullo stesso modello, ma poi OR insieme quando si estendono le relazioni.
gerdemb

3
Credo che tu abbia sbagliato nel primo paragrafo: il filtro (A, B) è la situazione AND ('lennon' AND 2008 nei documenti), mentre il filtro (A) .filter (B) è la situazione OR ( 'lennon' OR 2008). Questo ha senso quando guardi le query generate nella domanda: il caso .filter (A) .filter (B) crea due volte i join, risultando in un OR.
Sam

17
filtro (A, B) è il filtro AND (A). il filtro (B) è OR
WeizhongTu

3
quindi further restrictsignifica less restrictive?
boh

7
Questa risposta non è corretta. Non è "OR". Questa frase "Il secondo filtro ha limitato ulteriormente il set di blog a quelli che sono anche (B)". menziona chiaramente "che sono anche (B)". Se osservi un comportamento simile a OR in questo esempio specifico, non significa necessariamente che puoi generalizzare la tua interpretazione. Si prega di guardare le risposte di "Kevin 3112" e "Johnny Tsang". Credo che queste siano le risposte corrette.
1man

66

Questi due stili di filtro sono equivalenti nella maggior parte dei casi, ma quando si esegue una query su oggetti in base a ForeignKey o ManyToManyField, sono leggermente diversi.

Esempi dalla documentazione .

Il modello
Blog to Entry è una relazione uno-a-molti.

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

oggetti
Supponendo che ci siano alcuni oggetti blog e voce qui.
inserisci qui la descrizione dell'immagine

interrogazioni

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)  

Per la prima query (filtro singolo), corrisponde solo a blog1.

Per la seconda query (uno dei filtri concatenati), filtra blog1 e blog2.
Il primo filtro limita il set di query a blog1, blog2 e blog5; il secondo filtro restringe ulteriormente il set di blog a blog1 e blog2.

E dovresti rendertene conto

Stiamo filtrando gli elementi del blog con ciascuna istruzione di filtro, non gli elementi della voce.

Quindi, non è la stessa cosa, perché Blog e Entry sono relazioni multivalore.

Riferimento: https://docs.djangoproject.com/en/1.8/topics/db/queries/#spanning-multi-valued-relationships
Se c'è qualcosa che non va, correggimi.

Modifica: modificato dalla v1.6 alla v1.8 poiché i collegamenti 1.6 non sono più disponibili.


3
Sembra che tu sia confuso tra "corrispondenze" e "filtri fuori". Se ti attenessi a "questa query restituisce" sarebbe molto più chiaro.
OrangeDog

7

Come puoi vedere nelle istruzioni SQL generate, la differenza non è l '"OR" come alcuni potrebbero sospettare. È così che vengono posizionati WHERE e JOIN.

Esempio1 (stessa tabella unita):

(esempio da 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)

Questo ti darà tutti i blog che hanno una voce con entrambi (entry_ headline _contains = 'Lennon') E (entry__pub_date__year = 2008), che è quello che ti aspetteresti da questa query. Risultato: libro con {entry.headline: "Life of Lennon", entry.pub_date: "2008"}

Esempio 2 (concatenato)

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

Questo coprirà tutti i risultati dell'esempio 1, ma genererà un risultato leggermente superiore. Perché prima filtra tutti i blog con (entry_ headline _contains = 'Lennon') e poi dai filtri dei risultati (entry__pub_date__year = 2008).

La differenza è che ti darà anche risultati come: Book with {entry.headline: ' Lennon ', entry.pub_date: 2000}, {entry.headline: 'Bill', entry.pub_date: 2008 }

Nel tuo caso

Penso che sia questo di cui hai bisogno:

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

E se vuoi usare OR leggi: https://docs.djangoproject.com/en/dev/topics/db/queries/#complex-lookups-with-q-objects


Il secondo esempio non è effettivamente vero. Tutti i filtri concatenati vengono applicati agli oggetti interrogati, ovvero vengono associati con AND nella query.
Janne

Credo che l'Esempio 2 sia corretto, ed è in realtà una spiegazione presa dai documenti ufficiali di Django, a cui si fa riferimento. Potrei non essere il miglior spiegatore e me ne scuso. L'esempio 1 è un AND diretto come ci si aspetterebbe in una normale scrittura SQL. L'esempio 1 fornisce qualcosa di simile: 'SELEZIONA blog ISCRIVITI alla voce WHERE entry.head_line MI PIACE " Lennon " AND entry.year == 2008 L'esempio 2 dà qualcosa di simile a questo:' SELEZIONA il blog PARTECIPA alla voce WHERE entry.head_list COME " Lennon " SELEZIONA il blog ISCRIVITI alla voce WHERE entry.head_list COME " Lennon " '
Johnny Tsang,

Signore, ha perfettamente ragione. In fretta mi sono perso il fatto che i nostri criteri di filtraggio puntino a una relazione uno-a-molti, non al blog stesso.
Janne

0

A volte non vuoi unire più filtri insieme in questo modo:

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

E il codice seguente in realtà non restituirebbe la cosa corretta.

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

Quello che puoi fare ora è utilizzare un filtro per il conteggio delle annotazioni.

In questo caso contiamo tutti i turni che appartengono a un determinato evento.

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

Successivamente puoi filtrare per annotazione.

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

Questa soluzione è anche più economica su set di query di grandi dimensioni.

Spero che questo ti aiuti.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.