Django-Filter-Abfragesatz __in für * jedes * Element in der Liste

101

Angenommen, ich habe die folgenden Modelle

class Photo(models.Model):
    tags = models.ManyToManyField(Tag)

class Tag(models.Model):
    name = models.CharField(max_length=50)

In einer Ansicht habe ich eine Liste mit aktiven Filtern, die als Kategorien bezeichnet werden . Ich möchte Fotoobjekte filtern, bei denen alle Tags in Kategorien vorhanden sind .

Ich habe es versucht:

Photo.objects.filter(tags__name__in=categories)

Dies entspricht jedoch jedem Element in Kategorien, nicht allen Elementen.

Wenn also Kategorien ['Urlaub', 'Sommer'] wären, möchte ich Fotos mit einem Feiertags- und einem Sommer-Tag.

Kann das erreicht werden?

Sander van Leeuwen
quelle
6
Vielleicht: qs = Photo.objects.all (); für Kategorie in Kategorien: qs = qs.filter (tags__name = Kategorie)
jpic
2
jpic ist richtig, Photo.objects.filter(tags__name='holiday').filter(tags__name='summer')ist der richtige Weg. (Dies ist das gleiche wie im Beispiel von jpic). Jeder filtersollte JOINder Abfrage weitere s hinzufügen , damit Sie einen Annotationsansatz wählen können, wenn zu viele vorhanden sind.
Davor Lucic
1
Hier ist die Referenz in den Dokumenten: docs.djangoproject.com/de/dev/topics/db/queries/…
sgallen
Sie würden erwarten, dass es eine eingebaute Funktion dafür von Django
Vincent

Antworten:

124

Zusammenfassung:

Eine Option ist, wie von jpic und sgallen in den Kommentaren vorgeschlagen, .filter()für jede Kategorie hinzuzufügen . Jede weitere filterfügt weitere Verknüpfungen hinzu, was für kleine Kategorien kein Problem sein sollte.

Es ist die Aggregation Ansatz . Diese Abfrage wäre für eine große Anzahl von Kategorien kürzer und möglicherweise schneller.

Sie haben auch die Möglichkeit, benutzerdefinierte Abfragen zu verwenden .


Einige Beispiele

Versuchsaufbau:

class Photo(models.Model):
    tags = models.ManyToManyField('Tag')

class Tag(models.Model):
    name = models.CharField(max_length=50)

    def __unicode__(self):
        return self.name

In [2]: t1 = Tag.objects.create(name='holiday')
In [3]: t2 = Tag.objects.create(name='summer')
In [4]: p = Photo.objects.create()
In [5]: p.tags.add(t1)
In [6]: p.tags.add(t2)
In [7]: p.tags.all()
Out[7]: [<Tag: holiday>, <Tag: summer>]

Verwenden des Ansatzes für verkettete Filter :

In [8]: Photo.objects.filter(tags=t1).filter(tags=t2)
Out[8]: [<Photo: Photo object>]

Resultierende Abfrage:

In [17]: print Photo.objects.filter(tags=t1).filter(tags=t2).query
SELECT "test_photo"."id"
FROM "test_photo"
INNER JOIN "test_photo_tags" ON ("test_photo"."id" = "test_photo_tags"."photo_id")
INNER JOIN "test_photo_tags" T4 ON ("test_photo"."id" = T4."photo_id")
WHERE ("test_photo_tags"."tag_id" = 3  AND T4."tag_id" = 4 )

Beachten Sie, dass jeder der Abfrage filtermehr hinzufügt JOINS.

Mit Anmerkung Ansatz :

In [29]: from django.db.models import Count
In [30]: Photo.objects.filter(tags__in=[t1, t2]).annotate(num_tags=Count('tags')).filter(num_tags=2)
Out[30]: [<Photo: Photo object>]

Resultierende Abfrage:

In [32]: print Photo.objects.filter(tags__in=[t1, t2]).annotate(num_tags=Count('tags')).filter(num_tags=2).query
SELECT "test_photo"."id", COUNT("test_photo_tags"."tag_id") AS "num_tags"
FROM "test_photo"
LEFT OUTER JOIN "test_photo_tags" ON ("test_photo"."id" = "test_photo_tags"."photo_id")
WHERE ("test_photo_tags"."tag_id" IN (3, 4))
GROUP BY "test_photo"."id", "test_photo"."id"
HAVING COUNT("test_photo_tags"."tag_id") = 2

ANDed QObjekte würden nicht funktionieren:

In [9]: from django.db.models import Q
In [10]: Photo.objects.filter(Q(tags__name='holiday') & Q(tags__name='summer'))
Out[10]: []
In [11]: from operator import and_
In [12]: Photo.objects.filter(reduce(and_, [Q(tags__name='holiday'), Q(tags__name='summer')]))
Out[12]: []

Resultierende Abfrage:

