mirror of
				https://github.com/django/django.git
				synced 2025-10-31 01:25:32 +00:00 
			
		
		
		
	Fixed #26676 -- Prevented prefetching to_attr from caching its result in through attr.
Thanks Ursidours for the report.
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 |             # 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: | ||||||
|   | |||||||
| @@ -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)) | ||||||
|  |  | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user