Dlaczego funkcja prefetch_related () django działa tylko z all (), a nie filter ()?

89

przypuśćmy, że mam ten model:

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

Teraz, jeśli chcę efektywnie przeglądać podzbiór zdjęć w podzbiorze albumów. Robię to mniej więcej tak:

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

Robi to tylko dwa zapytania, czego się spodziewam (jedno do uzyskania albumów, a następnie jedno w stylu `SELECT * IN photos WHERE photoalbum_id IN ().

Wszystko w porządku.

Ale jeśli to zrobię:

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

Następnie wykonuje mnóstwo zapytań WHERE format = 1! Czy robię coś źle, czy też django nie jest na tyle sprytne, aby zdać sobie sprawę, że pobrał już wszystkie zdjęcia i może je filtrować w Pythonie? Przysięgam, że gdzieś w dokumentacji przeczytałem, że ma to robić ...

Timmmm
źródło
możliwy duplikat filtru na prefetch_related w Django
akaihola

Odpowiedzi:

166

W Django 1.6 i wcześniejszych nie można uniknąć dodatkowych zapytań. prefetch_relatedWezwanie skutecznie buforuje rezultaty a.photoset.all()dla każdego albumu w queryset. Jednak a.photoset.filter(format=1)jest to inny zestaw zapytań, więc dla każdego albumu wygenerujesz dodatkowe zapytanie.

Jest to wyjaśnione w prefetch_relateddokumentacji. filter(format=1)Jest równoważna filter(spicy=True).

Zauważ, że możesz zmniejszyć liczbę lub zapytań, filtrując zdjęcia w Pythonie:

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]

W Django 1.7 istnieje Prefetch()obiekt, który pozwala kontrolować zachowanie 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

Więcej przykładów użycia Prefetchobiektu można znaleźć w prefetch_relateddokumentacji.

Alasdair
źródło
8

Z dokumentów :

... jak zawsze w przypadku QuerySets, wszelkie kolejne metody łańcuchowe, które implikują inne zapytanie do bazy danych, zignorują wyniki zapisane wcześniej w pamięci podręcznej i będą pobierać dane przy użyciu nowego zapytania do bazy danych. Tak więc, jeśli napiszesz:

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

... wtedy fakt, że pizza.toppings.all () została wstępnie pobrana, nie pomoże - w rzeczywistości obniża wydajność, ponieważ wykonałeś zapytanie do bazy danych, którego nie użyłeś. Dlatego używaj tej funkcji ostrożnie!

W twoim przypadku „a.photo_set.filter (format = 1)” jest traktowane jak nowe zapytanie.

Ponadto „photo_set” jest wyszukiwaniem wstecznym - zaimplementowanym przez zupełnie innego menedżera.

Ngure Nyaga
źródło
photo_setmożna również wstępnie pobrać za pomocą .prefetch_related('photo_set'). Ale porządek ma znaczenie, jak wyjaśniłeś.
Risadinha