Frage Django-Filter-Abfrage-Set __in für * jedes * Element in der Liste


Sagen wir, ich habe folgende 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 aufgerufen Kategorien. Ich möchte Fotoobjekte filtern, bei denen alle Tags vorhanden sind Kategorien.

Ich habe es versucht:

Photo.objects.filter(tags__name__in=categories)

Aber das passt irgendein Artikel in Kategorien, nicht alle Artikel.

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

Kann das erreicht werden?


75
2017-12-23 16:01


Ursprung


Antworten:


Zusammenfassung:

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

Dort ist der Anhäufung  Ansatz. Diese Abfrage wäre für eine große Gruppe von Kategorien kürzer und vielleicht schneller.

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


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 Kettenfilter Ansatz:

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 filter fügt mehr hinzu JOINS zur Abfrage.

Verwenden 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 Q Objekte 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 )

99
2017-12-26 17:54



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

Beispiel kopiert von Dokumente:

>>> 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>]>

ArrayField hat einige leistungsfähigere Funktionen wie Überlappung und Indextransformationen.


5
2018-01-11 13:34



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

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 besteht darin, für jede Kategorie geeignete Q-Objekte zu generieren und sie dann mit dem AND-Operator zu einem QuerySet zu kombinieren. Z.B. für dein Beispiel wäre es gleich

res = Photo.filter(Q(tags__name='holiday') & Q(tags__name='summer'))

2
2017-12-26 15:00



Wenn wir es dynamisch machen wollen, folgte das 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

-1
2018-05-05 08:16