Fixed #27554 -- Fixed prefetch_related() crash when fetching relations in nested Prefetches.

This commit is contained in:
François Freitag 2017-03-03 18:45:31 -08:00 committed by Tim Graham
parent 16121da78d
commit c0a2b9508a
3 changed files with 46 additions and 4 deletions

View File

@ -1423,10 +1423,15 @@ def prefetch_related_objects(model_instances, *related_lookups):
# that we can continue with nullable or reverse relations. # that we can continue with nullable or reverse relations.
new_obj_list = [] new_obj_list = []
for obj in obj_list: for obj in obj_list:
try: if through_attr in getattr(obj, '_prefetched_objects_cache', ()):
new_obj = getattr(obj, through_attr) # If related objects have been prefetched, use the
except exceptions.ObjectDoesNotExist: # cache rather than the object's through_attr.
continue 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: if new_obj is None:
continue continue
# We special-case `list` rather than something more generic # We special-case `list` rather than something more generic

View File

@ -91,3 +91,6 @@ Bugfixes
* Corrected the return type of ``ArrayField(CITextField())`` values retrieved * Corrected the return type of ``ArrayField(CITextField())`` values retrieved
from the database (:ticket:`28161`). from the database (:ticket:`28161`).
* Fixed ``QuerySet.prefetch_related()`` crash when fetching relations in nested
``Prefetch`` objects (:ticket:`27554`).

View File

@ -1361,3 +1361,37 @@ class Ticket25546Tests(TestCase):
self.assertEqual(book1.first_authors[0].happy_place, [self.author1_address1]) self.assertEqual(book1.first_authors[0].happy_place, [self.author1_address1])
self.assertEqual(book1.first_authors[1].happy_place, []) self.assertEqual(book1.first_authors[1].happy_place, [])
self.assertEqual(book2.first_authors[0].happy_place, [self.author2_address1]) self.assertEqual(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, ['<AuthorWithAge: Rousseau>', '<AuthorWithAge: Voltaire>'])