Набір запитів фільтра Джанго __in для * кожного * елемента в списку


102

Скажімо, у мене є такі моделі

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 = категорія)
jpic

2
jpic вірно, Photo.objects.filter(tags__name='holiday').filter(tags__name='summer')це шлях. (Це те саме, що і приклад jpic). Кожен filterповинен додавати більше JOINs до запиту, так що ви можете скористатись анотаційним підходом, якщо їх занадто багато.
Давор Лучич

1
Ось посилання на документи: docs.djangoproject.com/en/dev/topics/db/queries/…
sgallen

Ви очікували, що для цього буде функціонувати вбудована функція від Django
Vincent

Відповіді:


124

Підсумок:

Один варіант, як запропоновано 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 (теги); Я думаю, що жорсткий код 2 був просто заради.
tbm

3
@tbm Це все одно не буде працювати. Photo.objects.filter(tags__in=tags)відповідає фотографіям, що містять будь-які теги, не лише тим, що має всі. Деякі з тих, у яких є лише один із бажаних тегів, можуть мати саме таку кількість тегів, яку ви шукаєте, а деякі з тих, що мають усі бажані теги, можуть також мати додаткові теги.
Беруїк

1
@beruic анотація враховує лише теги, повернуті в результаті запиту, тому якщо (num теги, повернені запитом) == (шукані теги num), рядок включається; "зайві" теги не шукаються, тому не зараховуються. Я підтвердив це у власному додатку.
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'))

3
Це не спрацювало б. Ваші приклади запитів нічого не повернуть для моделей, про які йдеться.
Давор Лучич

Дякуємо за виправлення Я думав, що ланцюжок filterбуде таким самим, як використання andдля Q-об'єктів в одному фільтрі ... Моя помилка.
demalexx

Не хвилюйтесь, моя перша думка, де також Q об’єкти.
Давор Лучич

1
Це ми повільніше, якщо ви працюєте з великими таблицями та великими даними для порівняння. (як 1 мільйон кожен)
gies0r

Цей підхід повинен працювати, якщо ви переходите 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", "містить", "ne", ...



-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

Неможливо працювати, як тільки відбудеться друга ітерація, набір запитів буде порожнім
lapin
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.