mirror of
				https://github.com/django/django.git
				synced 2025-10-31 09:41:08 +00:00 
			
		
		
		
	[1.10.x] Fixed #26676 -- Prevented prefetching to_attr from caching its result in through attr.
Thanks Ursidours for the report.
Backport of 53a5fb3cc0 from master
			
			
This commit is contained in:
		| @@ -1451,7 +1451,8 @@ def prefetch_related_objects(model_instances, *related_lookups): | ||||
|             # We assume that objects retrieved are homogeneous (which is the premise | ||||
|             # of prefetch_related), so what applies to first object applies to all. | ||||
|             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: | ||||
|                 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 | ||||
|  | ||||
|  | ||||
| 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(). | ||||
|     Returns a 4 tuple containing: | ||||
|     (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 | ||||
|     # from the object, as this will trigger the query. So we first try | ||||
|     # 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: | ||||
|         attr_found = hasattr(instance, attr) | ||||
|         attr_found = hasattr(instance, through_attr) | ||||
|     else: | ||||
|         attr_found = True | ||||
|         if rel_obj_descriptor: | ||||
| @@ -1536,10 +1537,13 @@ def get_prefetcher(instance, attr): | ||||
|                 # descriptor doesn't support prefetching, so we go ahead and get | ||||
|                 # the attribute on the instance rather than the class to | ||||
|                 # support many related managers | ||||
|                 rel_obj = getattr(instance, attr) | ||||
|                 rel_obj = getattr(instance, through_attr) | ||||
|                 if hasattr(rel_obj, 'get_prefetch_queryset'): | ||||
|                     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 | ||||
|  | ||||
|  | ||||
| @@ -1619,7 +1623,6 @@ def prefetch_one_level(instances, prefetcher, lookup, level): | ||||
|         else: | ||||
|             if as_attr: | ||||
|                 setattr(obj, to_attr, vals) | ||||
|                 obj._prefetched_objects_cache[cache_name] = vals | ||||
|             else: | ||||
|                 manager = getattr(obj, to_attr) | ||||
|                 if leaf and lookup.queryset is not None: | ||||
|   | ||||
| @@ -5,7 +5,7 @@ import warnings | ||||
| from django.contrib.contenttypes.models import ContentType | ||||
| from django.core.exceptions import ObjectDoesNotExist | ||||
| 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.test import TestCase, override_settings | ||||
| from django.test.utils import CaptureQueriesContext | ||||
| @@ -737,6 +737,12 @@ class CustomPrefetchTests(TestCase): | ||||
|         with self.assertRaisesMessage(ValueError, 'Prefetch querysets cannot use values().'): | ||||
|             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): | ||||
|  | ||||
| @@ -1268,7 +1274,7 @@ class Ticket21760Tests(TestCase): | ||||
|             house.save() | ||||
|  | ||||
|     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] | ||||
|         self.assertNotIn(' JOIN ', force_text(queryset.query)) | ||||
|  | ||||
|   | ||||
		Reference in New Issue
	
	Block a user