From 804607df0e174c524a3ea880b8ecbb555ecb4abb Mon Sep 17 00:00:00 2001 From: Keryn Knight Date: Sat, 10 Jan 2026 08:39:45 +0100 Subject: Refs #28455 -- Avoided QuerySet cloning in simple prefetch_related() usages. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit manager.get_queryset() always returns freshly instantiated per-instance QuerySet which doesn't need subsequent cloning. Based on work originally done by Anssi Kääriäinen and Tim Graham. --- django/contrib/contenttypes/fields.py | 11 ++++++----- django/db/models/fields/related_descriptors.py | 26 ++++++++++++++------------ django/db/models/query.py | 2 +- 3 files changed, 21 insertions(+), 18 deletions(-) diff --git a/django/contrib/contenttypes/fields.py b/django/contrib/contenttypes/fields.py index 300fec4289..0d7ef0f256 100644 --- a/django/contrib/contenttypes/fields.py +++ b/django/contrib/contenttypes/fields.py @@ -651,11 +651,12 @@ def create_generic_related_manager(superclass, rel): Filter the queryset for the instance this manager is bound to. """ db = self._db or router.db_for_read(self.model, instance=self.instance) - return ( - queryset.using(db) - .fetch_mode(self.instance._state.fetch_mode) - .filter(**self.core_filters) - ) + with queryset._avoid_cloning(): + return ( + queryset.using(db) + .fetch_mode(self.instance._state.fetch_mode) + .filter(**self.core_filters) + ) def _remove_prefetched_objects(self): try: diff --git a/django/db/models/fields/related_descriptors.py b/django/db/models/fields/related_descriptors.py index 40ad8b260f..87b4921a6c 100644 --- a/django/db/models/fields/related_descriptors.py +++ b/django/db/models/fields/related_descriptors.py @@ -738,12 +738,13 @@ def create_reverse_many_to_one_manager(superclass, rel): empty_strings_as_null = connections[ db ].features.interprets_empty_strings_as_nulls - queryset._add_hints(instance=self.instance) - if self._db: - queryset = queryset.using(self._db) - queryset._fetch_mode = self.instance._state.fetch_mode - queryset._defer_next_filter = True - queryset = queryset.filter(**self.core_filters) + with queryset._avoid_cloning(): + queryset._add_hints(instance=self.instance) + if self._db: + queryset = queryset.using(self._db) + queryset._fetch_mode = self.instance._state.fetch_mode + queryset._defer_next_filter = True + queryset = queryset.filter(**self.core_filters) for field in self.field.foreign_related_fields: val = getattr(self.instance, field.attname) if val is None or (val == "" and empty_strings_as_null): @@ -1140,12 +1141,13 @@ def create_forward_many_to_many_manager(superclass, rel, reverse): """ Filter the queryset for the instance this manager is bound to. """ - queryset._add_hints(instance=self.instance) - if self._db: - queryset = queryset.using(self._db) - queryset._fetch_mode = self.instance._state.fetch_mode - queryset._defer_next_filter = True - return queryset._next_is_sticky().filter(**self.core_filters) + with queryset._avoid_cloning(): + queryset._add_hints(instance=self.instance) + if self._db: + queryset = queryset.using(self._db) + queryset._fetch_mode = self.instance._state.fetch_mode + queryset._defer_next_filter = True + return queryset._next_is_sticky().filter(**self.core_filters) def get_prefetch_cache(self): # Walk up the ancestor-chain (if cached) to try and find a prefetch diff --git a/django/db/models/query.py b/django/db/models/query.py index d4775308b8..dca504e441 100644 --- a/django/db/models/query.py +++ b/django/db/models/query.py @@ -2906,7 +2906,7 @@ def prefetch_one_level(instances, prefetcher, lookup, level): else: manager = getattr(obj, to_attr) if leaf and lookup.queryset is not None: - qs = manager._apply_rel_filters(lookup.queryset) + qs = manager._apply_rel_filters(lookup.queryset._chain()) else: qs = manager.get_queryset() qs._result_cache = vals -- cgit v1.3