Commit ef33bc2d authored by Adam Chainz's avatar Adam Chainz Committed by Tim Graham
Browse files

Fixed #25279 -- Made prefetch_related_objects() public.

parent d5f89ff6
Loading
Loading
Loading
Loading
+3 −1
Original line number Diff line number Diff line
@@ -14,7 +14,9 @@ from django.db.models.fields.files import FileField, ImageField # NOQA
from django.db.models.fields.proxy import OrderWrt  # NOQA
from django.db.models.lookups import Lookup, Transform  # NOQA
from django.db.models.manager import Manager  # NOQA
from django.db.models.query import Q, Prefetch, QuerySet  # NOQA
from django.db.models.query import (  # NOQA
    Q, Prefetch, QuerySet, prefetch_related_objects,
)

# Imports that would create circular imports if sorted
from django.db.models.base import Model  # NOQA isort:skip
+6 −9
Original line number Diff line number Diff line
@@ -654,7 +654,7 @@ class QuerySet(object):

    def _prefetch_related_objects(self):
        # This method can only be called once the result cache has been filled.
        prefetch_related_objects(self._result_cache, self._prefetch_related_lookups)
        prefetch_related_objects(self._result_cache, *self._prefetch_related_lookups)
        self._prefetch_done = True

    ##################################################
@@ -1368,15 +1368,12 @@ def normalize_prefetch_lookups(lookups, prefix=None):
    return ret


def prefetch_related_objects(result_cache, related_lookups):
def prefetch_related_objects(model_instances, *related_lookups):
    """
    Helper function for prefetch_related functionality

    Populates prefetched objects caches for a list of results
    from a QuerySet
    Populate prefetched object caches for a list of model instances based on
    the lookups/Prefetch instances given.
    """

    if len(result_cache) == 0:
    if len(model_instances) == 0:
        return  # nothing to do

    related_lookups = normalize_prefetch_lookups(related_lookups)
@@ -1401,7 +1398,7 @@ def prefetch_related_objects(result_cache, related_lookups):

        # Top level, the list of objects to decorate is the result cache
        # from the primary QuerySet. It won't be for deeper levels.
        obj_list = result_cache
        obj_list = model_instances

        through_attrs = lookup.prefetch_through.split(LOOKUP_SEP)
        for level, through_attr in enumerate(through_attrs):
+24 −2
Original line number Diff line number Diff line
@@ -920,6 +920,10 @@ results; these ``QuerySets`` are then used in the ``self.toppings.all()`` calls.
The additional queries in ``prefetch_related()`` are executed after the
``QuerySet`` has begun to be evaluated and the primary query has been executed.

If you have an iterable of model instances, you can prefetch related attributes
on those instances using the :func:`~django.db.models.prefetch_related_objects`
function.

Note that the result cache of the primary ``QuerySet`` and all specified related
objects will then be fully loaded into memory. This changes the typical
behavior of ``QuerySets``, which normally try to avoid loading all objects into
@@ -2998,8 +3002,8 @@ by the aggregate.

.. _SQLite documentation: https://www.sqlite.org/contrib

Query-related classes
=====================
Query-related tools
===================

This section provides reference material for query-related tools not documented
elsewhere.
@@ -3064,3 +3068,21 @@ attribute:
    provide a significant speed improvement over traditional
    ``prefetch_related`` calls which store the cached result within a
    ``QuerySet`` instance.

``prefetch_related_objects()``
------------------------------

.. function:: prefetch_related_objects(model_instances, *related_lookups)

.. versionadded:: 1.10

Prefetches the given lookups on an iterable of model instances. This is useful
in code that receives a list of model instances as opposed to a ``QuerySet``;
for example, when fetching models from a cache or instantiating them manually.

Pass an iterable of model instances (must all be of the same class) and the
lookups or :class:`Prefetch` objects you want to prefetch for. For example::

    >>> from django.db.models import prefetch_related_objects
    >>> restaurants = fetch_top_restaurants_from_cache()  # A list of Restaurants
    >>> prefetch_related_objects(restaurants, 'pizzas__toppings')
+3 −0
Original line number Diff line number Diff line
@@ -312,6 +312,9 @@ Models
  app label and class interpolation using the ``'%(app_label)s'`` and
  ``'%(class)s'`` strings.

* The :func:`~django.db.models.prefetch_related_objects` function is now a
  public API.

Requests and Responses
~~~~~~~~~~~~~~~~~~~~~~

+119 −0
Original line number Diff line number Diff line
from django.db.models import Prefetch, prefetch_related_objects
from django.test import TestCase

