From 174a4e14f0c255b0a3af09015478df553adee2cd Mon Sep 17 00:00:00 2001 From: Bernhard Bliem Date: Wed, 29 Oct 2025 14:23:49 +0100 Subject: [PATCH] Fix prefetching in DeferringManyRelatedManager._apply_rel_filters() Suppose we have a `ParentalManyToManyField` and we use `prefetch_related` with a lookup queryset. Django's `prefetch_one_level()` in `django.db.models.query` iterates over a list of instances. For each of them, it calls `DeferringManyRelatedManager._apply_rel_filters(lookup.queryset)` and thus obtains a queryset, for which `prefetch_one_level()` then sets the attribute `_result_cache`. This is currently buggy in modelcluster as `DeferringManyRelatedManager._apply_rel_filters()` directly returns the given queryset instead of a copy. When `_apply_rel_filters(lookup.queryset)` is called twice, the second call returns the same object as the first. This is a problem because, in the second loop iteration of `prefetch_one_level()`, the cache for the first object will be overwritten by the cache for the second object. In fact, all objects will end up with the same cache -- the one of the last object. The current commit fixes this by making `DeferringManyRelatedManager._apply_rel_filters()` return a copy of the given queryset. It also adds a unit test. Note that basically the same problem was already fixed in https://github.com/wagtail/django-modelcluster/pull/130 for `RelatedManager`, but apparently applying the fix to `DeferringManyRelatedManager` has been forgotten. --- modelcluster/fields.py | 4 +++- tests/tests/test_cluster.py | 16 ++++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/modelcluster/fields.py b/modelcluster/fields.py index cc78136..e7ca355 100644 --- a/modelcluster/fields.py +++ b/modelcluster/fields.py @@ -397,7 +397,9 @@ def get_prefetch_queryset(self, instances, queryset=None): def _apply_rel_filters(self, queryset): # Required for get_prefetch_queryset. - return queryset._next_is_sticky() + # NOTE: _apply_rel_filters() must return a copy of the queryset + # to work correctly with prefetch + return queryset._next_is_sticky().all() def get_object_list(self): """ diff --git a/tests/tests/test_cluster.py b/tests/tests/test_cluster.py index adc6dc1..683eae2 100644 --- a/tests/tests/test_cluster.py +++ b/tests/tests/test_cluster.py @@ -1311,3 +1311,19 @@ def test_prefetch_related_with_lookup(self): res = list(query) self.assertEqual(query[0].menu_items.all()[0], menu_item1) self.assertEqual(query[1].menu_items.all()[0], menu_item2) + + def test_m2m_prefetch_related_with_lookup(self): + person1 = Person.objects.create(name='Joe') + person2 = Person.objects.create(name='Mary') + room = Room.objects.create(name='Dining room') + house = House.objects.create(name='House 1', address='123 Main St', owner=person1, main_room=room) + person1.houses = [house] + person1.save() + + query = Person.objects.all().prefetch_related( + Prefetch('houses', queryset=House.objects.all()) + ) + + res = list(query) + self.assertEqual(query[0], person1) + self.assertEqual(query[0].houses.count(), 1)