Łączenie wielu filtrów () w Django, czy to błąd?

106

Zawsze zakładałem, że łączenie w łańcuch wielu wywołań filter () w Django jest zawsze tym samym, co zbieranie ich w jednym wywołaniu.

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

ale natknąłem się na skomplikowany zestaw zapytań w moim kodzie, gdzie tak nie jest

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

Wygenerowany kod SQL to

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 )

Pierwszy zestaw zapytań z połączonymi filter()wywołaniami dwukrotnie łączy się z modelem Inventory, tworząc OR między dwoma warunkami, podczas gdy drugi zestaw zapytań ORAZ te dwa warunki razem. Spodziewałem się, że pierwsze zapytanie będzie również ORAZ dwoma warunkami. Czy jest to oczekiwane zachowanie, czy jest to błąd w Django?

Odpowiedź na pokrewne pytanie Czy używanie funkcji „.filter (). Filter (). Filter () ...” w Django ma wady? wydaje się wskazywać, że dwa zestawy zapytań powinny być równoważne.

gerdemb
źródło

Odpowiedzi:

120

Sposób, w jaki to rozumiem, jest taki, że są one subtelnie różne z założenia (i na pewno jestem otwarty na korektę): filter(A, B)najpierw przefiltruje według A, a następnie podfiltruje według B, podczas gdy filter(A).filter(B)zwróci wiersz pasujący do A 'i' potencjalnie inny wiersz, który pasuje do B.

Spójrz na przykład tutaj:

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

szczególnie:

Wszystko wewnątrz pojedynczego wywołania filter () jest stosowane jednocześnie, aby odfiltrować elementy spełniające wszystkie te wymagania. Kolejne wywołania filter () dodatkowo ograniczają zbiór obiektów

...

W tym drugim przykładzie (filter (A) .filter (B)), pierwszy filtr ograniczył zestaw zapytań do (A). Drugi filtr ograniczył zbiór blogów dalej do tych, które również są (B). Pozycje wybrane przez drugi filtr mogą, ale nie muszą być takie same, jak wpisy w pierwszym filtrze. "

Timmy O'Mahony
źródło
18
Takie zachowanie, choć udokumentowane, wydaje się naruszać zasadę najmniejszego zdziwienia. Wielokrotne operatory AND () są razem, gdy zmienne są na tym samym modelu, ale potem razem, gdy obejmują relacje.
gerdemb
3
Wydaje mi się, że w pierwszym akapicie masz to w niewłaściwy sposób - filtr (A, B) to sytuacja AND ('lennon' AND 2008 w dokumentacji), podczas gdy filter (A) .filter (B) to sytuacja OR ( „lennon” LUB 2008). Ma to sens, gdy spojrzysz na zapytania wygenerowane w pytaniu - przypadek .filter (A) .filter (B) tworzy połączenia dwukrotnie, co daje OR.
Sam,
17
filter (A, B) to filtr AND (A). filtr (B) to OR
WeizhongTu
3
więc further restrictznaczy less restrictive?
boh
7
Ta odpowiedź jest nieprawidłowa. To nie jest „LUB”. To zdanie „Drugi filtr ograniczył zbiór blogów dalej do tych, które są również (B)”. wyraźnie wspomina „które są również (B)”. Jeśli zaobserwujesz zachowanie podobne do OR w tym konkretnym przykładzie, niekoniecznie oznacza to, że możesz uogólnić swoją własną interpretację. Proszę spojrzeć na odpowiedzi „Kevin 3112” i „Johnny Tsang”. Uważam, że to są poprawne odpowiedzi.
osoba
71

Te dwa style filtrowania są w większości przypadków równoważne, ale podczas wykonywania zapytań dotyczących obiektów opartych na ForeignKey lub ManyToManyField są nieco inne.

Przykłady z dokumentacji .

Model
Blog to Entry to relacja jeden do wielu.

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

obiekty
Zakładając, że są tutaj obiekty blogów i wpisów.
wprowadź opis obrazu tutaj

zapytania

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)  
    

W przypadku pierwszego zapytania (jedno filtru) pasuje tylko do blog1.

