Skip to content
/ django Public

Comments

Fixed #26379 - Documented that first filter() chained to a RelatedManager is sticky.#20085

Merged
jacobtylerwalls merged 1 commit intodjango:mainfrom
annalauraw:ticket_26379
Nov 17, 2025
Merged

Fixed #26379 - Documented that first filter() chained to a RelatedManager is sticky.#20085
jacobtylerwalls merged 1 commit intodjango:mainfrom
annalauraw:ticket_26379

Conversation

@annalauraw
Copy link
Contributor

@annalauraw annalauraw commented Nov 12, 2025

Trac ticket number

ticket-26379

Branch description

Document that first filter() chained to a RelatedManager is sticky. Add heading "Filters on RelatedManager" to docs/topics/db/queries.txt. Document the sticky filter behaviour with a query example based on the models Author and Entry at the top of the docs page.

Checklist

  • This PR targets the main branch.
  • The commit message is written in past tense, mentions the ticket number, and ends with a period.
  • I have checked the "Has patch" ticket flag in the Trac system.
  • I have added or updated relevant tests.
  • I have added or updated relevant docs, including release notes if applicable.
  • I have attached screenshots in both light and dark modes for any UI changes.

Copy link
Member

@jacobtylerwalls jacobtylerwalls left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great start @annalauraw. I agreed with your hesitance to get too digressive here. Just a few questions.

Filters on RelatedManager
~~~~~~~~~~~~~~~~~~~~~~~~~

When calling ``filter()`` on a
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Curious, does exclude() behave the same? We have one weirdness doc'd here:

The behavior of :meth:`~django.db.models.query.QuerySet.filter` for queries
that span multi-value relationships, as described above, is not implemented
equivalently for :meth:`~django.db.models.query.QuerySet.exclude`. Instead,
the conditions in a single :meth:`~django.db.models.query.QuerySet.exclude`
call will not necessarily refer to the same item.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is probably coherent to talk about exclude() in this context as well. I tried exclude() with our so-far example: https://dryorm.xterm.info/ebqiqyqf. I suppose this behaviour can be called "different from filter()", since e2 is returned despite having taylor as a co-author. i need some time to understand the underlying SQL WHERE clause...

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, okay. I feel like that's the same class of behavior as what I linked to as "one weirdness" above. Simon discusses this in his "What the JOIN?" talk at 39:41, "when negating against a multi-valued relationship..." and calls it a "subquery pushdown".

I don't feel like we have to document the weirdness in detail twice, but I'm open to a sentence that clarifies or maybe links to the other section with details 🤔

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After looking into the exclude() behaviour a bit deeper, I would say that in this case, it behaves similarly to filter(), although implemented differently:

  • single exclude(): co-authored entry is not excluded despite matching the exclude criteria
  • double exclude(): co-authored entry is excluded as expected

https://dryorm.xterm.info/t6p0rdrc

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see. Just like the filter() example, the sticky scenario is excluding on something that logically can't exist (a single row referring to both authors). I like how you got this expressed without drowning in details 👍

Comment on lines 1973 to 1974
:class:`~django.db.models.fields.related.RelatedManager`, be aware that the
first method call is "sticky". Consider the following example:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There could be an alternative here that leaves the reader in a bit less suspense, not needing to read to the end to finally find out what "sticky" is. Having sat with this, can you think of anything? Maybe:

be aware that the first filter call reuses the blah blah blah—in other words, it's "sticky".

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will try. I really struggled with a concise explanation and felt the need to move to the example as quickly as possible.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's fair!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New version reads well here.

Copy link
Member

@jacobtylerwalls jacobtylerwalls left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the PR, @annalauraw, this looks great!

between one author and one entry - can fulfill the query condition (entries
that are co-authored by ``anna`` and ``gloria``). You can circumvent this
behavior by chaining two consecutive ``filter()`` calls, resulting in two
separate joins and thus a more permissive filter.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This reads perfectly, thank you ⭐

>>> anna.entry_set.filter().filter(authors__name="Gloria")
<QuerySet [<Entry: Supporting social movements with drums>]>

.. note::
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We avoid bare notes in favor of an admonition:: my heading so that we can use a heading (I'll add one for you).

Filters on RelatedManager
~~~~~~~~~~~~~~~~~~~~~~~~~

When calling ``filter()`` on a
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see. Just like the filter() example, the sticky scenario is excluding on something that logically can't exist (a single row referring to both authors). I like how you got this expressed without drowning in details 👍

Comment on lines 1973 to 1974
:class:`~django.db.models.fields.related.RelatedManager`, be aware that the
first method call is "sticky". Consider the following example:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New version reads well here.

@jacobtylerwalls jacobtylerwalls merged commit 3c005b5 into django:main Nov 17, 2025
30 checks passed
@annalauraw
Copy link
Contributor Author

Thank you so much @jacobtylerwalls, that was a great review experience!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants