Pourquoi prefetch_related () de django ne fonctionne-t-il qu'avec all () et pas filter ()?

89

supposons que j'ai ce modèle:

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

Maintenant, si je veux regarder efficacement un sous-ensemble de photos dans un sous-ensemble d'albums. Je fais quelque chose comme ça:

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

Cela ne fait que deux requêtes, ce que j'attends (une pour obtenir les albums, puis une comme `SELECT * IN photos WHERE photoalbum_id IN ().

Tout est bon.

Mais si je fais ça:

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

Ensuite, il fait une tonne de requêtes avec WHERE format = 1! Est-ce que je fais quelque chose de mal ou est-ce que Django n'est pas assez intelligent pour se rendre compte qu'il a déjà récupéré toutes les photos et peut les filtrer en python? Je jure que j'ai lu quelque part dans la documentation qu'il est censé le faire ...

Timmmm
la source
duplication possible du filtre sur prefetch_related dans Django
akaihola

Réponses:

166

Dans Django 1.6 et les versions antérieures, il n'est pas possible d'éviter les requêtes supplémentaires. L' prefetch_relatedappel met effectivement en cache les résultats de a.photoset.all()pour chaque album de l'ensemble de requêtes. Cependant, il a.photoset.filter(format=1)s'agit d'un ensemble de requêtes différent, vous générerez donc une requête supplémentaire pour chaque album.

Ceci est expliqué dans la prefetch_relateddocumentation. Le filter(format=1)équivaut à filter(spicy=True).

Notez que vous pouvez réduire le nombre de requêtes en filtrant les photos en python à la place:

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]

Dans Django 1.7, il existe un Prefetch()objet qui vous permet de contrôler le comportement 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

Pour plus d'exemples d'utilisation de l' Prefetchobjet, consultez la prefetch_relateddocumentation.

Alasdair
la source
8

À partir de la documentation :

... comme toujours avec QuerySets, toutes les méthodes chaînées ultérieures qui impliquent une requête de base de données différente ignorent les résultats précédemment mis en cache et récupèrent les données à l'aide d'une nouvelle requête de base de données. Donc, si vous écrivez ce qui suit:

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

... alors le fait que pizza.toppings.all () ait été préchargé ne vous aidera pas - en fait, cela nuit aux performances, car vous avez effectué une requête de base de données que vous n'avez pas utilisée. Alors utilisez cette fonctionnalité avec prudence!

Dans votre cas, "a.photo_set.filter (format = 1)" est traité comme une nouvelle requête.

De plus, "photo_set" est une recherche inversée - implémentée via un gestionnaire complètement différent.

Ngure Nyaga
la source
photo_setpeut également être préchargé avec .prefetch_related('photo_set'). Mais l'ordre compte, comme vous l'avez expliqué.
Risadinha