Warum funktioniert djangos prefetch_related () nur mit all () und nicht mit filter ()?

87

Angenommen, ich habe dieses Modell:

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

Nun, wenn ich eine Teilmenge von Fotos in einer Teilmenge von Alben effizient betrachten möchte. Ich mache es so ähnlich:

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

Dies führt nur zwei Abfragen aus, was ich erwarte (eine, um die Alben zu erhalten, und eine wie `SELECT * IN photos WHERE photoalbum_id IN ().

Alles ist großartig.

Aber wenn ich das mache:

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

Dann macht es eine Menge Abfragen mit WHERE format = 1! Mache ich etwas falsch oder ist Django nicht klug genug, um zu erkennen, dass es bereits alle Fotos abgerufen hat und sie in Python filtern kann? Ich schwöre, ich habe irgendwo in der Dokumentation gelesen, dass es das tun soll ...

Timmmm
quelle
Mögliches Duplikat von Filter auf prefetch_related in Django
akaihola

Antworten:

164

In Django 1.6 und früheren Versionen können zusätzliche Abfragen nicht vermieden werden. Der prefetch_relatedAufruf a.photoset.all()speichert die Ergebnisse für jedes Album im Abfrageset effektiv zwischen . Da a.photoset.filter(format=1)es sich jedoch um ein anderes Abfrageset handelt, generieren Sie für jedes Album eine zusätzliche Abfrage.

Dies wird in den prefetch_relatedDokumenten erklärt. Das filter(format=1)entspricht filter(spicy=True).

Beachten Sie, dass Sie die Anzahl oder die Abfragen reduzieren können, indem Sie stattdessen die Fotos in Python filtern:

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]

In Django 1.7 gibt es ein Prefetch()Objekt, mit dem Sie das Verhalten von steuern können 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

Weitere Beispiele zur Verwendung des PrefetchObjekts finden Sie in den prefetch_relatedDokumenten.

Alasdair
quelle
7

Aus den Dokumenten :

... wie immer bei QuerySets ignorieren alle nachfolgenden verketteten Methoden, die eine andere Datenbankabfrage implizieren, zuvor zwischengespeicherte Ergebnisse und rufen Daten mithilfe einer neuen Datenbankabfrage ab. Wenn Sie also Folgendes schreiben:

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

... dann hilft Ihnen die Tatsache, dass pizza.toppings.all () vorab abgerufen wurde, nicht weiter - tatsächlich beeinträchtigt dies die Leistung, da Sie eine Datenbankabfrage durchgeführt haben, die Sie nicht verwendet haben. Verwenden Sie diese Funktion daher mit Vorsicht!

In Ihrem Fall wird "a.photo_set.filter (format = 1)" wie eine neue Abfrage behandelt.

Darüber hinaus ist "photo_set" eine umgekehrte Suche, die über einen anderen Manager implementiert wird.

Ngure Nyaga
quelle
photo_setkann auch mit vorabgerufen werden .prefetch_related('photo_set'). Aber Ordnung ist wichtig, wie Sie erklärt haben.
Risadinha