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


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.


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.


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

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.