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

89

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

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? Клянусь, я где-то читал в документации, что он должен это делать ...

Тимммм
источник
возможный дубликат фильтра на prefetch_related в Django
akaihola

Ответы:

166

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

Это объясняется в prefetch_relatedдокументации. 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.photo_set.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документации.

Alasdair
источник
8

Из документов :

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

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

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

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

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

Нгуре Ньяга
источник
photo_setтакже можно предварительно загрузить с помощью .prefetch_related('photo_set'). Но порядок имеет значение, как вы объяснили.
Risadinha