Verketten Sie mehrere Filter () in Django. Ist dies ein Fehler?

102

Ich habe immer angenommen, dass das Verketten mehrerer filter () -Aufrufe in Django immer dasselbe ist wie das Sammeln in einem einzigen Aufruf.

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

Ich bin jedoch auf einen komplizierten Abfragesatz in meinem Code gestoßen, bei dem dies nicht der Fall ist

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

Das generierte SQL ist

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 )

Das erste Abfrageset mit den verketteten filter()Aufrufen verbindet das Inventarmodell zweimal und erstellt effektiv ein ODER zwischen den beiden Bedingungen, während das zweite Abfrageset die beiden Bedingungen UND-verknüpft. Ich hatte erwartet, dass die erste Abfrage auch UND die beiden Bedingungen würde. Ist dies das erwartete Verhalten oder ist dies ein Fehler in Django?

Die Antwort auf eine verwandte Frage Gibt es einen Nachteil bei der Verwendung von ".filter (). Filter (). Filter () ..." in Django? scheint darauf hinzudeuten, dass die beiden Abfragesätze gleichwertig sein sollten.

gerdemb
quelle

Antworten:

117

Ich verstehe es so, dass sie sich vom Design her geringfügig unterscheiden (und ich bin auf jeden Fall offen für Korrekturen): filter(A, B)Zuerst wird nach A gefiltert und dann nach B gefiltert, während filter(A).filter(B)eine Zeile zurückgegeben wird, die mit A 'und' einem möglicherweise anderen übereinstimmt Zeile, die mit B übereinstimmt.

Schauen Sie sich das Beispiel hier an:

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

insbesondere:

Alles in einem einzelnen filter () -Aufruf wird gleichzeitig angewendet, um Elemente herauszufiltern, die all diesen Anforderungen entsprechen. Aufeinanderfolgende filter () -Aufrufe schränken die Menge der Objekte weiter ein

...

In diesem zweiten Beispiel (Filter (A) .filter (B)) beschränkte der erste Filter den Abfragesatz auf (A). Der zweite Filter beschränkte den Satz von Blogs weiter auf diejenigen, die auch (B) sind. Die vom zweiten Filter ausgewählten Einträge können mit den Einträgen im ersten Filter identisch sein oder nicht. "

Timmy O'Mahony
quelle
18
Obwohl dieses Verhalten dokumentiert ist, scheint es gegen das Prinzip des geringsten Erstaunens zu verstoßen. Das UND mehrerer Filter () zusammen, wenn sich Felder im selben Modell befinden, aber dann zusammen ODER, wenn sich Beziehungen erstrecken.
Gerdemb
3
Ich glaube, Sie haben es im ersten Absatz falsch herum - Filter (A, B) ist die AND-Situation ('lennon' AND 2008 in den Dokumenten), während Filter (A) .filter (B) die OR-Situation ist ( 'lennon' ODER 2008). Dies ist sinnvoll, wenn Sie sich die in der Frage generierten Abfragen ansehen. Der Fall .filter (A) .filter (B) erstellt die Verknüpfungen zweimal, was zu einem ODER führt.
Sam
17
Filter (A, B) ist der UND-Filter (A). Filter (B) ist OR
WeizhongTu
3
so further restrictMittel less restrictive?
Boh
7
Diese Antwort ist falsch. Es ist nicht "ODER". Dieser Satz "Der zweite Filter hat die Anzahl der Blogs weiter auf diejenigen beschränkt, die auch (B) sind." erwähnt deutlich "das sind auch (B)." Wenn Sie in diesem speziellen Beispiel ein ähnliches Verhalten wie OR beobachten, bedeutet dies nicht unbedingt, dass Sie Ihre eigene Interpretation verallgemeinern können. Bitte schauen Sie sich die Antworten von "Kevin 3112" und "Johnny Tsang" an. Ich glaube, das sind die richtigen Antworten.
1man
66

Diese beiden Filterstile sind in den meisten Fällen gleichwertig. Bei Abfragen von Objekten, die auf ForeignKey oder ManyToManyField basieren, unterscheiden sie sich jedoch geringfügig.

Beispiele aus der Dokumentation .

Modell
Blog to Entry ist eine Eins-zu-Viele-Beziehung.

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

Objekte
Angenommen, es gibt hier einige Blog- und Eintragsobjekte.
Geben Sie hier die Bildbeschreibung ein

Anfragen

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)  

