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

104

Допустим, у меня есть следующие модели

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)

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

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

Можно ли этого добиться?

Сандер ван Леувен
источник
7
Может быть: qs = Photo.objects.all (); для категории в категориях: qs = qs.filter (tags__name = category)
jpic
2
jpic правильный, Photo.objects.filter(tags__name='holiday').filter(tags__name='summer')это путь. (То же, что и в примере jpic). Каждый filterдолжен добавить больше JOINs в запрос, чтобы вы могли использовать аннотационный подход, если их слишком много.
Davor Lucic
1
Вот ссылка в документации: docs.djangoproject.com/en/dev/topics/db/queries/…
sgallen
Вы ожидаете, что для этого будет встроенная функция от Django
Винсент

Ответы:

125

Резюме:

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

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

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


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

Испытательная установка:

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

ANDQобъекты ed не будут работать:

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 )
Давор Лучич
источник
6
Есть ли решение с кастомным поиском? docs.djangoproject.com/en/1.10/howto/custom-lookups Было бы здорово переключить «__in» на «__all» и заставить его создать правильный sql-запрос.
t1m0
1
Это решение для аннотации кажется неправильным. Что, если есть три возможных тега (давайте назовем дополнительный для t3, а фотография будет иметь теги t2и t3. Тогда эта фотография все равно будет соответствовать заданному запросу.
beruic
@beruic Думаю, идея состоит в том, чтобы заменить num_tags = 2 на num_tags = len (tags); Я полагаю, что жестко запрограммированные 2 были просто для примера.
tbm
3
@tbm Это все равно не сработает. Photo.objects.filter(tags__in=tags)сопоставляет фотографии с любым из тегов, а не только с теми, на которых есть все. Некоторые из тех, у которых есть только один из желаемых тегов, могут иметь именно то количество тегов, которое вы ищете, а некоторые из тех, у которых есть все желаемые теги, могут также иметь дополнительные теги.
beruic
1
@beruic аннотация учитывает только теги, возвращенные запросом, поэтому если (число тегов, возвращенных запросом) == (число тегов, найденных), то строка включается; "лишние" теги не ищутся, поэтому не учитываются. Я проверил это в собственном приложении.
tbm
8

Другой подход, который работает, хотя и только для 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'))
demalexx
источник
4
Это не сработает. Примеры ваших запросов ничего не вернут для рассматриваемых моделей.
Давор Лючич,
Спасибо за исправление. Я думал, что цепочка filterбудет такой же, как использование andдля объектов Q в одном фильтре ... Моя ошибка.
demalexx
Не беспокойтесь, моя первая мысль, где также Q объектов.
Давор Лючич
1
Это будет медленнее, если вы работаете с большими таблицами и большими данными для сравнения. (по 1
миллиону
1
Такой подход должен работать , если перейти от filterк excludeи использовать нивелирует оператор. Вот так: res = Photo.exclude(~reduce(and_, [Q(tags__name=c) for c in categories]))
Бен
1

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

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', 'contains', 'ne', ...

Дэвид
источник
0
queryset = Photo.objects.filter(tags__name="vacaciones") | Photo.objects.filter(tags__name="verano")
Эдгар Эдуардо де лос сантос
источник
-1

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

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