Merangkai banyak filter () di Django, apakah ini bug?

103

Saya selalu berasumsi bahwa merangkai banyak pemanggilan filter () di Django selalu sama dengan mengumpulkannya dalam satu panggilan.

# Equivalent
Model.objects.filter(foo=1).filter(bar=2)
Model.objects.filter(foo=1,bar=2)

tetapi saya telah menemukan queryset yang rumit dalam kode saya di mana hal ini tidak terjadi

class Inventory(models.Model):
    book = models.ForeignKey(Book)

class Profile(models.Model):
    user = models.OneToOneField(auth.models.User)
    vacation = models.BooleanField()
    country = models.CharField(max_length=30)

# Not Equivalent!
Book.objects.filter(inventory__user__profile__vacation=False).filter(inventory__user__profile__country='BR')
Book.objects.filter(inventory__user__profile__vacation=False, inventory__user__profile__country='BR')

SQL yang dihasilkan adalah

SELECT "library_book"."id", "library_book"."asin", "library_book"."added", "library_book"."updated" FROM "library_book" INNER JOIN "library_inventory" ON ("library_book"."id" = "library_inventory"."book_id") INNER JOIN "auth_user" ON ("library_inventory"."user_id" = "auth_user"."id") INNER JOIN "library_profile" ON ("auth_user"."id" = "library_profile"."user_id") INNER JOIN "library_inventory" T5 ON ("library_book"."id" = T5."book_id") INNER JOIN "auth_user" T6 ON (T5."user_id" = T6."id") INNER JOIN "library_profile" T7 ON (T6."id" = T7."user_id") WHERE ("library_profile"."vacation" = False  AND T7."country" = BR )
SELECT "library_book"."id", "library_book"."asin", "library_book"."added", "library_book"."updated" FROM "library_book" INNER JOIN "library_inventory" ON ("library_book"."id" = "library_inventory"."book_id") INNER JOIN "auth_user" ON ("library_inventory"."user_id" = "auth_user"."id") INNER JOIN "library_profile" ON ("auth_user"."id" = "library_profile"."user_id") WHERE ("library_profile"."vacation" = False  AND "library_profile"."country" = BR )

Queryset pertama dengan filter()panggilan berantai bergabung dengan model Inventory dua kali secara efektif membuat OR antara dua kondisi sedangkan queryset kedua AND menyatukan kedua kondisi tersebut. Saya berharap bahwa kueri pertama juga akan DAN dua ketentuan. Apakah ini perilaku yang diharapkan atau apakah ini bug di Django?

Jawaban untuk pertanyaan terkait Apakah ada kerugian menggunakan ".filter (). Filter (). Filter () ..." di Django? tampaknya menunjukkan bahwa dua queryset harus setara.

gerdemb
sumber

Jawaban:

118

Cara saya memahaminya adalah bahwa mereka sedikit berbeda menurut desain (dan saya pasti terbuka untuk koreksi): filter(A, B)pertama-tama akan menyaring menurut A dan kemudian subfilter menurut B, sementara filter(A).filter(B)akan mengembalikan baris yang cocok dengan A 'dan' yang berpotensi berbeda baris yang cocok dengan B.

Lihat contohnya di sini:

https://docs.djangoproject.com/en/dev/topics/db/queries/#spanning-multi-valued-relationships

terutama:

Semua yang ada di dalam panggilan filter () tunggal diterapkan secara bersamaan untuk memfilter item yang cocok dengan semua persyaratan tersebut. Panggilan filter () yang berurutan semakin membatasi kumpulan objek

...

