Let's look at example from django docs with Pizza and Topping models. One pizza may have multiple toppings.
If we make a query:
pizzas = Pizza.objects.prefetch_related('toppings')
We'll get all the pizzas and their toppings in 2 queries. Now let's suppose that I want to prefetch only vegetarian toppings (assume we have such property):
pizzas = Pizza.objects.prefetch_related(
Prefetch('toppings', queryset=Topping.objects.filter(is_vegetarian=True))
)
It works pretty well and Django doesn't perform yet another query for each pizza, when making something like this:
for pizza in pizzas:
print(pizza.toppings.filter(is_vegetarian=True))
Now let's suppose We have a custom manager for Topping model and we decided to put there a method that allows us to filter only vegetarian toppings like in code example above:
class ToppingManager(models.Manager):
def filter_vegetarian(self):
return self.filter(is_vegetarian=True)
Now I make a new query and prefetch custom queryset with my method from manager:
pizzas = Pizza.objects.prefetch_related(
Prefetch('toppings', queryset=Topping.objects.filter_vegetarian()))
And the try to execute my code:
for pizza in pizzas:
print(pizza.toppings.filter_vegeterian())
I get a new one query for each iteration of the loop. That is my question. Why? Both these constructions return the same type object which is queryset:
Topping.objects.filter_vegetarian()
Topping.objects.filter(is_vegetarian=True)
Django's prefetch_related() method reduces the number of queries made to your database by combining all related model lookups into one additional query. In the professionals list example, we can use prefetch_related() to get the professionals and their related work experiences and references in 3 queries instead of 21.
A QuerySet is a collection of data from a database. A QuerySet is built up as a list of objects. QuerySets makes it easier to get the data you actually need, by allowing you to filter and order the data.
A QuerySet represents a collection of objects from your database. It can have zero, one or many filters. Filters narrow down the query results based on the given parameters. In SQL terms, a QuerySet equates to a SELECT statement, and a filter is a limiting clause such as WHERE or LIMIT .
I haven't tested this directly, but you should not invoke a method or filter again in the loop, as prefetch_related has already attached the data. So either of these should work:
pizzas = Pizza.objects.prefetch_related(
Prefetch('toppings', queryset=Topping.objects.filter(is_vegetarian=True))
)
for pizza in pizzas:
print(pizza.toppings.all()) # uses prefetched queryset
or
pizzas = Pizza.objects.prefetch_related(
Prefetch('toppings', queryset=Topping.objects.filter_vegetarian(),
to_attr="veg_toppings"))
for pizza in pizzas:
print(pizza.toppings.veg_toppings)
Your examples do not work because they invoke another queryset, and this cannot be compared to the prefetched one to determine if it would be same.
It also says so in the docs:
The
prefetch_related('toppings')
impliedpizza.toppings.all()
, butpizza.toppings.filter()
is a new and different query. The prefetched cache can’t help here; in fact it hurts performance, since you have done a database query that you haven’t used.
and
Using to_attr is recommended when filtering down the prefetch result as it is less ambiguous than storing a filtered result in the related manager’s cache.
This implementation:
class ToppingManager(models.Manager):
def filter_vegetarian(self):
return self.filter(is_vegetarian=True)
Looks non-standard. docs look like they do a safer method of modifying the super-class method for this sort of lazy-eval stuff. If I rewrite your method in that style, it would look like:
class ToppingManager(models.Manager):
def filter_vegetarian(self):
return super(ToppingManager, self).get_queryset().filter(is_vegetarian=True)
You wouldn't strictly need the super() here, but safer to use it because you should know that you want to start with the models.Manager get_queryset method.
Doing a brief test of this in my own environment, I find that it works feeding into Prefetch
without triggering queries on each item. I do not have any reason to believe this would not work for the problem here.
However, I'm also inclined to believe that specifying to_attr
in webjunkie's answer may also be necessary.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With