[4.0.x] Fixed #27936 -- Rewrote spanning multi-valued relationships docs.

Backport of 6174814dbe from main
This commit is contained in:
Jacob Walls 2021-12-24 09:12:39 -05:00 committed by Mariusz Felisiak
parent e9b023b8e4
commit c46e996307
1 changed files with 61 additions and 39 deletions

View File

@ -530,55 +530,77 @@ those latter objects, you could write::
Spanning multi-valued relationships
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
When you are filtering an object based on a
:class:`~django.db.models.ManyToManyField` or a reverse
:class:`~django.db.models.ForeignKey`, there are two different sorts of filter
you may be interested in. Consider the ``Blog``/``Entry`` relationship
(``Blog`` to ``Entry`` is a one-to-many relation). We might be interested in
finding blogs that have an entry which has both *"Lennon"* in the headline and
was published in 2008. Or we might want to find blogs that have an entry with
*"Lennon"* in the headline as well as an entry that was published
in 2008. Since there are multiple entries associated with a single ``Blog``,
both of these queries are possible and make sense in some situations.
When spanning a :class:`~django.db.models.ManyToManyField` or a reverse
:class:`~django.db.models.ForeignKey` (such as from ``Blog`` to ``Entry``),
filtering on multiple attributes raises the question of whether to require each
attribute to coincide in the same related object. We might seek blogs that have
an entry from 2008 with *“Lennon”* in its headline, or we might seek blogs that
merely have any entry from 2008 as well as some newer or older entry with
*“Lennon”* in its headline.
The same type of situation arises with a
:class:`~django.db.models.ManyToManyField`. For example, if an ``Entry`` has a
:class:`~django.db.models.ManyToManyField` called ``tags``, we might want to
find entries linked to tags called *"music"* and *"bands"* or we might want an
entry that contains a tag with a name of *"music"* and a status of *"public"*.
To handle both of these situations, Django has a consistent way of processing
:meth:`~django.db.models.query.QuerySet.filter` calls. Everything inside a
single :meth:`~django.db.models.query.QuerySet.filter` call is applied
simultaneously to filter out items matching all those requirements. Successive
:meth:`~django.db.models.query.QuerySet.filter` calls further restrict the set
of objects, but for multi-valued relations, they apply to any object linked to
the primary model, not necessarily those objects that were selected by an
earlier :meth:`~django.db.models.query.QuerySet.filter` call.
That may sound a bit confusing, so hopefully an example will clarify. To
select all blogs that contain entries with both *"Lennon"* in the headline
and that were published in 2008 (the same entry satisfying both conditions),
we would write::
To select all blogs containing at least one entry from 2008 having *"Lennon"*
in its headline (the same entry satisfying both conditions), we would write::
Blog.objects.filter(entry__headline__contains='Lennon', entry__pub_date__year=2008)
To select all blogs that contain an entry with *"Lennon"* in the headline
**as well as** an entry that was published in 2008, we would write::
Otherwise, to perform a more permissive query selecting any blogs with merely
*some* entry with *"Lennon"* in its headline and *some* entry from 2008, we
would write::
Blog.objects.filter(entry__headline__contains='Lennon').filter(entry__pub_date__year=2008)
Suppose there is only one blog that had both entries containing *"Lennon"* and
Suppose there is only one blog that has both entries containing *"Lennon"* and
entries from 2008, but that none of the entries from 2008 contained *"Lennon"*.
The first query would not return any blogs, but the second query would return
that one blog.
that one blog. (This is because the entries selected by the second filter may
or may not be the same as the entries in the first filter. We are filtering the
``Blog`` items with each filter statement, not the ``Entry`` items.) In short,
if each condition needs to match the same related object, then each should be
contained in a single :meth:`~django.db.models.query.QuerySet.filter` call.
In the second example, the first filter restricts the queryset to all those
blogs linked to entries with *"Lennon"* in the headline. The second filter
restricts the set of blogs *further* to those that are also linked to entries
that were published in 2008. The entries selected by the second filter may or
may not be the same as the entries in the first filter. We are filtering the
``Blog`` items with each filter statement, not the ``Entry`` items.
.. note::
As the second (more permissive) query chains multiple filters, it performs
multiple joins to the primary model, potentially yielding duplicates.
>>> from datetime import date
>>> beatles = Blog.objects.create(name='Beatles Blog')
>>> pop = Blog.objects.create(name='Pop Music Blog')
>>> Entry.objects.create(
... blog=beatles,
... headline='New Lennon Biography',
... pub_date=date(2008, 6, 1),
... )
<Entry: New Lennon Biography>
>>> Entry.objects.create(
... blog=beatles,
... headline='New Lennon Biography in Paperback',
... pub_date=date(2009, 6, 1),
... )
<Entry: New Lennon Biography in Paperback>
>>> Entry.objects.create(
... blog=pop,
... headline='Best Albums of 2008',
... pub_date=date(2008, 12, 15),
... )
<Entry: Best Albums of 2008>
>>> Entry.objects.create(
... blog=pop,
... headline='Lennon Would Have Loved Hip Hop',
... pub_date=date(2020, 4, 1),
... )
<Entry: Lennon Would Have Loved Hip Hop>
>>> Blog.objects.filter(
... entry__headline__contains='Lennon',
... entry__pub_date__year=2008,
... )
<QuerySet [<Blog: Beatles Blog>]>
>>> Blog.objects.filter(
... entry__headline__contains='Lennon',
... ).filter(
... entry__pub_date__year=2008,
... )
<QuerySet [<Blog: Beatles Blog>, <Blog: Beatles Blog>, <Blog: Pop Music Blog]>
.. note::