Набор запросов фильтра Django __in для * каждого * элемента в списке - PullRequest
87 голосов
/ 23 декабря 2011

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

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

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

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

Я пытался:

Photo.objects.filter(tags__name__in=categories)

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

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

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

Ответы [ 5 ]

108 голосов
/ 26 декабря 2011

Резюме:

Один из вариантов, как предложено 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

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 )
8 голосов
/ 11 января 2017

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

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

3 голосов
/ 26 декабря 2011

Это также может быть сделано путем динамической генерации запросов с использованием 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'))
1 голос
/ 11 ноября 2018

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

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», ...

0 голосов
/ 05 мая 2018

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

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