Django filter queryset __in для * каждого * элемента в списке

Скажем, у меня есть следующие модели

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

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

В представлении у меня есть список с активными фильтрами, называемыми категориями. Я хочу фильтровать объекты фото, которые имеют все теги в категориях.

Я пробовал:

Photo.objects.filter(tags__name__in=categories)

Но это соответствует любому элементу в категориях, а не ко всем элементам.

Так что, если категории будут ['holiday', 'summer'], я хочу Фото как с праздничным, так и с летним тегом.

Можно ли это достичь?

Ответ 1

Резюме:

Один из вариантов, как было предложено jpic и sgallen в комментариях, добавить .filter() для каждой категории. Каждый дополнительный filter добавляет больше объединений, что не должно быть проблемой для небольшого набора категорий.

Существует aggregation подход. Этот запрос будет короче и, возможно, быстрее для большого набора категорий.

У вас также есть возможность использовать пользовательские запросы.


Некоторые примеры

Настройка теста:

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

Использование прикованных фильтров:

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

Результирующий запрос:

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 )

Обратите внимание, что каждый filter добавляет больше JOINS к запросу.

Использование аннотация подход:

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

Результирующий запрос:

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 объекты не будут работать:

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

Результирующий запрос:

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 )

Ответ 2

Другой подход, который работает, хотя и только в PostgreSQL, использует django.contrib.postgres.fields.ArrayField:

Пример скопирован из документации:

>>> 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 имеет некоторые более мощные функции, такие как перекрытие и преобразование индекса.

Ответ 3

Это также может быть сделано путем создания динамических запросов с использованием Django ORM и некоторой магии Python:)

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

Идея состоит в том, чтобы создать соответствующие объекты Q для каждой категории, а затем объединить их с помощью оператора AND в один QuerySet. Например. для вашего примера он будет равен

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

Ответ 4

Я использую небольшую функцию, которая выполняет итерацию фильтров по списку для данного оператора и имени столбца:

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  

и эта функция может быть вызвана так:

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

он также работает с любым классом и другими тегами в списке; Операторы могут быть такими, как "iexact", "in", "содержит", "ne",...

Ответ 5

Если мы хотим сделать это динамически, следуем примеру:

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