Dalam contoh kedua ini (filter (A) .filter (B)), filter pertama membatasi queryset ke (A). Filter kedua membatasi kumpulan blog lebih jauh ke blog yang juga (B). Entri yang dipilih oleh filter kedua mungkin atau mungkin tidak sama dengan entri dalam filter pertama.`

Timmy O'Mahony
sumber
18
Perilaku ini, meskipun didokumentasikan, tampaknya melanggar prinsip yang paling tidak mengherankan. Beberapa filter () DAN bersama-sama ketika bidang berada pada model yang sama, tetapi kemudian ATAU bersama-sama ketika mencakup hubungan.
gerdemb
3
Saya yakin Anda memiliki cara yang salah di paragraf pertama - filter (A, B) adalah situasi AND ('lennon' AND 2008 dalam dokumen), sedangkan filter (A) .filter (B) adalah situasi OR ( 'lennon' OR 2008). Ini masuk akal saat Anda melihat kueri yang dihasilkan dalam pertanyaan - kasus .filter (A) .filter (B) membuat gabungan dua kali, menghasilkan ATAU.
Sam
17
filter (A, B) adalah AND filter (A) .filter (B) adalah OR
WeizhongTu
3
further restrictberarti begitu less restrictive?
boh
7
Jawaban ini salah. Ini bukan "ATAU". Kalimat ini "Filter kedua membatasi kumpulan blog lebih jauh ke blog yang juga (B)." dengan jelas menyebutkan "itu juga (B)." Jika Anda mengamati perilaku yang mirip dengan ATAU dalam contoh khusus ini, itu tidak berarti Anda dapat menggeneralisasi interpretasi Anda sendiri. Silakan lihat jawaban dari "Kevin 3112" dan "Johnny Tsang." Saya yakin itu adalah jawaban yang benar.
1man
67

Kedua gaya pemfilteran ini setara dalam banyak kasus, tetapi ketika kueri pada objek berbasis ForeignKey atau ManyToManyField, keduanya sedikit berbeda.

Contoh dari dokumentasi .

model
Blog to Entry adalah relasi one-to-many.

from django.db import models

class Blog(models.Model):
    ...

class Entry(models.Model):
    blog = models.ForeignKey(Blog)
    headline = models.CharField(max_length=255)
    pub_date = models.DateField()
    ...

objek
Dengan asumsi ada beberapa objek blog dan entri di sini.
masukkan deskripsi gambar di sini

pertanyaan

Blog.objects.filter(entry__headline_contains='Lennon', 
    entry__pub_date__year=2008)
Blog.objects.filter(entry__headline_contains='Lennon').filter(
    entry__pub_date__year=2008)  

Untuk kueri pertama (filter tunggal satu), hanya cocok dengan blog1.

Untuk kueri kedua (filter berantai satu), filter ini menyaring blog1 dan blog2.
Filter pertama membatasi queryset ke blog1, blog2 dan blog5; filter kedua membatasi kumpulan blog lebih jauh ke blog1 dan blog2.

Dan Anda harus menyadari itu

Kami memfilter item Blog dengan setiap pernyataan filter, bukan item Entri.

Jadi, tidak sama, karena Blog dan Entri adalah hubungan yang memiliki banyak nilai.

Referensi: https://docs.djangoproject.com/en/1.8/topics/db/queries/#spanning-multi-valued-relationships
Jika ada yang salah, tolong perbaiki saya.

Edit: Mengubah v1.6 menjadi v1.8 karena link 1.6 tidak lagi tersedia.

Kevin_wyx
sumber
3
Anda tampaknya tercampur antara "pertandingan" dan "menyaring". Jika Anda terjebak pada "kueri ini kembali", itu akan jauh lebih jelas.
OrangeDog
7

Seperti yang Anda lihat dalam pernyataan SQL yang dihasilkan, perbedaannya bukanlah "ATAU" seperti yang diduga beberapa orang. Ini adalah bagaimana WHERE dan JOIN ditempatkan.

Example1 (tabel gabungan yang sama):

(contoh dari https://docs.djangoproject.com/en/dev/topics/db/queries/#spanning-multi-valued-relationships )

Blog.objects.filter(entry__headline__contains='Lennon', entry__pub_date__year=2008)

Ini akan memberi Anda semua Blog yang memiliki satu entri dengan keduanya (entry_ headline _contains = 'Lennon') AND (entry__pub_date__year = 2008), yang Anda harapkan dari kueri ini. Hasil: Buku dengan {entry.headline: 'Life of Lennon', entry.pub_date: '2008'}

Contoh 2 (dirantai)

Blog.objects.filter(entry__headline__contains='Lennon').filter(entry__pub_date__year=2008)

Ini akan mencakup semua hasil dari Contoh 1, tetapi akan menghasilkan hasil yang sedikit lebih banyak. Karena ini pertama-tama menyaring semua blog dengan (entry_ headline _contains = 'Lennon') dan kemudian dari filter hasil (entry__pub_date__year = 2008).

Perbedaannya adalah ini juga akan memberi Anda hasil seperti: Book with {entry.headline: ' Lennon ', entry.pub_date: 2000}, {entry.headline: 'Bill', entry.pub_date: 2008 }

Dalam kasus Anda

Saya pikir inilah yang Anda butuhkan:

Book.objects.filter(inventory__user__profile__vacation=False, inventory__user__profile__country='BR')

Dan jika Anda ingin menggunakan ATAU silakan baca: https://docs.djangoproject.com/en/dev/topics/db/queries/#complex-lookups-with-q-objects

Johnny Tsang
sumber
Contoh kedua sebenarnya tidak benar. Semua filter yang dirantai diterapkan ke objek yang ditanyakan, yaitu mereka di-AND bersama-sama dalam kueri.
Janne
Saya percaya bahwa Contoh 2 benar, dan ini sebenarnya adalah penjelasan yang diambil dari dokumen resmi Django, sebagai referensi. Saya mungkin bukan penjelas terbaik dan saya memaafkannya. Contoh 1 adalah DAN langsung seperti yang Anda harapkan dalam penulisan SQL normal. Contoh 1 memberikan sesuatu seperti ini: 'PILIH blog GABUNG entri DI MANA entri.head_line LIKE " Lennon " DAN entri.year == 2008 Contoh 2 memberikan sesuatu seperti ini:' PILIH blog GABUNG entri DI MANA entri.head_list LIKE " Lennon " UNION SELECT blog GABUNG entri DI MANA entry.head_list LIKE " Lennon " '
Johnny Tsang
Tuan, Anda benar. Dengan tergesa-gesa, saya melewatkan fakta bahwa kriteria pemfilteran kami mengarah ke hubungan satu-ke-banyak, bukan ke blog itu sendiri.
Janne
0

Terkadang Anda tidak ingin menggabungkan beberapa filter bersama seperti ini:

def your_dynamic_query_generator(self, event: Event):
    qs \
    .filter(shiftregistrations__event=event) \
    .filter(shiftregistrations__shifts=False)

Dan kode berikut sebenarnya tidak mengembalikan hal yang benar.

def your_dynamic_query_generator(self, event: Event):
    return Q(shiftregistrations__event=event) & Q(shiftregistrations__shifts=False)

Yang dapat Anda lakukan sekarang adalah menggunakan filter hitungan anotasi.

Dalam hal ini kami menghitung semua shift yang termasuk dalam peristiwa tertentu.

qs: EventQuerySet = qs.annotate(
    num_shifts=Count('shiftregistrations__shifts', filter=Q(shiftregistrations__event=event))
)

Setelah itu Anda dapat memfilter berdasarkan anotasi.

def your_dynamic_query_generator(self):
    return Q(num_shifts=0)

Solusi ini juga lebih murah pada queryset besar.

Semoga ini membantu.

Tobias Ernst
sumber