Thanks to visit codestin.com
Credit goes to github.com

Skip to content

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.

Comment on lines 1978 to 1979
>>> e1 = Entry.objects.get(headline="Best Albums of 2008")
>>> e2 = Entry.objects.get(headline="Lennon Would Have Loved Hip Hop")
Copy link
Member

Choose a reason for hiding this comment

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

To my knowledge we depend on object persistence across shell examples, but here we're depending on objects created before in a "note" section, which feels more separate to me. Regrettably, as much as I've enjoyed wringing more value out of this example, (yoko & taylor blogging together?!), we might just create new blog entries, no?

>>> e1.authors.add(taylor)
>>> e2.authors.add(yoko, taylor)

>>> yoko.entry_set.filter(authors=taylor)
Copy link
Member

@jacobtylerwalls jacobtylerwalls Nov 12, 2025

Choose a reason for hiding this comment

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

The syntax of using a model instance directly as the right-hand-side of a filter hasn't been introduced in this doc thus far; it's clarified at the end under "Queries over related objects". Given that, perhaps we want to query like authors__name="Yoko Ono"? I'm unsure. When clarified, it does say it ~works like other model fields, so maybe this isn't something to fuss over.

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 change the query to authors__name="Yoko Ono". That will make it clearer.

a.entry_set.set([e1, e2])
a.entry_set.set([e1.pk, e2.pk])

Filters on RelatedManager
Copy link
Member

Choose a reason for hiding this comment

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

This just applies to m2m fields, yes? This filter on a reverse FK involves s RelatedManager but isn't sticky:

class Author(models.Model):
    name = models.CharField(max_length=200)
    email = models.EmailField()
    
class Entry(models.Model):
    mod_date = models.DateField(default=date.today)
    author = models.ForeignKey(Author, on_delete=models.SET_NULL, null=True)
    yoko = Author.objects.create(name="Yoko")
    taylor = Author.objects.create(name='Taylor')
    e1 = Entry.objects.create(author=yoko)
    print(type(yoko.entry_set))
    qs = yoko.entry_set.filter(author__name="Taylor")
    print(qs.query)
    print(qs)
<class 'django.db.models.fields.related_descriptors.create_reverse_many_to_one_manager.<locals>.
RelatedManager'>
SELECT "app_entry"."id", "app_entry"."mod_date", "app_entry"."author_id" FROM "app_entry"
  INNER JOIN "app_author" ON ("app_entry"."author_id" = "app_author"."id") WHERE ("app_entry"."author_id" = 1 AND "app_author"."name" = Taylor)
<QuerySet []>

In other words, qs is no different than this qs2 following your mentioned workaround:

qs2 = yoko.entry_set.filter().filter(author__name="Taylor")

The docstring helped me:

def _next_is_sticky(self):
"""
Indicate that the next filter call and the one following that should
be treated as a single filter. This is only important when it comes to
determining when to reuse tables for many-to-many filters. Required so
that we can filter naturally on the results of related managers.

Given that, I think this just needs a small wording adjustment to deemphasize the RelatedManager-ness and reemphasize the m2m-ness.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

You're right. I noticed it too, but was unsure how to reword it.

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

``Entry`` and ``Author``, the "entry" table is joined against the intermediary
table "entry_authors". On the first ``filter()`` call, this join is performed
only once. This means that it is impossible for one table row to refer to both
authors, *yoko* and *taylor*. So although e2 fulfills the query condition, you
Copy link
Member

Choose a reason for hiding this comment

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

I think backticks are better here for the variable names to make sure it doesn't accidentally get translated. Same for e2. But perhaps you won't have a variable name for yoko after all if you don't save the value of create().

Comment on lines 2001 to 2002
>>> yoko.entry_set.filter().filter(authors=taylor)
<QuerySet [<Entry: Lennon Would Have Loved Hip Hop>]>
Copy link
Member

Choose a reason for hiding this comment

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

This query is pretty weird (understandably, it's a workaround). It might help to restate what it's doing. (Above, after "fulfills the query condition", I'm not quite sure what the condition is.) Am I right that it's getting entries that are co-authored by Yoko & Taylor?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, that's right. I will make that more explicit.


Since the filter condition traverses the many-to-many relationship between
``Entry`` and ``Author``, the "entry" table is joined against the intermediary
table "entry_authors". On the first ``filter()`` call, this join is performed
Copy link
Member

Choose a reason for hiding this comment

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

This doc hasn't introduced the table naming behavior for m2m through models, perhaps we should remove mention of tables and prefer wording like "two separate joins traversing Entry to Author"?

Other mentions of "table" are pretty scant in this doc, describing instead the model instance abstraction.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, I also noticed that table names are not present on this doc page. I can reword as you suggest. This also aligns with not getting lost in database-level details. Although it was essential for my understanding of the problem that the join happens from "entry" against "entry_authors" and not against "author".

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!

- Created new blog entries in example queries
- Replaced object filter value with literal
- Emphasized many-to-many instead of RelatedManager
- added note on exclude() behaviour
- Added backticks to variable names
- Added paraphrase to query explanation
- Replaced table names with model names
- Explained "sticky" right at the beginning of the section
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