In [25]: print Photo.objects.filter(Q(tags__name='holiday') & Q(tags__name='summer')).query
SELECT "test_photo"."id"
FROM "test_photo"
INNER JOIN "test_photo_tags" ON ("test_photo"."id" = "test_photo_tags"."photo_id")
INNER JOIN "test_tag" ON ("test_photo_tags"."tag_id" = "test_tag"."id")
WHERE ("test_tag"."name" = holiday  AND "test_tag"."name" = summer )
Davor Lucic
quelle
6
Gibt es eine Lösung mit einer benutzerdefinierten Suche? docs.djangoproject.com/de/1.10/howto/custom-lookups Es wäre cool, "__in" auf "__all" zu ändern und die richtige SQL-Abfrage erstellen zu lassen.
t1m0
1
Diese Annotationslösung scheint falsch zu sein. Was ist, wenn drei Tags möglich sind (nennen t3t2t3
wir
@beruic Ich denke, die Idee ist, dass Sie num_tags = 2 durch num_tags = len (tags) ersetzen würden; Ich gehe davon aus, dass die fest codierte 2 nur zum Beispiel war.
tbm
3
@tbm Es würde immer noch nicht funktionieren. Photo.objects.filter(tags__in=tags)Stimmt mit Fotos überein, die eines der Tags haben, nicht nur mit Fotos, die alle haben. Einige von denen, die nur eines der gewünschten Tags haben, haben möglicherweise genau die Anzahl der Tags, nach denen Sie suchen, und einige von denen, die alle gewünschten Tags haben, haben möglicherweise auch zusätzliche Tags.
Berühmter
1
@beruic Die Annotation zählt nur die von der Abfrage zurückgegebenen Tags. Wenn also (von der Abfrage zurückgegebene num-Tags) == (gesuchte num-Tags), wird die Zeile eingeschlossen. "zusätzliche" Tags werden nicht gesucht und daher nicht gezählt. Ich habe dies in meiner eigenen App überprüft.
tbm
8

Ein anderer Ansatz, der funktioniert, obwohl nur PostgreSQL, verwendet django.contrib.postgres.fields.ArrayField:

Beispiel aus Dokumenten kopiert :

>>> Post.objects.create(name='First post', tags=['thoughts', 'django'])
>>> Post.objects.create(name='Second post', tags=['thoughts'])
>>> Post.objects.create(name='Third post', tags=['tutorial', 'django'])

>>> Post.objects.filter(tags__contains=['thoughts'])
<QuerySet [<Post: First post>, <Post: Second post>]>

>>> Post.objects.filter(tags__contains=['django'])
<QuerySet [<Post: First post>, <Post: Third post>]>

>>> Post.objects.filter(tags__contains=['django', 'thoughts'])
<QuerySet [<Post: First post>]>

ArrayFieldverfügt über einige leistungsstärkere Funktionen wie Überlappungs- und Indextransformationen .

Sander van Leeuwen
quelle
3

Dies kann auch durch dynamische Abfragegenerierung mit Django ORM und etwas Python-Magie erfolgen :)

from operator import and_
from django.db.models import Q

categories = ['holiday', 'summer']
res = Photo.filter(reduce(and_, [Q(tags__name=c) for c in categories]))

Die Idee ist, für jede Kategorie geeignete Q-Objekte zu generieren und diese dann mit dem AND-Operator zu einem QuerySet zu kombinieren. Zum Beispiel für Ihr Beispiel wäre es gleich

res = Photo.filter(Q(tags__name='holiday') & Q(tags__name='summer'))
demalexx
quelle
3
Das würde nicht funktionieren. Ihre Abfragebeispiele würden für die betreffenden Modelle nichts zurückgeben.
Davor Lucic
Danke für die Korrektur. Ich dachte, die Verkettung filterwäre die gleiche wie andfür Q-Objekte in einem Filter ... Mein Fehler.
Demalexx
Keine Sorge, mein erster Gedanke wo auch Q Objekte.
Davor Lucic
1
Dies wäre langsamer, wenn Sie mit großen Tabellen und großen Datenmengen arbeiten, mit denen Sie vergleichen können. (wie jeweils 1 Million)
gies0r
Dieser Ansatz sollte funktionieren, wenn Sie von filterzu wechseln excludeund einen Negationsoperator verwenden. Wie so: res = Photo.exclude(~reduce(and_, [Q(tags__name=c) for c in categories]))
Ben
1

Ich verwende eine kleine Funktion, die Filter über eine Liste für einen bestimmten Operator und einen Spaltennamen iteriert:

def exclusive_in (cls,column,operator,value_list):         
    myfilter = column + '__' + operator
    query = cls.objects
    for value in value_list:
        query=query.filter(**{myfilter:value})
    return query  

und diese Funktion kann so aufgerufen werden:

exclusive_in(Photo,'tags__name','iexact',['holiday','summer'])

Es funktioniert auch mit jeder Klasse und mehr Tags in der Liste. Operatoren können alle sein wie 'iexact', 'in', 'enthält', 'ne', ...

David
quelle
0
queryset = Photo.objects.filter(tags__name="vacaciones") | Photo.objects.filter(tags__name="verano")
Edgar Eduardo de los Santos
quelle
-1

Wenn wir es dynamisch machen wollen, folgen Sie dem Beispiel:

tag_ids = [t1.id, t2.id]
qs = Photo.objects.all()

for tag_id in tag_ids:
    qs = qs.filter(tag__id=tag_id)    

print qs
tarasinf
quelle
Kann nicht funktionieren, sobald die zweite Iteration, das Queryset wird leer sein
Lapin