Queryset filter Django __in untuk * setiap * item dalam daftar


102

Katakanlah saya memiliki model berikut

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

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

Dalam tampilan saya memiliki daftar dengan filter aktif yang disebut kategori . Saya ingin memfilter objek Foto yang semua tagnya ada dalam kategori .

Saya mencoba:

Photo.objects.filter(tags__name__in=categories)

Tapi ini cocok dengan semua item dalam kategori, tidak semua item.

Jadi jika kategorinya adalah ['liburan', 'musim panas'], saya ingin Foto dengan tag liburan dan musim panas.

Bisakah ini dicapai?


7
Mungkin: qs = Photo.objects.all (); untuk kategori dalam kategori: qs = qs.filter (tags__name = category)
jpic

2
jpic benar, Photo.objects.filter(tags__name='holiday').filter(tags__name='summer')adalah cara yang tepat. (Ini sama dengan contoh jpic). Masing filter- masing harus menambahkan lebih banyak JOINke kueri, sehingga Anda dapat mengambil pendekatan anotasi jika terlalu banyak.
Davor Lucic


Anda akan mengharapkan ada fungsi built-in untuk ini oleh Django
Vincent

Jawaban:


124

Ringkasan:

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

Ada pendekatan agregasi . 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 pendekatan filter berantai :

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

Kueri 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 filtermenambahkan lebih banyak JOINSke kueri.

Menggunakan pendekatan anotasi :

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

Kueri 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

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

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

6
Apakah ada solusi dengan pencarian kustom? docs.djangoproject.com/en/1.10/howto/custom-lookups Sebaiknya alihkan "__in" ke "__all" dan buat kueri sql yang benar.
t1m0

1
Solusi anotasi ini sepertinya salah. Bagaimana jika ada tiga tag yang memungkinkan (sebut saja yang tambahan untuk t3, dan foto memiliki tag t2dan t3. Maka foto ini akan tetap cocok dengan kueri yang diberikan.
beruic

@beruic Saya pikir idenya adalah bahwa Anda akan mengganti num_tags = 2 dengan num_tags = len (tag); Saya berharap kode keras 2 hanya untuk contoh saja.
tbm

3
@tbm Ini masih tidak akan berhasil. Photo.objects.filter(tags__in=tags)cocok dengan foto yang memiliki salah satu tag, tidak hanya yang memiliki semua. Beberapa dari mereka yang hanya memiliki salah satu tag yang diinginkan, mungkin memiliki jumlah yang tepat dari tag yang Anda cari, dan beberapa yang memiliki semua tag yang diinginkan, mungkin juga memiliki tag tambahan.
beruic

1
@beruic anotasi hanya menghitung tag yang dikembalikan oleh kueri, jadi jika (tag num dikembalikan oleh kueri) == (num tag yang dicari) maka baris tersebut akan disertakan; tag "ekstra" tidak dicari, jadi tidak akan dihitung. Saya telah memverifikasi ini dalam aplikasi saya sendiri.
tbm

8

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

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

ArrayFieldmemiliki beberapa fitur yang lebih kuat seperti tumpang tindih dan transformasi indeks .


3

Ini juga dapat dilakukan dengan pembuatan kueri dinamis menggunakan Django ORM dan beberapa sihir 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]))

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

3
Ini tidak akan berhasil. Contoh kueri Anda tidak akan mengembalikan apa pun untuk model yang dimaksud.
Davor Lucic

Terima kasih atas koreksinya. Saya pikir merangkai filterakan sama dengan menggunakan andobjek Q dalam satu filter ... Kesalahan saya.
demalexx

Jangan khawatir, pikiran pertama saya juga di mana objek Q.
Davor Lucic

1
Ini akan lebih lambat jika Anda bekerja dengan tabel besar dan data besar untuk dibandingkan. (seperti masing-masing 1 Juta)
gies0r

Pendekatan ini akan berhasil jika Anda beralih dari filterke excludedan menggunakan operator negate. Seperti: res = Photo.exclude(~reduce(and_, [Q(tags__name=c) for c in categories]))
Ben

1

Saya menggunakan fungsi kecil yang mengulang filter di atas daftar untuk operator tertentu dengan nama kolom:

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  

dan fungsi ini bisa disebut seperti itu:

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

itu juga bekerja dengan kelas apa pun dan lebih banyak tag dalam daftar; operator bisa siapa saja seperti 'iexact', 'in', 'contains', 'ne', ...



-1

Jika kita ingin melakukannya secara dinamis, ikuti contoh:

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

Tidak dapat berfungsi segera setelah iterasi kedua, queryset akan kosong
lapin
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.