Bei der ersten Abfrage (Einzelfilter 1) stimmt sie nur mit blog1 überein.

Bei der zweiten Abfrage (verkettete Filter eins) werden blog1 und blog2 herausgefiltert.
Der erste Filter beschränkt das Abfrageset auf blog1, blog2 und blog5. Der zweite Filter beschränkt die Anzahl der Blogs weiter auf Blog1 und Blog2.

Und das solltest du erkennen

Wir filtern die Blog-Elemente mit jeder Filteranweisung, nicht die Eintragselemente.

Es ist also nicht dasselbe, denn Blog und Eintrag sind mehrwertige Beziehungen.

Referenz: https://docs.djangoproject.com/de/1.8/topics/db/queries/#spanning-multi-valued-relationships
Wenn etwas nicht stimmt, korrigieren Sie mich bitte.

Bearbeiten: v1.6 in v1.8 geändert, da die 1.6-Links nicht mehr verfügbar sind.

Kevin_wyx
quelle
3
Sie scheinen zwischen "Übereinstimmungen" und "Herausfiltern" verwechselt zu sein. Wenn Sie sich an "Diese Abfrage kehrt zurück" halten, ist dies viel klarer.
OrangeDog
7

Wie Sie in den generierten SQL-Anweisungen sehen können, ist der Unterschied nicht das "ODER", wie manche vermuten. So wird WHERE und JOIN platziert.

Beispiel 1 (gleiche verbundene Tabelle):

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

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

Dadurch erhalten Sie alle Blogs, die einen Eintrag mit sowohl (entry_ headline _contains = 'Lennon') als auch (entry__pub_date__year = 2008) haben, was Sie von dieser Abfrage erwarten würden. Ergebnis: Buchen Sie mit {entry.headline: 'Life of Lennon', entry.pub_date: '2008'}

Beispiel 2 (verkettet)

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

Dies deckt alle Ergebnisse aus Beispiel 1 ab, erzeugt jedoch etwas mehr Ergebnisse. Weil es zuerst alle Blogs mit (entry_ headline _contains = 'Lennon') und dann aus den Ergebnisfiltern (entry__pub_date__year = 2008) filtert.

Der Unterschied besteht darin, dass Sie auch folgende Ergebnisse erhalten: Buchen mit {entry.headline: ' Lennon ', entry.pub_date: 2000}, {entry.headline: 'Bill', entry.pub_date: 2008 }

In deinem Fall

Ich denke, es ist das, was du brauchst:

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

Und wenn Sie OR verwenden möchten, lesen Sie bitte: https://docs.djangoproject.com/de/dev/topics/db/queries/#complex-lookups-with-q-objects

Johnny Tsang
quelle
Das zweite Beispiel ist nicht wirklich wahr. Alle verketteten Filter werden auf die abgefragten Objekte angewendet, dh sie werden in der Abfrage UND-verknüpft.
Janne
Ich glaube, dass Beispiel 2 korrekt ist, und es ist tatsächlich eine Erklärung, die den offiziellen Django-Dokumenten entnommen ist, auf die verwiesen wird. Ich bin vielleicht nicht der beste Erklärer und ich entschuldige mich dafür. Beispiel 1 ist ein direktes UND, wie Sie es bei einem normalen SQL-Schreiben erwarten würden. Beispiel 1 gibt ungefähr Folgendes an: 'SELECT blog JOIN entry WHERE entry.head_line LIKE " Lennon " AND entry.year == 2008 Beispiel 2 gibt ungefähr Folgendes an:' SELECT blog JOIN entry WHERE entry.head_list LIKE " Lennon " UNION SELECT blog JOIN entry WHERE entry.head_list LIKE " Lennon " '
Johnny Tsang
Sir, Sie haben ganz recht. In Eile habe ich die Tatsache übersehen, dass unsere Filterkriterien auf eine Eins-zu-Viele-Beziehung hinweisen, nicht auf den Blog selbst.
Janne
0

Manchmal möchten Sie nicht mehrere Filter wie folgt zusammenfügen:

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

Und der folgende Code würde tatsächlich nicht das Richtige zurückgeben.

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

Was Sie jetzt tun können, ist die Verwendung eines Anmerkungszählungsfilters.

In diesem Fall zählen wir alle Schichten, die zu einem bestimmten Ereignis gehören.

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

Anschließend können Sie nach Anmerkungen filtern.

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

Diese Lösung ist auch bei großen Abfragesätzen günstiger.

Hoffe das hilft.

Tobias Ernst
quelle