[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 Spanning multi-valued relationships
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
When you are filtering an object based on a When spanning a :class:`~django.db.models.ManyToManyField` or a reverse
:class:`~django.db.models.ManyToManyField` or a reverse :class:`~django.db.models.ForeignKey` (such as from ``Blog`` to ``Entry``),
:class:`~django.db.models.ForeignKey`, there are two different sorts of filter filtering on multiple attributes raises the question of whether to require each
you may be interested in. Consider the ``Blog``/``Entry`` relationship attribute to coincide in the same related object. We might seek blogs that have
(``Blog`` to ``Entry`` is a one-to-many relation). We might be interested in an entry from 2008 with *“Lennon”* in its headline, or we might seek blogs that
finding blogs that have an entry which has both *"Lennon"* in the headline and merely have any entry from 2008 as well as some newer or older entry with
was published in 2008. Or we might want to find blogs that have an entry with *“Lennon”* in its headline.
*"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.
The same type of situation arises with a To select all blogs containing at least one entry from 2008 having *"Lennon"*
:class:`~django.db.models.ManyToManyField`. For example, if an ``Entry`` has a in its headline (the same entry satisfying both conditions), we would write::
: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::
Blog.objects.filter(entry__headline__contains='Lennon', entry__pub_date__year=2008) 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 Otherwise, to perform a more permissive query selecting any blogs with merely
**as well as** an entry that was published in 2008, we would write:: *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) 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"*. 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 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 .. note::
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 As the second (more permissive) query chains multiple filters, it performs
that were published in 2008. The entries selected by the second filter may or multiple joins to the primary model, potentially yielding duplicates.
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. >>> 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:: .. note::