Por que o prefetch_related () do django só funciona com all () e não com filter ()?

90

suponha que eu tenha este modelo:

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

Agora, se eu quiser olhar um subconjunto de fotos em um subconjunto de álbuns de forma eficiente. Eu faço algo assim:

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

Isso faz apenas duas consultas, que é o que eu espero (uma para obter os álbuns e outra como `SELECT * IN photos WHERE photoalbum_id IN ().

Tudo é bom.

Mas se eu fizer isso:

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

Em seguida, ele faz uma tonelada de consultas com WHERE format = 1! Estou fazendo algo errado ou o Django não é inteligente o suficiente para perceber que já pegou todas as fotos e pode filtrá-las em python? Juro que li em algum lugar da documentação que é para fazer isso ...

Timmmm
fonte
possível duplicata do Filtro em prefetch_related no Django
akaihola

Respostas:

168

No Django 1.6 e anteriores, não é possível evitar as consultas extras. A prefetch_relatedchamada efetivamente armazena em cache os resultados de a.photoset.all()para cada álbum no queryset. No entanto, a.photoset.filter(format=1)é um queryset diferente, então você irá gerar uma consulta extra para cada álbum.

Isso é explicado nos prefetch_relateddocumentos. O filter(format=1)é equivalente a filter(spicy=True).

Observe que você pode reduzir o número de consultas filtrando as fotos em 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]

No Django 1.7, existe um Prefetch()objeto que permite controlar o comportamento de 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

Para obter mais exemplos de como usar o Prefetchobjeto, consulte a prefetch_relateddocumentação.

Alasdair
fonte
8

Dos documentos :

... como sempre com QuerySets, qualquer método subsequente encadeado que implique em uma consulta de banco de dados diferente irá ignorar os resultados previamente armazenados em cache e recuperar dados usando uma nova consulta de banco de dados. Então, se você escrever o seguinte:

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

... então o fato de pizza.toppings.all () ter sido pré-buscada não irá ajudá-lo - na verdade, prejudica o desempenho, já que você fez uma consulta ao banco de dados que não usou. Portanto, use esse recurso com cuidado!

No seu caso, "a.photo_set.filter (format = 1)" é tratado como uma nova consulta.

Além disso, "photo_set" é uma pesquisa reversa - implementada por meio de um gerenciador diferente.

Ngure Nyaga
fonte
photo_settambém pode ser pré-obtido com .prefetch_related('photo_set'). Mas a ordem é importante, como você explicou.
Risadinha