summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFrançois Freitag <mail@franek.fr>2017-03-03 18:45:31 -0800
committerTim Graham <timograham@gmail.com>2017-05-04 09:17:29 -0400
commitc0a2b9508aa2c6eaa22e9787010276edca887f0b (patch)
tree011a500063926e0f754d342afff242caa28e4efd
parent16121da78d7a522bf6b6e5fe704eb655b9ab442d (diff)
Fixed #27554 -- Fixed prefetch_related() crash when fetching relations in nested Prefetches.
-rw-r--r--django/db/models/query.py13
-rw-r--r--docs/releases/1.11.1.txt3
-rw-r--r--tests/prefetch_related/tests.py34
3 files changed, 46 insertions, 4 deletions
diff --git a/django/db/models/query.py b/django/db/models/query.py
index 7302a1dfee..1cc5217d0e 100644
--- a/django/db/models/query.py
+++ b/django/db/models/query.py
@@ -1423,10 +1423,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 5ebd39db87..5289661ad7 100644
--- a/tests/prefetch_related/tests.py
+++ b/tests/prefetch_related/tests.py
@@ -1361,3 +1361,37 @@ class Ticket25546Tests(TestCase):
self.assertEqual(book1.first_authors[0].happy_place, [self.author1_address1])
self.assertEqual(book1.first_authors[1].happy_place, [])
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>'])