web-dev-qa-db-fra.com

Pourquoi prefetch_related () de Django fonctionne-t-il uniquement avec all () et pas filter ()?

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 le 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 à quoi je m'attends (une pour obtenir les albums, puis une autre 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 Django n'est pas assez intelligent pour réaliser qu'il a déjà récupéré toutes les photos et qu'il peut les filtrer en python? Je jure d'avoir lu quelque part dans la documentation ce qu'il est supposé faire cette...

78
Timmmm

Dans Django 1.6 et versions antérieures, il n'est pas possible d'éviter les requêtes supplémentaires. L'appel prefetch_related Met effectivement en cache les résultats de a.photoset.all() pour chaque album de la Cependant, comme a.photoset.filter(format=1) est un ensemble de requêtes différent, vous allez générer une requête supplémentaire pour chaque album.

Ceci est expliqué dans la prefetch_related docs. La filter(format=1) est équivalente à filter(spicy=True).

Notez que vous pouvez réduire le nombre de requêtes en filtrant les photos dans 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 objet Prefetch() 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'objet Prefetch, reportez-vous à la documentation prefetch_related .

147
Alasdair

De la docs :

... comme toujours avec QuerySets, toutes les méthodes chaînées ultérieures impliquant une requête de base de données différente ignoreront les résultats précédemment mis en cache et récupéreront les données à l'aide d'une requête de base de données récente. 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, les performances en sont affectées, car vous avez effectué une requête de base de données que vous n'avez pas utilisée. Utilisez donc cette fonctionnalité avec prudence!

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

De plus, "photo_set" est une recherche inversée - mise en œuvre via un tout autre gestionnaire.

7
Ngure Nyaga