diff --git a/django/db/models/query.py b/django/db/models/query.py index 5069c419d1..c5e8b432fe 100644 --- a/django/db/models/query.py +++ b/django/db/models/query.py @@ -1471,10 +1471,15 @@ def prefetch_related_objects(model_instances, *related_lookups): # that we can continue with nullable or reverse relations. new_obj_list = [] for obj in obj_list: - try: - new_obj = getattr(obj, through_attr) - except exceptions.ObjectDoesNotExist: - continue + if through_attr in getattr(obj, '_prefetched_objects_cache', ()): + # If related objects have been prefetched, use the + # cache rather than the object's through_attr. + new_obj = list(obj._prefetched_objects_cache.get(through_attr)) + else: + try: + new_obj = getattr(obj, through_attr) + except exceptions.ObjectDoesNotExist: + continue if new_obj is None: continue # We special-case `list` rather than something more generic diff --git a/docs/releases/1.11.1.txt b/docs/releases/1.11.1.txt index 0b7d6d9efb..ba23d3e17e 100644 --- a/docs/releases/1.11.1.txt +++ b/docs/releases/1.11.1.txt @@ -91,3 +91,6 @@ Bugfixes * Corrected the return type of ``ArrayField(CITextField())`` values retrieved from the database (:ticket:`28161`). + +* Fixed ``QuerySet.prefetch_related()`` crash when fetching relations in nested + ``Prefetch`` objects (:ticket:`27554`). diff --git a/tests/prefetch_related/tests.py b/tests/prefetch_related/tests.py index 23ea2458fc..74f933f7c9 100644 --- a/tests/prefetch_related/tests.py +++ b/tests/prefetch_related/tests.py @@ -1391,3 +1391,37 @@ class Ticket25546Tests(TestCase): self.assertListEqual(book1.first_authors[0].happy_place, [self.author1_address1]) self.assertListEqual(book1.first_authors[1].happy_place, []) self.assertListEqual(book2.first_authors[0].happy_place, [self.author2_address1]) + + +class ReadPrefetchedObjectsCacheTests(TestCase): + @classmethod + def setUpTestData(cls): + cls.book1 = Book.objects.create(title='Les confessions Volume I') + cls.book2 = Book.objects.create(title='Candide') + cls.author1 = AuthorWithAge.objects.create(name='Rousseau', first_book=cls.book1, age=70) + cls.author2 = AuthorWithAge.objects.create(name='Voltaire', first_book=cls.book2, age=65) + cls.book1.authors.add(cls.author1) + cls.book2.authors.add(cls.author2) + FavoriteAuthors.objects.create(author=cls.author1, likes_author=cls.author2) + + def test_retrieves_results_from_prefetched_objects_cache(self): + """ + When intermediary results are prefetched without a destination + attribute, they are saved in the RelatedManager's cache + (_prefetched_objects_cache). prefetch_related() uses this cache + (#27554). + """ + authors = AuthorWithAge.objects.prefetch_related( + Prefetch( + 'author', + queryset=Author.objects.prefetch_related( + # Results are saved in the RelatedManager's cache + # (_prefetched_objects_cache) and do not replace the + # RelatedManager on Author instances (favorite_authors) + Prefetch('favorite_authors__first_book'), + ), + ), + ) + with self.assertNumQueries(4): + # AuthorWithAge -> Author -> FavoriteAuthors, Book + self.assertQuerysetEqual(authors, ['', ''])