Waarom werkt django’s prefetch_related() alleen met all() en niet met filter()?

stel dat ik dit model heb:

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

Als ik nu efficiënt naar een subset van foto’s in een subset van albums wil kijken. Ik doe het ongeveer als volgt:

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

Dit doet slechts twee zoekopdrachten, wat ik verwacht (een om de albums te krijgen, en dan een zoals `SELECT * IN foto’s WHERE photoalbum_id IN ().

Alles is geweldig.

Maar als ik dit doe:

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

Vervolgens doet het een heleboel queries met WHERE format = 1! Doe ik iets verkeerd of is django niet slim genoeg om te beseffen dat het alle foto’s al heeft opgehaald en ze in python kan filteren? Ik zweer dat ik ergens in de documentatie heb gelezen dat het dat zou moeten doen…


Antwoord 1, autoriteit 100%

In Django 1.6 en eerder is het niet mogelijk om de extra zoekopdrachten te vermijden. De aanroep prefetch_relatedslaat effectief de resultaten van a.photoset.all()op in de cache voor elk album in de queryset. a.photoset.filter(format=1)is echter een andere queryset, dus u genereert voor elk album een ​​extra query.

Dit wordt uitgelegd in de prefetch_relateddoc. Het filter(format=1)is gelijk aan filter(spicy=True).

Houd er rekening mee dat u het aantal zoekopdrachten kunt verminderen door in plaats daarvan de foto’s in python te filteren:

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 is er een Prefetch()object waarmee u het gedrag van prefetch_relatedkunt regelen.

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

Voor meer voorbeelden van het gebruik van het Prefetch-object, zie de prefetch_relateddocumenten.


Antwoord 2, autoriteit 5%

Van de docs:

…zoals altijd bij QuerySets, zullen alle daaropvolgende geketende methoden die een andere databasequery impliceren eerder in de cache opgeslagen resultaten negeren en gegevens ophalen met een nieuwe databasequery. Dus, als je het volgende schrijft:

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

…dan zal het feit dat pizza.toppings.all() vooraf is opgehaald je niet helpen – in feite doet het de prestaties afbreuk, aangezien je een databasequery hebt uitgevoerd die je niet hebt gebruikt. Gebruik deze functie dus voorzichtig!

In jouw geval wordt “a.photo_set.filter(format=1)” behandeld als een nieuwe zoekopdracht.

Bovendien is “photo_set” een omgekeerde zoekactie – geïmplementeerd via een geheel andere beheerder.


Antwoord 3

Je kunt select_relatedgebruiken als je het wilt gebruiken met filter()

results = Geography.objects.filter(state__pk = 1).select_related('country')
results.query

Voor meer informatie: https://docs.djangoproject .com/en/3.1/ref/models/querysets/#select-related

Other episodes