From 349c12d3f5c015c2f8b36917da21230c2ac1acb4 Mon Sep 17 00:00:00 2001 From: Marc Tamlyn Date: Tue, 15 Oct 2013 13:15:02 +0100 Subject: [PATCH] Fixed #16855 -- select_related() chains as expected. select_related('foo').select_related('bar') is now equivalent to select_related('foo', 'bar'). Also reworded docs to recommend select_related(*fields) over select_related() --- django/db/models/sql/query.py | 5 ++- docs/ref/models/querysets.txt | 74 ++++++++++++---------------------- docs/releases/1.7.txt | 6 +++ tests/select_related/models.py | 9 +++++ tests/select_related/tests.py | 11 ++++- 5 files changed, 55 insertions(+), 50 deletions(-) diff --git a/django/db/models/sql/query.py b/django/db/models/sql/query.py index 9e54b97374..8be55d7962 100644 --- a/django/db/models/sql/query.py +++ b/django/db/models/sql/query.py @@ -1712,7 +1712,10 @@ class Query(object): certain related models (as opposed to all models, when self.select_related=True). """ - field_dict = {} + if isinstance(self.select_related, bool): + field_dict = {} + else: + field_dict = self.select_related for field in fields: d = field_dict for part in field.split(LOOKUP_SEP): diff --git a/docs/ref/models/querysets.txt b/docs/ref/models/querysets.txt index e2b6efc93a..170df975bd 100644 --- a/docs/ref/models/querysets.txt +++ b/docs/ref/models/querysets.txt @@ -688,13 +688,12 @@ manager or a ``QuerySet`` and do further filtering on the result. After calling select_related ~~~~~~~~~~~~~~ -.. method:: select_related() +.. method:: select_related(*fields) -Returns a ``QuerySet`` that will automatically "follow" foreign-key -relationships, selecting that additional related-object data when it executes -its query. This is a performance booster which results in (sometimes much) -larger queries but means later use of foreign-key relationships won't require -database queries. +Returns a ``QuerySet`` that will "follow" foreign-key relationships, selecting +additional related-object data when it executes its query. This is a +performance booster which results in a single more complex query but means +later use of foreign-key relationships won't require database queries. The following examples illustrate the difference between plain lookups and ``select_related()`` lookups. Here's standard lookup:: @@ -708,13 +707,13 @@ The following examples illustrate the difference between plain lookups and And here's ``select_related`` lookup:: # Hits the database. - e = Entry.objects.select_related().get(id=5) + e = Entry.objects.select_related('blog').get(id=5) # Doesn't hit the database, because e.blog has been prepopulated # in the previous query. b = e.blog -``select_related()`` follows foreign keys as far as possible. If you have the +You can follow foreign keys in a similar way to querying them. If you have the following models:: from django.db import models @@ -731,10 +730,11 @@ following models:: # ... author = models.ForeignKey(Person) -...then a call to ``Book.objects.select_related().get(id=4)`` will cache the -related ``Person`` *and* the related ``City``:: +...then a call to ``Book.objects.select_related('person', +'person__city').get(id=4)`` will cache the related ``Person`` *and* the related +``City``:: - b = Book.objects.select_related().get(id=4) + b = Book.objects.select_related('person__city').get(id=4) p = b.author # Doesn't hit the database. c = p.hometown # Doesn't hit the database. @@ -742,45 +742,9 @@ related ``Person`` *and* the related ``City``:: p = b.author # Hits the database. c = p.hometown # Hits the database. -Note that, by default, ``select_related()`` does not follow foreign keys that -have ``null=True``. - -Usually, using ``select_related()`` can vastly improve performance because your -app can avoid many database calls. However, there are times you are only -interested in specific related models, or have deeply nested sets of -relationships, and in these cases ``select_related()`` can be optimized by -explicitly passing the related field names you are interested in. Only -the specified relations will be followed. - -You can even do this for models that are more than one relation away by -separating the field names with double underscores, just as for filters. For -example, if you have this model:: - - class Room(models.Model): - # ... - building = models.ForeignKey(...) - - class Group(models.Model): - # ... - teacher = models.ForeignKey(...) - room = models.ForeignKey(Room) - subject = models.ForeignKey(...) - -...and you only needed to work with the ``room`` and ``subject`` attributes, -you could write this:: - - g = Group.objects.select_related('room', 'subject') - -This is also valid:: - - g = Group.objects.select_related('room__building', 'subject') - -...and would also pull in the ``building`` relation. - You can refer to any :class:`~django.db.models.ForeignKey` or :class:`~django.db.models.OneToOneField` relation in the list of fields -passed to ``select_related()``. This includes foreign keys that have -``null=True`` (which are omitted in a no-parameter ``select_related()`` call). +passed to ``select_related()``. You can also refer to the reverse direction of a :class:`~django.db.models.OneToOneField` in the list of fields passed to @@ -789,6 +753,13 @@ You can also refer to the reverse direction of a is defined. Instead of specifying the field name, use the :attr:`related_name ` for the field on the related object. +There may be some situations where you wish to call ``select_related()`` with a +lot of related objects, or where you don't know all of the relations. In these +cases it is possible to call ``select_related()`` with no arguments. This will +follow all non-null foreign keys it can find - nullable foreign keys must be +specified. This is not recommended in most cases as it is likely to make the +underlying query more complex, and return more data, than is actually needed. + .. versionadded:: 1.6 If you need to clear the list of related fields added by past calls of @@ -796,6 +767,13 @@ If you need to clear the list of related fields added by past calls of >>> without_relations = queryset.select_related(None) +.. versionchanged:: 1.7 + +Chaining ``select_related`` calls now works in a similar way to other methods - +that is that ``select_related('foo', 'bar')`` is equivalent to +``select_related('foo').select_related('bar')``. Previously the latter would +have been equivalent to ``select_related('bar')``. + prefetch_related ~~~~~~~~~~~~~~~~ diff --git a/docs/releases/1.7.txt b/docs/releases/1.7.txt index a3fdc0e9f3..aa613c4538 100644 --- a/docs/releases/1.7.txt +++ b/docs/releases/1.7.txt @@ -533,6 +533,12 @@ Miscellaneous you relied on the default field ordering while having fields defined on both the current class *and* on a parent ``Form``. +* :meth:`~django.db.models.query.QuerySet.select_related` now chains in the + same way as other similar calls like ``prefetch_related``. That is, + ``select_related('foo', 'bar')`` is equivalent to + ``select_related('foo').select_related('bar')``. Previously the latter would + have been equivalent to ``select_related('bar')``. + Features deprecated in 1.7 ========================== diff --git a/tests/select_related/models.py b/tests/select_related/models.py index ec41957adf..11ef7db91f 100644 --- a/tests/select_related/models.py +++ b/tests/select_related/models.py @@ -66,3 +66,12 @@ class Species(models.Model): genus = models.ForeignKey(Genus) def __str__(self): return self.name + +# and we'll invent a new thing so we have a model with two foreign keys +@python_2_unicode_compatible +class HybridSpecies(models.Model): + name = models.CharField(max_length=50) + parent_1 = models.ForeignKey(Species, related_name='child_1') + parent_2 = models.ForeignKey(Species, related_name='child_2') + def __str__(self): + return self.name diff --git a/tests/select_related/tests.py b/tests/select_related/tests.py index f07e28df99..8949d99788 100644 --- a/tests/select_related/tests.py +++ b/tests/select_related/tests.py @@ -2,7 +2,7 @@ from __future__ import unicode_literals from django.test import TestCase -from .models import Domain, Kingdom, Phylum, Klass, Order, Family, Genus, Species +from .models import Domain, Kingdom, Phylum, Klass, Order, Family, Genus, Species, HybridSpecies class SelectRelatedTests(TestCase): @@ -144,3 +144,12 @@ class SelectRelatedTests(TestCase): def test_none_clears_list(self): queryset = Species.objects.select_related('genus').select_related(None) self.assertEqual(queryset.query.select_related, False) + + def test_chaining(self): + parent_1, parent_2 = Species.objects.all()[:2] + HybridSpecies.objects.create(name='hybrid', parent_1=parent_1, parent_2=parent_2) + queryset = HybridSpecies.objects.select_related('parent_1').select_related('parent_2') + with self.assertNumQueries(1): + obj = queryset[0] + self.assertEquals(obj.parent_1, parent_1) + self.assertEquals(obj.parent_2, parent_2)