From e08fa42fa6d0e9f2a74e8fcdc5a47f5c3b825877 Mon Sep 17 00:00:00 2001 From: blingblin-g Date: Thu, 31 Jul 2025 00:03:27 +0900 Subject: [PATCH] Fixed #36426 -- Added support for further iterables in prefetch_related_objects(). Thanks Sarah Boyce for the review. --- django/db/models/query.py | 6 ++-- docs/ref/models/querysets.txt | 5 +-- .../test_prefetch_related_objects.py | 31 +++++++++++++++++++ 3 files changed, 37 insertions(+), 5 deletions(-) diff --git a/django/db/models/query.py b/django/db/models/query.py index 2359ee3bb4..0de5787f42 100644 --- a/django/db/models/query.py +++ b/django/db/models/query.py @@ -2333,8 +2333,8 @@ def normalize_prefetch_lookups(lookups, prefix=None): def prefetch_related_objects(model_instances, *related_lookups): """ - Populate prefetched object caches for a list of model instances based on - the lookups/Prefetch instances given. + Populate prefetched object caches for an iterable of model instances based + on the lookups/Prefetch instances given. """ if not model_instances: return # nothing to do @@ -2402,7 +2402,7 @@ def prefetch_related_objects(model_instances, *related_lookups): # We assume that objects retrieved are homogeneous (which is the # premise of prefetch_related), so what applies to first object # applies to all. - first_obj = obj_list[0] + first_obj = next(iter(obj_list)) to_attr = lookup.get_current_to_attr(level)[0] prefetcher, descriptor, attr_found, is_fetched = get_prefetcher( first_obj, through_attr, to_attr diff --git a/docs/ref/models/querysets.txt b/docs/ref/models/querysets.txt index baeb3e8746..59550e6690 100644 --- a/docs/ref/models/querysets.txt +++ b/docs/ref/models/querysets.txt @@ -4223,8 +4223,9 @@ Prefetches the given lookups on an iterable of model instances. This is useful in code that receives a list of model instances as opposed to a ``QuerySet``; for example, when fetching models from a cache or instantiating them manually. -Pass an iterable of model instances (must all be of the same class) and the -lookups or :class:`Prefetch` objects you want to prefetch for. For example: +Pass an iterable of model instances (must all be of the same class and able to +be iterated multiple times) and the lookups or :class:`Prefetch` objects you +want to prefetch for. For example: .. code-block:: pycon diff --git a/tests/prefetch_related/test_prefetch_related_objects.py b/tests/prefetch_related/test_prefetch_related_objects.py index eea9a7fff7..20f620417a 100644 --- a/tests/prefetch_related/test_prefetch_related_objects.py +++ b/tests/prefetch_related/test_prefetch_related_objects.py @@ -1,3 +1,5 @@ +from collections import deque + from django.db.models import Prefetch, prefetch_related_objects from django.test import TestCase @@ -221,3 +223,32 @@ class PrefetchRelatedObjectsTests(TestCase): with self.assertNumQueries(0): self.assertCountEqual(book1.authors.all(), [self.author1, self.author2]) + + def test_prefetch_related_objects_with_various_iterables(self): + book = self.book1 + + class MyIterable: + def __iter__(self): + yield book + + cases = { + "set": {book}, + "tuple": (book,), + "dict_values": {"a": book}.values(), + "frozenset": frozenset([book]), + "deque": deque([book]), + "custom iterator": MyIterable(), + } + for case_type, case in cases.items(): + with self.subTest(case=case_type): + # Clear the prefetch cache. + book._prefetched_objects_cache = {} + with self.assertNumQueries(1): + prefetch_related_objects(case, "authors") + with self.assertNumQueries(0): + self.assertCountEqual( + book.authors.all(), [self.author1, self.author2, self.author3] + ) + + def test_prefetch_related_objects_empty(self): + prefetch_related_objects([], "authors")