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ąć?


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,


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 )

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 .


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

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



-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

Nie może działać, gdy tylko druga iteracja, zestaw zapytań będzie pusty
lapin
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.