Mengapa prefetch_related () django hanya bekerja dengan all () dan bukan filter ()?

90

misalkan saya memiliki model ini:

class PhotoAlbum(models.Model):
    title = models.CharField(max_length=128)
    author = models.CharField(max_length=128)

class Photo(models.Model):
    album = models.ForeignKey('PhotoAlbum')
    format = models.IntegerField()

Sekarang jika saya ingin melihat subset foto dalam subset album secara efisien. Saya melakukannya seperti ini:

someAlbums = PhotoAlbum.objects.filter(author="Davey Jones").prefetch_related("photo_set")
for a in someAlbums:
    somePhotos = a.photo_set.all()

Ini hanya melakukan dua kueri, yang saya harapkan (satu untuk mendapatkan album, dan satu lagi seperti `SELECT * IN photos WHERE photoalbum_id IN ().

Semuanya bagus.

Tetapi jika saya melakukan ini:

someAlbums = PhotoAlbum.objects.filter(author="Davey Jones").prefetch_related("photo_set")
for a in someAlbums:
    somePhotos = a.photo_set.filter(format=1)

Kemudian ia melakukan banyak pertanyaan dengan WHERE format = 1! Apakah saya melakukan sesuatu yang salah atau apakah Django tidak cukup pintar untuk menyadari bahwa ia telah mengambil semua foto dan dapat memfilternya dengan python? Saya bersumpah saya membaca di suatu tempat di dokumentasi bahwa itu seharusnya melakukan itu ...

Timmmm
sumber
kemungkinan duplikat Filter pada prefetch_related di Django
akaihola

Jawaban:

168

Dalam Django 1.6 dan sebelumnya, tidaklah mungkin untuk menghindari kueri tambahan. The prefetch_relatedpanggilan efektif cache hasil a.photoset.all()untuk setiap album di queryset tersebut. Namun, a.photoset.filter(format=1)ini adalah kumpulan kueri yang berbeda, jadi Anda akan menghasilkan kueri tambahan untuk setiap album.

Ini dijelaskan di prefetch_relateddokumen. The filter(format=1)setara dengan filter(spicy=True).

Perhatikan bahwa Anda dapat mengurangi jumlah atau kueri dengan memfilter foto dengan python sebagai gantinya:

someAlbums = PhotoAlbum.objects.filter(author="Davey Jones").prefetch_related("photo_set")
for a in someAlbums:
    somePhotos = [p for p in a.photo_set.all() if p.format == 1]

Dalam Django 1.7, ada Prefetch()obyek yang mengijinkan anda untuk mengontrol perilakunya prefetch_related.

from django.db.models import Prefetch

someAlbums = PhotoAlbum.objects.filter(author="Davey Jones").prefetch_related(
    Prefetch(
        "photo_set",
        queryset=Photo.objects.filter(format=1),
        to_attr="some_photos"
    )
)
for a in someAlbums:
    somePhotos = a.some_photos

Untuk contoh lebih lanjut tentang bagaimana menggunakan Prefetchobjek, lihat prefetch_relateddokumen.

Alasdair
sumber
8

Dari dokumen :

... seperti biasa dengan QuerySets, setiap metode berantai berikutnya yang menyiratkan kueri database yang berbeda akan mengabaikan hasil cache sebelumnya, dan mengambil data menggunakan kueri database baru. Jadi, jika Anda menulis yang berikut ini:

pizzas = Pizza.objects.prefetch_related('toppings') [list(pizza.toppings.filter(spicy=True)) for pizza in pizzas]

... maka fakta bahwa pizza.toppings.all () telah di-prefetch tidak akan membantu Anda - pada kenyataannya hal itu merusak kinerja, karena Anda telah melakukan kueri database yang belum Anda gunakan. Jadi gunakan fitur ini dengan hati-hati!

Dalam kasus Anda, "a.photo_set.filter (format = 1)" diperlakukan seperti kueri baru.

Selain itu, "photo_set" adalah pencarian terbalik - yang diterapkan melalui pengelola yang berbeda sekaligus.

Ngure Nyaga
sumber
photo_setdapat diambil sebelumnya, juga, dengan .prefetch_related('photo_set'). Tapi ketertiban penting, seperti yang telah Anda jelaskan.
Risadinha