from .models import Author, Book, Reader


class PrefetchRelatedObjectsTests(TestCase):
    """
    Since prefetch_related_objects() is just the inner part of
    prefetch_related(), only do basic tests to ensure its API hasn't changed.
    """
    @classmethod
    def setUpTestData(cls):
        cls.book1 = Book.objects.create(title='Poems')
        cls.book2 = Book.objects.create(title='Jane Eyre')
        cls.book3 = Book.objects.create(title='Wuthering Heights')
        cls.book4 = Book.objects.create(title='Sense and Sensibility')

        cls.author1 = Author.objects.create(name='Charlotte', first_book=cls.book1)
        cls.author2 = Author.objects.create(name='Anne', first_book=cls.book1)
        cls.author3 = Author.objects.create(name='Emily', first_book=cls.book1)
        cls.author4 = Author.objects.create(name='Jane', first_book=cls.book4)

        cls.book1.authors.add(cls.author1, cls.author2, cls.author3)
        cls.book2.authors.add(cls.author1)
        cls.book3.authors.add(cls.author3)
        cls.book4.authors.add(cls.author4)

        cls.reader1 = Reader.objects.create(name='Amy')
        cls.reader2 = Reader.objects.create(name='Belinda')

        cls.reader1.books_read.add(cls.book1, cls.book4)
        cls.reader2.books_read.add(cls.book2, cls.book4)

    def test_unknown(self):
        book1 = Book.objects.get(id=self.book1.id)
        with self.assertRaises(AttributeError):
            prefetch_related_objects([book1], 'unknown_attribute')

    def test_m2m_forward(self):
        book1 = Book.objects.get(id=self.book1.id)
        with self.assertNumQueries(1):
            prefetch_related_objects([book1], 'authors')

        with self.assertNumQueries(0):
            self.assertEqual(set(book1.authors.all()), {self.author1, self.author2, self.author3})

    def test_m2m_reverse(self):
        author1 = Author.objects.get(id=self.author1.id)
        with self.assertNumQueries(1):
            prefetch_related_objects([author1], 'books')

        with self.assertNumQueries(0):
            self.assertEqual(set(author1.books.all()), {self.book1, self.book2})

    def test_foreignkey_forward(self):
        authors = list(Author.objects.all())
        with self.assertNumQueries(1):
            prefetch_related_objects(authors, 'first_book')

        with self.assertNumQueries(0):
            [author.first_book for author in authors]

    def test_foreignkey_reverse(self):
        books = list(Book.objects.all())
        with self.assertNumQueries(1):
            prefetch_related_objects(books, 'first_time_authors')

        with self.assertNumQueries(0):
            [list(book.first_time_authors.all()) for book in books]

    def test_m2m_then_m2m(self):
        """
        We can follow a m2m and another m2m.
        """
        authors = list(Author.objects.all())
        with self.assertNumQueries(2):
            prefetch_related_objects(authors, 'books__read_by')

        with self.assertNumQueries(0):
            self.assertEqual(
                [
                    [[str(r) for r in b.read_by.all()] for b in a.books.all()]
                    for a in authors
                ],
                [
                    [['Amy'], ['Belinda']],  # Charlotte - Poems, Jane Eyre
                    [['Amy']],               # Anne - Poems
                    [['Amy'], []],           # Emily - Poems, Wuthering Heights
                    [['Amy', 'Belinda']],    # Jane - Sense and Sense
                ]
            )

    def test_prefetch_object(self):
        book1 = Book.objects.get(id=self.book1.id)
        with self.assertNumQueries(1):
            prefetch_related_objects([book1], Prefetch('authors'))

        with self.assertNumQueries(0):
            self.assertEqual(set(book1.authors.all()), {self.author1, self.author2, self.author3})

    def test_prefetch_object_to_attr(self):
        book1 = Book.objects.get(id=self.book1.id)
        with self.assertNumQueries(1):
            prefetch_related_objects([book1], Prefetch('authors', to_attr='the_authors'))

        with self.assertNumQueries(0):
            self.assertEqual(set(book1.the_authors), {self.author1, self.author2, self.author3})

    def test_prefetch_queryset(self):
        book1 = Book.objects.get(id=self.book1.id)
        with self.assertNumQueries(1):
            prefetch_related_objects(
                [book1],
                Prefetch('authors', queryset=Author.objects.filter(id__in=[self.author1.id, self.author2.id]))
            )

        with self.assertNumQueries(0):
            self.assertEqual(set(book1.authors.all()), {self.author1, self.author2})