-
-
Notifications
You must be signed in to change notification settings - Fork 33.2k
Fixed #26379 - Documented that first filter() chained to a RelatedManager is sticky. #20085
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
4104dfd to
9519364
Compare
jacobtylerwalls
left a comment
There was a problem hiding this 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.
docs/topics/db/queries.txt
Outdated
| >>> e1 = Entry.objects.get(headline="Best Albums of 2008") | ||
| >>> e2 = Entry.objects.get(headline="Lennon Would Have Loved Hip Hop") |
There was a problem hiding this comment.
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?
docs/topics/db/queries.txt
Outdated
| >>> e1.authors.add(taylor) | ||
| >>> e2.authors.add(yoko, taylor) | ||
|
|
||
| >>> yoko.entry_set.filter(authors=taylor) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
docs/topics/db/queries.txt
Outdated
| a.entry_set.set([e1, e2]) | ||
| a.entry_set.set([e1.pk, e2.pk]) | ||
|
|
||
| Filters on RelatedManager |
There was a problem hiding this comment.
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:
django/django/db/models/query.py
Lines 2096 to 2101 in a1ce852
| 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.
There was a problem hiding this comment.
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.
docs/topics/db/queries.txt
Outdated
| Filters on RelatedManager | ||
| ~~~~~~~~~~~~~~~~~~~~~~~~~ | ||
|
|
||
| When calling ``filter()`` on a |
There was a problem hiding this comment.
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:
django/docs/topics/db/queries.txt
Lines 660 to 664 in a1ce852
| 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. |
There was a problem hiding this comment.
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...
There was a problem hiding this comment.
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 🤔
There was a problem hiding this comment.
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
docs/topics/db/queries.txt
Outdated
| ``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 |
There was a problem hiding this comment.
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().
docs/topics/db/queries.txt
Outdated
| >>> yoko.entry_set.filter().filter(authors=taylor) | ||
| <QuerySet [<Entry: Lennon Would Have Loved Hip Hop>]> |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
docs/topics/db/queries.txt
Outdated
|
|
||
| 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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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".
docs/topics/db/queries.txt
Outdated
| :class:`~django.db.models.fields.related.RelatedManager`, be aware that the | ||
| first method call is "sticky". Consider the following example: |
There was a problem hiding this comment.
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".
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
0758729 to
5f9b419
Compare
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
mainbranch.