Fixed #17001 -- Custom querysets for prefetch_related.

This patch introduces the Prefetch object which allows customizing prefetch
operations.

This enables things like filtering prefetched relations, calling select_related
from a prefetched relation, or prefetching the same relation multiple times
with different querysets.

When a Prefetch instance specifies a to_attr argument, the result is stored
in a list rather than a QuerySet. This has the fortunate consequence of being
significantly faster. The preformance improvement is due to the fact that we
save the costly creation of a QuerySet instance.

Thanks @akaariai for the original patch and @bmispelon and @timgraham
for the reviews.
This commit is contained in:
Loic Bistuer 2013-11-07 00:25:05 +07:00 committed by Anssi Kääriäinen
parent b1b04df065
commit f51c1f5900
9 changed files with 616 additions and 65 deletions

View file

@ -129,3 +129,32 @@ In general, ``Q() objects`` make it possible to define and reuse conditions.
This permits the :ref:`construction of complex database queries
<complex-lookups-with-q>` using ``|`` (``OR``) and ``&`` (``AND``) operators;
in particular, it is not otherwise possible to use ``OR`` in ``QuerySets``.
``Prefetch()`` objects
======================
.. versionadded:: 1.7
.. class:: Prefetch(lookup, queryset=None, to_attr=None)
The ``Prefetch()`` object can be used to control the operation of
:meth:`~django.db.models.query.QuerySet.prefetch_related()`.
The ``lookup`` argument describes the relations to follow and works the same
as the string based lookups passed to
:meth:`~django.db.models.query.QuerySet.prefetch_related()`.
The ``queryset`` argument supplies a base ``QuerySet`` for the given lookup.
This is useful to further filter down the prefetch operation, or to call
:meth:`~django.db.models.query.QuerySet.select_related()` from the prefetched
relation, hence reducing the number of queries even further.
The ``to_attr`` argument sets the result of the prefetch operation to a custom
attribute.
.. note::
When using ``to_attr`` the prefetched result is stored in a list.
This can provide a significant speed improvement over traditional
``prefetch_related`` calls which store the cached result within a
``QuerySet`` instance.

View file

@ -898,7 +898,7 @@ objects have already been fetched, and it will skip fetching them again.
Chaining ``prefetch_related`` calls will accumulate the lookups that are
prefetched. To clear any ``prefetch_related`` behavior, pass ``None`` as a
parameter::
parameter:
>>> non_prefetched = qs.prefetch_related(None)
@ -925,6 +925,91 @@ profile for your use case!
Note that if you use ``iterator()`` to run the query, ``prefetch_related()``
calls will be ignored since these two optimizations do not make sense together.
.. versionadded:: 1.7
You can use the :class:`~django.db.models.Prefetch` object to further control
the prefetch operation.
In its simplest form ``Prefetch`` is equivalent to the traditional string based
lookups:
>>> Restaurant.objects.prefetch_related(Prefetch('pizzas__toppings'))
You can provide a custom queryset with the optional ``queryset`` argument.
This can be used to change the default ordering of the queryset:
>>> Restaurant.objects.prefetch_related(
... Prefetch('pizzas__toppings', queryset=Toppings.objects.order_by('name')))
Or to call :meth:`~django.db.models.query.QuerySet.select_related()` when
applicable to reduce the number of queries even further:
>>> Pizza.objects.prefetch_related(
... Prefetch('restaurants', queryset=Restaurant.objects.select_related('best_pizza')))
You can also assign the prefetched result to a custom attribute with the optional
``to_attr`` argument. The result will be stored directly in a list.
This allows prefetching the same relation multiple times with a different
``QuerySet``; for instance:
>>> vegetarian_pizzas = Pizza.objects.filter(vegetarian=True)
>>> Restaurant.objects.prefetch_related(
... Prefetch('pizzas', to_attr('menu')),
... Prefetch('pizzas', queryset=vegetarian_pizzas to_attr='vegetarian_menu'))
Lookups created with custom ``to_attr`` can still be traversed as usual by other
lookups:
>>> vegetarian_pizzas = Pizza.objects.filter(vegetarian=True)
>>> Restaurant.objects.prefetch_related(
... Prefetch('pizzas', queryset=vegetarian_pizzas to_attr='vegetarian_menu'),
... 'vegetarian_menu__toppings')
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:
>>> queryset = Pizza.objects.filter(vegetarian=True)
>>>
>>> # Recommended:
>>> restaurants = Restaurant.objects.prefetch_related(
... Prefetch('pizzas', to_attr='vegetarian_pizzas' queryset=queryset))
>>> vegetarian_pizzas = restaurants[0].vegetarian_pizzas
>>>
>>> # Not recommended:
>>> restaurants = Restaurant.objects.prefetch_related(
... Prefetch('pizzas', queryset=queryset))
>>> vegetarian_pizzas = restaurants[0].pizzas.all()
.. note::
The ordering of lookups matters.
Take the following examples:
>>> prefetch_related('pizzas__toppings', 'pizzas')
This works even though it's unordered because ``'pizzas__toppings'``
already contains all the needed information, therefore the second argument
``'pizzas'`` is actually redundant.
>>> prefetch_related('pizzas__toppings', Prefetch('pizzas', queryset=Pizza.objects.all()))
This will raise a ``ValueError`` because of the attempt to redefine the
queryset of a previously seen lookup. Note that an implicit queryset was
created to traverse ``'pizzas'`` as part of the ``'pizzas__toppings'``
lookup.
>>> prefetch_related('pizza_list__toppings', Prefetch('pizzas', to_attr='pizza_list'))
This will trigger an ``AttributeError`` because ``'pizza_list'`` doesn't exist yet
when ``'pizza_list__toppings'`` is being processed.
This consideration is not limited to the use of ``Prefetch`` objects. Some
advanced techniques may require that the lookups be performed in a
specific order to avoid creating extra queries; therefore it's recommended
to always carefully order ``prefetch_related`` arguments.
extra
~~~~~