From 6174814dbe04fb6668aa212a6cdbca765a8b0522 Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Fri, 24 Dec 2021 09:12:39 -0500 Subject: [PATCH] Fixed #27936 -- Rewrote spanning multi-valued relationships docs. --- docs/topics/db/queries.txt | 100 ++++++++++++++++++++++--------------- 1 file changed, 61 insertions(+), 39 deletions(-) diff --git a/docs/topics/db/queries.txt b/docs/topics/db/queries.txt index d32bca9e30..23326fb778 100644 --- a/docs/topics/db/queries.txt +++ b/docs/topics/db/queries.txt @@ -532,55 +532,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.objects.create( + ... blog=beatles, + ... headline='New Lennon Biography in Paperback', + ... pub_date=date(2009, 6, 1), + ... ) + + >>> Entry.objects.create( + ... blog=pop, + ... headline='Best Albums of 2008', + ... pub_date=date(2008, 12, 15), + ... ) + + >>> Entry.objects.create( + ... blog=pop, + ... headline='Lennon Would Have Loved Hip Hop', + ... pub_date=date(2020, 4, 1), + ... ) + + >>> Blog.objects.filter( + ... entry__headline__contains='Lennon', + ... entry__pub_date__year=2008, + ... ) + ]> + >>> Blog.objects.filter( + ... entry__headline__contains='Lennon', + ... ).filter( + ... entry__pub_date__year=2008, + ... ) + , , .. note::