为什么Django的prefetch_related()只能与all()一起使用,而不能与filter()搭配使用?

102

假设我有这个模型:

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中对它们进行过滤?我发誓在文档某处读到过应该这样做......


2个回答

198
在Django 1.6及更早版本中,无法避免额外的查询。 prefetch_related调用实际上会缓存查询集中每个相册的a.photoset.all()结果。但是,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 文档。


11
文档中得知:

...与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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接