Почему django prefetch_related() работает только со всеми(), а не с фильтром()?

Предположим, что у меня есть эта модель:

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

Теперь, если я хочу эффективно просматривать подмножество фотографий в подмножестве альбомов. Я делаю это примерно так:

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

Это делает только два запроса, которые я ожидаю (один для получения альбомов, а затем один из них: SELECT * IN photos WHERE photoalbum_id IN().

Все отлично.

Но если я это сделаю:

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

Затем он обрабатывает тонну запросов с помощью WHERE format = 1! Я что-то делаю неправильно или django недостаточно умен, чтобы понять, что он уже набрал все фотографии и может фильтровать их на python? Клянусь, я где-то читал в документации, что он должен это делать...

Ответ 1

В Django 1.6 и ранее невозможно избежать дополнительных запросов. Вызов prefetch_related эффективно кэширует результаты a.photoset.all() для каждого альбома в наборе запросов. Однако a.photoset.filter(format=1) - это другой запрос, поэтому вы создадите дополнительный запрос для каждого альбома.

Это объясняется в prefetch_related docs. Значение filter(format=1) эквивалентно filter(spicy=True).

Обратите внимание, что вы можете уменьшить число или запросы, фильтруя фотографии в python:

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

В Django 1.7 существует объект Prefetch(), который позволяет вам управлять поведением 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

Дополнительные примеры использования объекта Prefetch см. в prefetch_related docs.

Ответ 2

Из docs:

... как всегда с QuerySets, любые последующие цепные методы, которые подразумевают другой запрос к базе данных, будут игнорировать ранее полученные в кэше результаты и извлекать данные с использованием нового запроса к базе данных. Итак, если вы пишете следующее:

  

pizzas = Pizza.objects.prefetch_related ('toppings')     [list (pizza.toppings.filter(spicy = True)) для пиццы в пицце]

  

... то тот факт, что pizza.toppings.all() был предварительно запрограммирован, вам не поможет - на самом деле это повредит производительность, поскольку вы сделали запрос к базе данных, который вы не использовали. Поэтому используйте эту функцию с осторожностью!

В вашем случае "a.photo_set.filter(format = 1)" обрабатывается как новый запрос.

Кроме того, "photo_set" - это обратный поиск, реализованный через другого менеджера.