Ver código fonte

Fixed #32089 -- Fixed prefetch_related_objects() when some objects are already fetched.

Thanks Dennis Kliban for the report and Adam Johnson for the initial
patch.

Co-authored-by: Adam Johnson <me@adamj.eu>
Alexey Nigin 4 anos atrás
pai
commit
b9ba85a7ce

+ 26 - 10
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
 
 

+ 26 - 0
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):