0
0
mirror of https://github.com/django/django.git synced 2024-11-24 11:48:10 +01:00

Fixed #34791 -- Fixed incorrect Prefetch()'s cache for singly related objects.

Changed the cache name used for singly related objects to be the
to_attr parameter passed to a Prefetch object. This fixes issues with
checking if values have already been fetched in cases where the Field
already has some prefetched value, but not for the same model attr.
This commit is contained in:
Maxime Toussaint 2023-08-31 08:11:03 -04:00 committed by Mariusz Felisiak
parent f333e3513e
commit 254df3a3bb
3 changed files with 48 additions and 16 deletions

View File

@ -682,6 +682,7 @@ answer newbie questions, and generally made Django that much better:
Max Derkachev <mderk@yandex.ru>
Max Smolens <msmolens@gmail.com>
Maxime Lorant <maxime.lorant@gmail.com>
Maxime Toussaint <m.toussaint@mail.com>
Maxime Turcotte <maxocub@riseup.net>
Maximilian Merz <django@mxmerz.de>
Maximillian Dornseif <md@hudora.de>

View File

@ -2434,11 +2434,23 @@ def get_prefetcher(instance, through_attr, to_attr):
the attribute has already been fetched for that instance)
"""
def has_to_attr_attribute(instance):
return hasattr(instance, to_attr)
def is_to_attr_fetched(model, to_attr):
# Special case cached_property instances because hasattr() triggers
# attribute computation and assignment.
if isinstance(getattr(model, to_attr, None), cached_property):
def has_cached_property(instance):
return to_attr in instance.__dict__
return has_cached_property
def has_to_attr_attribute(instance):
return hasattr(instance, to_attr)
return has_to_attr_attribute
prefetcher = None
is_fetched = has_to_attr_attribute
is_fetched = is_to_attr_fetched(instance.__class__, to_attr)
# For singly related objects, we have to avoid getting the attribute
# from the object, as this will trigger the query. So we first try
@ -2453,7 +2465,12 @@ def get_prefetcher(instance, through_attr, to_attr):
# get_prefetch_queryset() method.
if hasattr(rel_obj_descriptor, "get_prefetch_queryset"):
prefetcher = rel_obj_descriptor
is_fetched = rel_obj_descriptor.is_cached
# If to_attr is set, check if the value has already been set,
# which is done with has_to_attr_attribute(). Do not use the
# method from the descriptor, as the cache_name it defines
# checks the field name, not the to_attr value.
if through_attr == to_attr:
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
@ -2461,18 +2478,7 @@ def get_prefetcher(instance, through_attr, to_attr):
rel_obj = getattr(instance, through_attr)
if hasattr(rel_obj, "get_prefetch_queryset"):
prefetcher = rel_obj
if 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
):
def has_cached_property(instance):
return to_attr in instance.__dict__
is_fetched = has_cached_property
else:
if through_attr == to_attr:
def in_prefetched_cache(instance):
return through_attr in instance._prefetched_objects_cache

View File

@ -978,6 +978,31 @@ class CustomPrefetchTests(TestCase):
with self.assertNumQueries(5):
self.traverse_qs(list(houses), [["occupants", "houses", "main_room"]])
def test_nested_prefetch_related_with_duplicate_prefetch_and_depth(self):
people = Person.objects.prefetch_related(
Prefetch(
"houses__main_room",
queryset=Room.objects.filter(name="Dining room"),
to_attr="dining_room",
),
"houses__main_room",
)
with self.assertNumQueries(4):
main_room = people[0].houses.all()[0]
people = Person.objects.prefetch_related(
"houses__main_room",
Prefetch(
"houses__main_room",
queryset=Room.objects.filter(name="Dining room"),
to_attr="dining_room",
),
)
with self.assertNumQueries(4):
main_room = people[0].houses.all()[0]
self.assertEqual(main_room.main_room, self.room1_1)
def test_values_queryset(self):
msg = "Prefetch querysets cannot use raw(), values(), and values_list()."
with self.assertRaisesMessage(ValueError, msg):