Loading django/db/models/query.py +16 −20 Original line number Diff line number Diff line Loading @@ -2,8 +2,8 @@ The main QuerySet implementation. This provides the public API for the ORM. """ from collections import deque import copy import itertools import sys from django.conf import settings Loading Loading @@ -1680,6 +1680,9 @@ class Prefetch(object): return self.prefetch_to == other.prefetch_to return False def __hash__(self): return hash(self.__class__) ^ hash(self.prefetch_to) def normalize_prefetch_lookups(lookups, prefix=None): """ Loading Loading @@ -1713,11 +1716,12 @@ def prefetch_related_objects(result_cache, related_lookups): # ensure we don't do duplicate work. done_queries = {} # dictionary of things like 'foo__bar': [results] auto_lookups = [] # we add to this as we go through. auto_lookups = set() # we add to this as we go through. followed_descriptors = set() # recursion protection all_lookups = itertools.chain(related_lookups, auto_lookups) for lookup in all_lookups: all_lookups = deque(related_lookups) while all_lookups: lookup = all_lookups.popleft() if lookup.prefetch_to in done_queries: if lookup.queryset: raise ValueError("'%s' lookup was already seen with a different queryset. " Loading Loading @@ -1788,7 +1792,9 @@ def prefetch_related_objects(result_cache, related_lookups): # the new lookups from relationships we've seen already. if not (lookup in auto_lookups and descriptor in followed_descriptors): done_queries[prefetch_to] = obj_list auto_lookups.extend(normalize_prefetch_lookups(additional_lookups, prefetch_to)) new_lookups = normalize_prefetch_lookups(additional_lookups, prefetch_to) auto_lookups.update(new_lookups) all_lookups.extendleft(new_lookups) followed_descriptors.add(descriptor) else: # Either a singly related object that has already been fetched Loading Loading @@ -1827,7 +1833,6 @@ def get_prefetcher(instance, attr): a boolean that is True if the attribute has already been fetched) """ prefetcher = None attr_found = False is_fetched = False # For singly related objects, we have to avoid getting the attribute Loading @@ -1835,16 +1840,7 @@ def get_prefetcher(instance, attr): # on the class, in order to get the descriptor object. rel_obj_descriptor = getattr(instance.__class__, attr, None) if rel_obj_descriptor is None: try: rel_obj = getattr(instance, attr) attr_found = True # If we are following a lookup path which leads us through a previous # fetch from a custom Prefetch then we might end up into a list # instead of related qs. This means the objects are already fetched. if isinstance(rel_obj, list): is_fetched = True except AttributeError: pass attr_found = hasattr(instance, attr) else: attr_found = True if rel_obj_descriptor: Loading Loading @@ -1889,10 +1885,10 @@ def prefetch_one_level(instances, prefetcher, lookup, level): rel_qs, rel_obj_attr, instance_attr, single, cache_name = ( prefetcher.get_prefetch_queryset(instances, lookup.get_current_queryset(level))) # We have to handle the possibility that the default manager itself added # prefetch_related lookups to the QuerySet we just got back. We don't want to # trigger the prefetch_related functionality by evaluating the query. # Rather, we need to merge in the prefetch_related lookups. # We have to handle the possibility that the QuerySet we just got back # contains some prefetch_related lookups. We don't want to trigger the # prefetch_related functionality by evaluating the query. Rather, we need # to merge in the prefetch_related lookups. additional_lookups = getattr(rel_qs, '_prefetch_related_lookups', []) if additional_lookups: # Don't need to clone because the manager should have given us a fresh Loading tests/prefetch_related/tests.py +6 −3 Original line number Diff line number Diff line Loading @@ -195,7 +195,7 @@ class PrefetchRelatedTests(TestCase): def test_reverse_one_to_one_then_m2m(self): """ Test that we can follow a m2m relation after going through the select_related reverse of a o2o. the select_related reverse of an o2o. """ qs = Author.objects.prefetch_related('bio__books').select_related('bio') Loading Loading @@ -559,13 +559,16 @@ class CustomPrefetchTests(TestCase): inner_rooms_qs = Room.objects.filter(pk__in=[self.room1_1.pk, self.room1_2.pk]) houses_qs_prf = House.objects.prefetch_related( Prefetch('rooms', queryset=inner_rooms_qs, to_attr='rooms_lst')) with self.assertNumQueries(3): with self.assertNumQueries(4): lst2 = list(Person.objects.prefetch_related( Prefetch('houses', queryset=houses_qs_prf.filter(pk=self.house1.pk), to_attr='houses_lst'))) Prefetch('houses', queryset=houses_qs_prf.filter(pk=self.house1.pk), to_attr='houses_lst'), Prefetch('houses_lst__rooms_lst__main_room_of') )) self.assertEqual(len(lst2[0].houses_lst[0].rooms_lst), 2) self.assertEqual(lst2[0].houses_lst[0].rooms_lst[0], self.room1_1) self.assertEqual(lst2[0].houses_lst[0].rooms_lst[1], self.room1_2) self.assertEqual(lst2[0].houses_lst[0].rooms_lst[0].main_room_of, self.house1) self.assertEqual(len(lst2[1].houses_lst), 0) # Test ReverseSingleRelatedObjectDescriptor. Loading Loading
django/db/models/query.py +16 −20 Original line number Diff line number Diff line Loading @@ -2,8 +2,8 @@ The main QuerySet implementation. This provides the public API for the ORM. """ from collections import deque import copy import itertools import sys from django.conf import settings Loading Loading @@ -1680,6 +1680,9 @@ class Prefetch(object): return self.prefetch_to == other.prefetch_to return False def __hash__(self): return hash(self.__class__) ^ hash(self.prefetch_to) def normalize_prefetch_lookups(lookups, prefix=None): """ Loading Loading @@ -1713,11 +1716,12 @@ def prefetch_related_objects(result_cache, related_lookups): # ensure we don't do duplicate work. done_queries = {} # dictionary of things like 'foo__bar': [results] auto_lookups = [] # we add to this as we go through. auto_lookups = set() # we add to this as we go through. followed_descriptors = set() # recursion protection all_lookups = itertools.chain(related_lookups, auto_lookups) for lookup in all_lookups: all_lookups = deque(related_lookups) while all_lookups: lookup = all_lookups.popleft() if lookup.prefetch_to in done_queries: if lookup.queryset: raise ValueError("'%s' lookup was already seen with a different queryset. " Loading Loading @@ -1788,7 +1792,9 @@ def prefetch_related_objects(result_cache, related_lookups): # the new lookups from relationships we've seen already. if not (lookup in auto_lookups and descriptor in followed_descriptors): done_queries[prefetch_to] = obj_list auto_lookups.extend(normalize_prefetch_lookups(additional_lookups, prefetch_to)) new_lookups = normalize_prefetch_lookups(additional_lookups, prefetch_to) auto_lookups.update(new_lookups) all_lookups.extendleft(new_lookups) followed_descriptors.add(descriptor) else: # Either a singly related object that has already been fetched Loading Loading @@ -1827,7 +1833,6 @@ def get_prefetcher(instance, attr): a boolean that is True if the attribute has already been fetched) """ prefetcher = None attr_found = False is_fetched = False # For singly related objects, we have to avoid getting the attribute Loading @@ -1835,16 +1840,7 @@ def get_prefetcher(instance, attr): # on the class, in order to get the descriptor object. rel_obj_descriptor = getattr(instance.__class__, attr, None) if rel_obj_descriptor is None: try: rel_obj = getattr(instance, attr) attr_found = True # If we are following a lookup path which leads us through a previous # fetch from a custom Prefetch then we might end up into a list # instead of related qs. This means the objects are already fetched. if isinstance(rel_obj, list): is_fetched = True except AttributeError: pass attr_found = hasattr(instance, attr) else: attr_found = True if rel_obj_descriptor: Loading Loading @@ -1889,10 +1885,10 @@ def prefetch_one_level(instances, prefetcher, lookup, level): rel_qs, rel_obj_attr, instance_attr, single, cache_name = ( prefetcher.get_prefetch_queryset(instances, lookup.get_current_queryset(level))) # We have to handle the possibility that the default manager itself added # prefetch_related lookups to the QuerySet we just got back. We don't want to # trigger the prefetch_related functionality by evaluating the query. # Rather, we need to merge in the prefetch_related lookups. # We have to handle the possibility that the QuerySet we just got back # contains some prefetch_related lookups. We don't want to trigger the # prefetch_related functionality by evaluating the query. Rather, we need # to merge in the prefetch_related lookups. additional_lookups = getattr(rel_qs, '_prefetch_related_lookups', []) if additional_lookups: # Don't need to clone because the manager should have given us a fresh Loading
tests/prefetch_related/tests.py +6 −3 Original line number Diff line number Diff line Loading @@ -195,7 +195,7 @@ class PrefetchRelatedTests(TestCase): def test_reverse_one_to_one_then_m2m(self): """ Test that we can follow a m2m relation after going through the select_related reverse of a o2o. the select_related reverse of an o2o. """ qs = Author.objects.prefetch_related('bio__books').select_related('bio') Loading Loading @@ -559,13 +559,16 @@ class CustomPrefetchTests(TestCase): inner_rooms_qs = Room.objects.filter(pk__in=[self.room1_1.pk, self.room1_2.pk]) houses_qs_prf = House.objects.prefetch_related( Prefetch('rooms', queryset=inner_rooms_qs, to_attr='rooms_lst')) with self.assertNumQueries(3): with self.assertNumQueries(4): lst2 = list(Person.objects.prefetch_related( Prefetch('houses', queryset=houses_qs_prf.filter(pk=self.house1.pk), to_attr='houses_lst'))) Prefetch('houses', queryset=houses_qs_prf.filter(pk=self.house1.pk), to_attr='houses_lst'), Prefetch('houses_lst__rooms_lst__main_room_of') )) self.assertEqual(len(lst2[0].houses_lst[0].rooms_lst), 2) self.assertEqual(lst2[0].houses_lst[0].rooms_lst[0], self.room1_1) self.assertEqual(lst2[0].houses_lst[0].rooms_lst[1], self.room1_2) self.assertEqual(lst2[0].houses_lst[0].rooms_lst[0].main_room_of, self.house1) self.assertEqual(len(lst2[1].houses_lst), 0) # Test ReverseSingleRelatedObjectDescriptor. Loading