Pertanyaan Django filter queryset __in untuk * setiap * item dalam daftar


Katakanlah saya memiliki model berikut

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

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

Dalam pandangan saya memiliki daftar dengan filter aktif yang disebut kategori. Saya ingin memfilter objek Foto yang memiliki semua tag di dalamnya kategori.

Saya mencoba:

Photo.objects.filter(tags__name__in=categories)

Tapi ini cocok apa saja item dalam kategori, bukan semua item.

Jadi jika kategori akan menjadi ['liburan', 'musim panas'] saya ingin Foto dengan tag liburan dan musim panas.

Bisakah ini tercapai?


75
2017-12-23 16:01


asal


Jawaban:


Ringkasan:

Salah satu pilihan adalah, seperti yang disarankan oleh jpic dan sgallen di komentar, untuk ditambahkan .filter() untuk setiap kategori. setiap tambahan filter menambahkan lebih banyak gabungan, yang seharusnya tidak menjadi masalah untuk sekumpulan kategori kecil.

Ada itu pengumpulan  pendekatan. Kueri ini akan lebih pendek dan mungkin lebih cepat untuk sekumpulan besar kategori.

Anda juga memiliki opsi untuk menggunakan kueri khusus.


Beberapa contoh

Pengaturan tes:

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

Menggunakan filter dirantai pendekatan:

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

Query yang dihasilkan:

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 )

Perhatikan bahwa masing-masing filter tambahkan lebih banyak JOINS ke kueri.

Menggunakan anotasi  pendekatan:

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

Query yang dihasilkan:

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 Q benda tidak akan berfungsi:

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

Query yang dihasilkan:

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 )

99
2017-12-26 17:54



Pendekatan lain yang berhasil, meskipun hanya menggunakan PostgreSQL django.contrib.postgres.fields.ArrayField:

Contoh yang disalin dari dokumen:

>>> 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 memiliki beberapa fitur yang lebih kuat seperti tumpang tindih dan indeks berubah.


5
2018-01-11 13:34



Ini juga dapat dilakukan oleh generasi query dinamis menggunakan Django ORM dan beberapa Python magic :)

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

Idenya adalah untuk menghasilkan objek Q yang sesuai untuk setiap kategori dan kemudian menggabungkannya menggunakan operator AND ke dalam satu QuerySet. Misalnya. untuk contoh Anda itu akan sama dengan

res = Photo.filter(Q(tags__name='holiday') & Q(tags__name='summer'))

2
2017-12-26 15:00



Jika kita ingin melakukannya secara dinamis, ikuti contohnya:

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

-1
2018-05-05 08:16