Loading django/db/models/__init__.py +3 −1 Original line number Diff line number Diff line Loading @@ -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 Loading django/db/models/query.py +6 −9 Original line number Diff line number Diff line Loading @@ -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 ################################################## Loading Loading @@ -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) Loading @@ -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): Loading docs/ref/models/querysets.txt +24 −2 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -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. Loading Loading @@ -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') docs/releases/1.10.txt +3 −0 Original line number Diff line number Diff line Loading @@ -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 ~~~~~~~~~~~~~~~~~~~~~~ Loading tests/prefetch_related/test_prefetch_related_objects.py 0 → 100644 +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}) Loading
django/db/models/__init__.py +3 −1 Original line number Diff line number Diff line Loading @@ -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 Loading
django/db/models/query.py +6 −9 Original line number Diff line number Diff line Loading @@ -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 ################################################## Loading Loading @@ -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) Loading @@ -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): Loading
docs/ref/models/querysets.txt +24 −2 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -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. Loading Loading @@ -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')
docs/releases/1.10.txt +3 −0 Original line number Diff line number Diff line Loading @@ -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 ~~~~~~~~~~~~~~~~~~~~~~ Loading
tests/prefetch_related/test_prefetch_related_objects.py 0 → 100644 +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})