W przypadku drugiego zapytania (jedno z filtrami łańcuchowymi) odfiltrowuje blog1 i blog2.
Pierwszy filtr ogranicza zestaw zapytań do blog1, blog2 i blog5; drugi filtr ogranicza zbiór blogów dalej do blog1 i blog2.

Powinniście to sobie uświadomić

Filtrujemy elementy bloga za pomocą każdego wyrażenia filtru, a nie elementów wpisu.

Więc to nie to samo, ponieważ Blog i Wpis są relacjami o wielu wartościach.

Źródła: https://docs.djangoproject.com/en/1.8/topics/db/queries/#spanning-multi-valued-relationships
Jeśli coś jest nie tak, proszę mnie poprawić.

Edycja: Zmieniono wersję 1.6 na 1.8, ponieważ linki 1.6 nie są już dostępne.

Kevin_wyx
źródło
3
Wydaje się, że jesteś pomieszany między „dopasowaniami” i „filtrowaniem”. Jeśli utkniesz przy „tym zapytaniu zwraca”, byłoby to o wiele jaśniejsze.
OrangeDog,
7

Jak widać w wygenerowanych instrukcjach SQL, różnica nie polega na „LUB”, jak niektórzy mogą podejrzewać. Tak jest umieszczane GDZIE i DOŁĄCZ.

Przykład1 (ta sama połączona tabela):

(przykład z 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)

To da ci wszystkie blogi, które mają jeden wpis z obydwoma (entry_ headline _contains = 'Lennon') ORAZ (entry__pub_date__year = 2008), czego można się spodziewać po tym zapytaniu. Wynik: Książka z {entry.headline: „Life of Lennon”, entry.pub_date: „2008”}

Przykład 2 (przykuty)

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

To obejmie wszystkie wyniki z przykładu 1, ale wygeneruje nieco więcej wyników. Ponieważ najpierw filtruje wszystkie blogi z (entry_ headline _contains = 'Lennon'), a następnie z filtrów wyników (entry__pub_date__year = 2008).

Różnica polega na tym, że daje to również wyniki takie jak: Książka z {entry.headline: ' Lennon ', entry.pub_date: 2000}, {entry.headline: 'Bill', entry.pub_date: 2008 }

W Twoim przypadku

Myślę, że to ten, którego potrzebujesz:

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

A jeśli chcesz użyć OR, przeczytaj: https://docs.djangoproject.com/en/dev/topics/db/queries/#complex-lookups-with-q-objects

Johnny Tsang
źródło
Drugi przykład nie jest w rzeczywistości prawdziwy. Wszystkie powiązane filtry są stosowane do zapytanych obiektów, tj. Są połączone operatorem logicznym AND w zapytaniu.
Janne
Uważam, że Przykład 2 jest poprawny i faktycznie jest to wyjaśnienie zaczerpnięte z oficjalnych dokumentów Django, o których mowa. Może nie jestem najlepszym tłumaczem i przepraszam za to. Przykład 1 to bezpośrednie ORAZ, jakiego można się spodziewać po normalnym pisaniu SQL. Przykład 1 daje coś takiego: 'SELECT blog JOIN entry WHERE entry.head_line LIKE " Lennon " AND entry.year == 2008 Przykład 2 daje mniej więcej tak:' SELECT blog JOIN entry WHERE entry.head_list LIKE " Lennon " UNION SELECT blog DOŁĄCZ wpis WHERE entry.head_list LIKE " Lennon " '
Johnny Tsang
Sir, ma pan rację. W pośpiechu przegapiłem fakt, że nasze kryteria filtrowania wskazują na relację jeden do wielu, a nie na sam blog.
Janne
0

Czasami nie chcesz łączyć wielu filtrów w ten sposób:

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

A poniższy kod w rzeczywistości nie zwróciłby właściwej rzeczy.

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

Teraz możesz użyć filtru zliczającego adnotacje.

W tym przypadku liczymy wszystkie przesunięcia, które należą do określonego zdarzenia.

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

Następnie możesz filtrować według adnotacji.

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

To rozwiązanie jest również tańsze w przypadku dużych zestawów zapytań.

Mam nadzieję że to pomoże.

Tobias Ernst
źródło