diff --git a/django/db/models/query.py b/django/db/models/query.py index 7956b1695e..9dc98c02d1 100644 --- a/django/db/models/query.py +++ b/django/db/models/query.py @@ -1720,8 +1720,17 @@ def prefetch_related_objects(model_instances, *related_lookups): "prefetching - this is an invalid parameter to " "prefetch_related()." % lookup.prefetch_through) - if prefetcher is not None and not is_fetched: - obj_list, additional_lookups = prefetch_one_level(obj_list, prefetcher, lookup, level) + obj_to_fetch = None + if prefetcher is not None: + obj_to_fetch = [obj for obj in obj_list if not is_fetched(obj)] + + if obj_to_fetch: + obj_list, additional_lookups = prefetch_one_level( + obj_to_fetch, + prefetcher, + lookup, + level, + ) # We need to ensure we don't keep adding lookups from the # same relationships to stop infinite recursion. So, if we # are already on an automatically added lookup, don't add @@ -1771,10 +1780,14 @@ def get_prefetcher(instance, through_attr, to_attr): (the object with get_prefetch_queryset (or None), the descriptor object representing this relationship (or None), a boolean that is False if the attribute was not found at all, - a boolean that is True if the attribute has already been fetched) + a function that takes an instance and returns a boolean that is True if + the attribute has already been fetched for that instance) """ + def has_to_attr_attribute(instance): + return hasattr(instance, to_attr) + prefetcher = None - is_fetched = False + is_fetched = has_to_attr_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 @@ -1789,8 +1802,7 @@ def get_prefetcher(instance, through_attr, to_attr): # get_prefetch_queryset() method. if hasattr(rel_obj_descriptor, 'get_prefetch_queryset'): prefetcher = rel_obj_descriptor - if rel_obj_descriptor.is_cached(instance): - is_fetched = True + is_fetched = rel_obj_descriptor.is_cached else: # descriptor doesn't support prefetching, so we go ahead and get # the attribute on the instance rather than the class to @@ -1802,11 +1814,15 @@ def get_prefetcher(instance, through_attr, to_attr): # Special case cached_property instances because hasattr # triggers attribute computation and assignment. if isinstance(getattr(instance.__class__, to_attr, None), cached_property): - is_fetched = to_attr in instance.__dict__ - else: - is_fetched = hasattr(instance, to_attr) + def has_cached_property(instance): + return to_attr in instance.__dict__ + + is_fetched = has_cached_property else: - is_fetched = through_attr in instance._prefetched_objects_cache + def in_prefetched_cache(instance): + return through_attr in instance._prefetched_objects_cache + + is_fetched = in_prefetched_cache return prefetcher, rel_obj_descriptor, attr_found, is_fetched diff --git a/tests/prefetch_related/test_prefetch_related_objects.py b/tests/prefetch_related/test_prefetch_related_objects.py index c0aa433b91..20d4da5a0a 100644 --- a/tests/prefetch_related/test_prefetch_related_objects.py +++ b/tests/prefetch_related/test_prefetch_related_objects.py @@ -97,6 +97,16 @@ class PrefetchRelatedObjectsTests(TestCase): with self.assertNumQueries(0): self.assertCountEqual(book1.authors.all(), [self.author1, self.author2, self.author3]) + def test_prefetch_object_twice(self): + book1 = Book.objects.get(id=self.book1.id) + book2 = Book.objects.get(id=self.book2.id) + with self.assertNumQueries(1): + prefetch_related_objects([book1], Prefetch('authors')) + with self.assertNumQueries(1): + prefetch_related_objects([book1, book2], Prefetch('authors')) + with self.assertNumQueries(0): + self.assertCountEqual(book2.authors.all(), [self.author1]) + def test_prefetch_object_to_attr(self): book1 = Book.objects.get(id=self.book1.id) with self.assertNumQueries(1): @@ -105,6 +115,22 @@ class PrefetchRelatedObjectsTests(TestCase): with self.assertNumQueries(0): self.assertCountEqual(book1.the_authors, [self.author1, self.author2, self.author3]) + def test_prefetch_object_to_attr_twice(self): + book1 = Book.objects.get(id=self.book1.id) + book2 = Book.objects.get(id=self.book2.id) + with self.assertNumQueries(1): + prefetch_related_objects( + [book1], + Prefetch('authors', to_attr='the_authors'), + ) + with self.assertNumQueries(1): + prefetch_related_objects( + [book1, book2], + Prefetch('authors', to_attr='the_authors'), + ) + with self.assertNumQueries(0): + self.assertCountEqual(book2.the_authors, [self.author1]) + def test_prefetch_queryset(self): book1 = Book.objects.get(id=self.book1.id) with self.assertNumQueries(1):