Fixed #26676 -- Prevented prefetching to_attr from caching its result in through attr.

Thanks Ursidours for the report.
This commit is contained in:
Simon Charette 2016-05-30 00:11:31 -04:00
parent 359be1c870
commit 53a5fb3cc0
2 changed files with 19 additions and 10 deletions

View File

@ -1451,7 +1451,8 @@ def prefetch_related_objects(model_instances, *related_lookups):
# We assume that objects retrieved are homogeneous (which is the premise # We assume that objects retrieved are homogeneous (which is the premise
# of prefetch_related), so what applies to first object applies to all. # of prefetch_related), so what applies to first object applies to all.
first_obj = obj_list[0] first_obj = obj_list[0]
prefetcher, descriptor, attr_found, is_fetched = get_prefetcher(first_obj, through_attr) to_attr = lookup.get_current_to_attr(level)[0]
prefetcher, descriptor, attr_found, is_fetched = get_prefetcher(first_obj, through_attr, to_attr)
if not attr_found: if not attr_found:
raise AttributeError("Cannot find '%s' on %s object, '%s' is an invalid " raise AttributeError("Cannot find '%s' on %s object, '%s' is an invalid "
@ -1504,9 +1505,9 @@ def prefetch_related_objects(model_instances, *related_lookups):
obj_list = new_obj_list obj_list = new_obj_list
def get_prefetcher(instance, attr): def get_prefetcher(instance, through_attr, to_attr):
""" """
For the attribute 'attr' on the given instance, finds For the attribute 'through_attr' on the given instance, finds
an object that has a get_prefetch_queryset(). an object that has a get_prefetch_queryset().
Returns a 4 tuple containing: Returns a 4 tuple containing:
(the object with get_prefetch_queryset (or None), (the object with get_prefetch_queryset (or None),
@ -1520,9 +1521,9 @@ def get_prefetcher(instance, attr):
# For singly related objects, we have to avoid getting the attribute # For singly related objects, we have to avoid getting the attribute
# from the object, as this will trigger the query. So we first try # from the object, as this will trigger the query. So we first try
# on the class, in order to get the descriptor object. # on the class, in order to get the descriptor object.
rel_obj_descriptor = getattr(instance.__class__, attr, None) rel_obj_descriptor = getattr(instance.__class__, through_attr, None)
if rel_obj_descriptor is None: if rel_obj_descriptor is None:
attr_found = hasattr(instance, attr) attr_found = hasattr(instance, through_attr)
else: else:
attr_found = True attr_found = True
if rel_obj_descriptor: if rel_obj_descriptor:
@ -1536,10 +1537,13 @@ def get_prefetcher(instance, attr):
# descriptor doesn't support prefetching, so we go ahead and get # descriptor doesn't support prefetching, so we go ahead and get
# the attribute on the instance rather than the class to # the attribute on the instance rather than the class to
# support many related managers # support many related managers
rel_obj = getattr(instance, attr) rel_obj = getattr(instance, through_attr)
if hasattr(rel_obj, 'get_prefetch_queryset'): if hasattr(rel_obj, 'get_prefetch_queryset'):
prefetcher = rel_obj prefetcher = rel_obj
is_fetched = attr in instance._prefetched_objects_cache if through_attr != to_attr:
is_fetched = hasattr(instance, to_attr)
else:
is_fetched = through_attr in instance._prefetched_objects_cache
return prefetcher, rel_obj_descriptor, attr_found, is_fetched return prefetcher, rel_obj_descriptor, attr_found, is_fetched
@ -1619,7 +1623,6 @@ def prefetch_one_level(instances, prefetcher, lookup, level):
else: else:
if as_attr: if as_attr:
setattr(obj, to_attr, vals) setattr(obj, to_attr, vals)
obj._prefetched_objects_cache[cache_name] = vals
else: else:
manager = getattr(obj, to_attr) manager = getattr(obj, to_attr)
if leaf and lookup.queryset is not None: if leaf and lookup.queryset is not None:

View File

@ -5,7 +5,7 @@ import warnings
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
from django.db import connection from django.db import connection
from django.db.models import Prefetch from django.db.models import Prefetch, QuerySet
from django.db.models.query import get_prefetcher from django.db.models.query import get_prefetcher
from django.test import TestCase, override_settings from django.test import TestCase, override_settings
from django.test.utils import CaptureQueriesContext from django.test.utils import CaptureQueriesContext
@ -737,6 +737,12 @@ class CustomPrefetchTests(TestCase):
with self.assertRaisesMessage(ValueError, 'Prefetch querysets cannot use values().'): with self.assertRaisesMessage(ValueError, 'Prefetch querysets cannot use values().'):
Prefetch('houses', House.objects.values('pk')) Prefetch('houses', House.objects.values('pk'))
def test_to_attr_doesnt_cache_through_attr_as_list(self):
house = House.objects.prefetch_related(
Prefetch('rooms', queryset=Room.objects.all(), to_attr='to_rooms'),
).get(pk=self.house3.pk)
self.assertIsInstance(house.rooms.all(), QuerySet)
class DefaultManagerTests(TestCase): class DefaultManagerTests(TestCase):
@ -1268,7 +1274,7 @@ class Ticket21760Tests(TestCase):
house.save() house.save()
def test_bug(self): def test_bug(self):
prefetcher = get_prefetcher(self.rooms[0], 'house')[0] prefetcher = get_prefetcher(self.rooms[0], 'house', 'house')[0]
queryset = prefetcher.get_prefetch_queryset(list(Room.objects.all()))[0] queryset = prefetcher.get_prefetch_queryset(list(Room.objects.all()))[0]
self.assertNotIn(' JOIN ', force_text(queryset.query)) self.assertNotIn(' JOIN ', force_text(queryset.query))