Django filter queryset __in dla * każdego * elementu na liście

102

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ę z aktywnymi filtrami zwanymi kategoriami . Chcę filtrować obiekty fotograficzne, które mają wszystkie tagi obecne w kategoriach .

Próbowałem:

Photo.objects.filter(tags__name__in=categories)

Ale to pasuje do dowolnego elementu w kategoriach, nie do wszystkich elementów.

Jeśli więc kategoriami byłyby [„wakacje”, „lato”], chcę mieć zdjęcia z tagami zarówno wakacji, jak i wakacji.

Czy można to osiągnąć?

Sander van Leeuwen
źródło
7
Może: qs = Photo.objects.all (); dla kategorii w kategoriach: qs = qs.filter (tags__name = category)
jpic
2
jpic ma rację, Photo.objects.filter(tags__name='holiday').filter(tags__name='summer')to droga do zrobienia. (To jest to samo, co przykład jpic). Każdy filterpowinien dodać więcej JOINs do zapytania, abyś mógł zastosować adnotacje, jeśli jest ich zbyt wiele.
Davor Lucic,
1
Oto odniesienie w dokumentacji: docs.djangoproject.com/en/dev/topics/db/queries/ ...
sgallen,
Można oczekiwać, że będzie do tego wbudowana funkcja autorstwa Django
Vincent

Odpowiedzi:

124

Podsumowanie:

Jedną z opcji jest, zgodnie z sugestiami jpic i sgallen w komentarzach, dodanie .filter()dla każdej kategorii. Każdy kolejny filterdodaje więcej sprzężeń, co nie powinno stanowić problemu dla małego zestawu kategorii.

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

Masz również możliwość korzystania z zapytań niestandardowych .


Kilka przykładów

Konfiguracja testowa:

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

Korzystanie z metody filtrów łańcuchowych :

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

Wynikowe zapytanie:

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 filterdodaje więcej JOINSdo zapytania.

Korzystanie z adnotacji podejścia :

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

Wynikowe zapytanie:

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

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

Wynikowe zapytanie:

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 )
Davor Lucic
źródło
6
Czy istnieje rozwiązanie z niestandardowym wyszukiwaniem? docs.djangoproject.com/en/1.10/howto/custom-lookups Fajnie byłoby przełączyć „__in” na „__all” i utworzyć poprawne zapytanie sql.
t1m0
1
To rozwiązanie adnotacji wydaje się niewłaściwe. A co jeśli możliwe są trzy tagi (zadzwońmy do dodatkowego t3, a zdjęcie ma tagi t2i t3. Wtedy to zdjęcie będzie nadal pasować do zadanego zapytania.
beruic
@beruic Myślę, że chodzi o to, aby zastąpić num_tags = 2 przez num_tags = len (tagi); Spodziewam się, że zakodowana na stałe 2 była tylko dla przykładu.
tbm
3
@tbm To nadal nie działa. Photo.objects.filter(tags__in=tags)dopasowuje zdjęcia, które mają dowolny z tagów, nie tylko te, które mają wszystkie. Niektóre z tych, które mają tylko jeden z pożądanych tagów, mogą mieć dokładnie taką liczbę tagów, których szukasz, a niektóre z tych, które mają wszystkie pożądane tagi, mogą również mieć dodatkowe tagi.
beruic
1
@beruic adnotacja liczy tylko tagi zwrócone przez zapytanie, więc jeśli (liczba tagów zwróconych przez zapytanie) == (liczba wyszukiwanych tagów) to wiersz jest uwzględniany; Tagi „dodatkowe” nie są wyszukiwane, więc nie będą liczone. Sprawdziłem to we własnej aplikacji.
tbm
8

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

Przykład skopiowany z dokumentów :

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

ArrayFieldma kilka bardziej zaawansowanych funkcji, takich jak przekształcenia nakładania i indeksowania .

Sander van Leeuwen
źródło
3

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

Chodzi o to, aby wygenerować odpowiednie obiekty Q dla każdej kategorii, a następnie połączyć je za pomocą operatora AND w jeden zestaw QuerySet. Np. Dla twojego przykładu byłoby to równe

res = Photo.filter(Q(tags__name='holiday') & Q(tags__name='summer'))
demalexx
źródło
3
To nie zadziała. Twoje przykłady zapytań nie zwróciłyby niczego dla tych modeli.
Davor Lucic
Dzięki za sprostowanie. Myślałem, że tworzenie łańcuchów filterbędzie tym samym, co używanie anddla obiektów Q w jednym filtrze ... Mój błąd.
demalexx
Bez obaw, moja pierwsza myśl dotyczyła również obiektów Q.
Davor Lucic
1
Byłoby to wolniejsze, gdybyś pracował z dużymi tabelami i dużymi danymi do porównania. (jak 1 milion każdy)
gies0r
To podejście powinno działać, jeśli przełączysz się z filterna excludei użyjesz operatora ujemnego. Na przykład: res = Photo.exclude(~reduce(and_, [Q(tags__name=c) for c in categories]))
Ben
1

Używam małej funkcji, która iteruje filtry po liście dla danego operatora i nazwy 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  

a tę funkcję można nazwać w ten sposób:

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

działa również z każdą klasą i innymi tagami na liście; operatory mogą być dowolnymi operatorami, takimi jak „iexact”, „in”, „zawiera”, „ne”, ...

David
źródło
0
queryset = Photo.objects.filter(tags__name="vacaciones") | Photo.objects.filter(tags__name="verano")
Edgar Eduardo de los Santos
źródło
-1

Jeśli chcemy to robić dynamicznie, postępowaliśmy zgodnie z 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
tarasinf
źródło
Nie może działać, gdy tylko druga iteracja, zestaw zapytań będzie pusty
lapin