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>
This commit is contained in:
parent
bebd4cfa8f
commit
b9ba85a7ce
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -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):
|
||||
|
|
Loading…
Reference in New Issue