Django filter queryset in for * every* item in list

Powiedzmy, że mam następujące modele

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

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

W widoku mam listę aktywnych filtrów o nazwie kategorie . Chcę filtrować Obiekty fotograficzne, które mają wszystkie tagi obecne w kategorii .

Próbowałem:

Photo.objects.filter(tags__name__in=categories)

Ale to pasuje do wszystkich pozycji w kategoriach, a nie do wszystkich pozycji.

Więc jeśli kategorie będą ['wakacje', 'lato'] chcę zdjęcia z tagiem wakacje i lato.

Czy to może być / align = "left" /

Author: Sander van Leeuwen, 2011-12-23

7 answers

Podsumowanie:

Jedną z opcji jest, jak sugerują jpic i sgallen w komentarzach, dodanie .filter() do każdej kategorii. Każda dodatkowa filter dodaje więcej złączeń, co nie powinno być problemem dla małych kategorii.

Istnieje agregacja podejście . To zapytanie byłoby krótsze i być może szybsze dla dużego zestawu kategorii.

Masz również możliwość użycia zapytań niestandardowych.


Niektóre przykłady

Konfiguracja testu:

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

Używanie filtrów łańcuchowych podejście:

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

Zapytanie wynikowe:

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 )

Zauważ, że każdy filter dodaje więcej JOINS do zapytania.

Using adnotacja podejście:

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

Zapytanie wynikowe:

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

AND ed Q obiekty nie będą działać:

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

Zapytanie wynikowe:

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 )
 131
Author: Davor Lucic,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-05-03 09:26:39

Innym podejściem, które działa, chociaż tylko PostgreSQL, jest użycie django.contrib.postgres.fields.ArrayField:

Przykład skopiowany z docs :

>>> 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 posiada kilka bardziej zaawansowanych funkcji, takich jak overlap i indeks przekształca .

 8
Author: Sander van Leeuwen,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-01-11 13:34:36

Można to również zrobić poprzez dynamiczne generowanie zapytań przy użyciu Django ORM i trochę magii Pythona:)

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

Idea polega na wygenerowaniu odpowiednich obiektów Q dla każdej kategorii, a następnie połączeniu ich za pomocą operatora i w jeden zestaw zapytań. Np. dla Twojego przykładu będzie równa

res = Photo.filter(Q(tags__name='holiday') & Q(tags__name='summer'))
 3
Author: demalexx,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-12-26 15:00:06

Używam małej funkcji, która iteruje filtry nad listą dla danego operatora a nazwę kolumny:

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  

I funkcję tę można wywołać w ten sposób:

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

Działa również z dowolną klasą i innymi znacznikami na liście; operatorami mogą być dowolne typy, takie jak 'iexact', 'in', 'contains', 'ne',...

 1
Author: David,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-11-11 18:55:30
queryset = Photo.objects.filter(tags__name="vacaciones") | Photo.objects.filter(tags__name="verano")
 0
Author: Edgar Eduardo de los santos,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2020-06-24 04:51:57

Jeśli zmagałeś się z tym problemem tak jak ja i nic Ci nie pomogło, może ten rozwiąże twój problem

Zamiast filtrowania łańcuchowego, w niektórych przypadkach lepiej byłoby po prostu przechowywać identyfikatory poprzedniego filtra

tags = [1, 2]
for tag in tags:
    ids = list(queryset.filter(tags__id=tag).values_list("id", flat=True))
    queryset = queryset.filter(id__in=ids)

Użycie tego podejścia pomoże Ci uniknąć stackowania JOIN w zapytaniu SQL:

 0
Author: Alexander Lekontsev,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2021-01-25 11:13:14

Jeśli chcemy zrobić to dynamicznie, to za przykładem:

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
Author: tarasinf,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-05-05 08:16:15