Skip to content

Fix prefetching in DeferringManyRelatedManager._apply_rel_filters()#203

Open
bbliem wants to merge 1 commit intowagtail:mainfrom
bbliem:fix/m2m-prefetch-related-with-lookup
Open

Fix prefetching in DeferringManyRelatedManager._apply_rel_filters()#203
bbliem wants to merge 1 commit intowagtail:mainfrom
bbliem:fix/m2m-prefetch-related-with-lookup

Conversation

@bbliem
Copy link
Copy Markdown

@bbliem bbliem commented Oct 29, 2025

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 #130 for RelatedManager, but apparently applying the fix to DeferringManyRelatedManager has been forgotten.

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 wagtail#130 for `RelatedManager`, but apparently applying the fix to `DeferringManyRelatedManager` has been forgotten.
@bbliem bbliem force-pushed the fix/m2m-prefetch-related-with-lookup branch from a338d5c to 174a4e1 Compare October 29, 2025 13:48
bbliem added a commit to kausaltech/kausal-watch that referenced this pull request Oct 29, 2025
This should be removed once this pull request is merged to modelcluster:
wagtail/django-modelcluster#203
juyrjola pushed a commit to kausaltech/kausal-watch-old that referenced this pull request Nov 14, 2025
This should be removed once this pull request is merged to modelcluster:
wagtail/django-modelcluster#203
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant