Fixed #19195 -- Allow explicit ordering by a relation `_id` field.

Thanks to chrisedgemon for the report and shaib, akaariai and
timgraham for the review.
This commit is contained in:
Simon Charette 2014-04-26 03:34:20 -04:00
parent a5f6cbce07
commit 24ec9538b7
5 changed files with 115 additions and 52 deletions

View File

@ -464,8 +464,9 @@ class SQLCompiler(object):
field, targets, alias, joins, path, opts = self._setup_joins(pieces, opts, alias) field, targets, alias, joins, path, opts = self._setup_joins(pieces, opts, alias)
# If we get to this point and the field is a relation to another model, # If we get to this point and the field is a relation to another model,
# append the default ordering for that model. # append the default ordering for that model unless the attribute name
if field.rel and path and opts.ordering: # of the field is specified.
if field.rel and path and opts.ordering and name != field.attname:
# Firstly, avoid infinite loops. # Firstly, avoid infinite loops.
if not already_seen: if not already_seen:
already_seen = set() already_seen = set()

View File

@ -294,6 +294,18 @@ primary key if there is no :attr:`Meta.ordering
...since the ``Blog`` model has no default ordering specified. ...since the ``Blog`` model has no default ordering specified.
.. versionadded:: 1.7
Note that it is also possible to order a queryset by a related field,
without incurring the cost of a JOIN, by referring to the ``_id`` of the
related field::
# No Join
Entry.objects.order_by('blog_id')
# Join
Entry.objects.order_by('blog__id')
Be cautious when ordering by fields in related models if you are also using Be cautious when ordering by fields in related models if you are also using
:meth:`distinct()`. See the note in :meth:`distinct` for an explanation of how :meth:`distinct()`. See the note in :meth:`distinct` for an explanation of how
related model ordering can change the expected results. related model ordering can change the expected results.
@ -435,6 +447,21 @@ Examples (those after the first will only work on PostgreSQL)::
>>> Entry.objects.order_by('author', 'pub_date').distinct('author') >>> Entry.objects.order_by('author', 'pub_date').distinct('author')
[...] [...]
.. note::
Keep in mind that :meth:`order_by` uses any default related model ordering
that has been defined. You might have to explicitly order by the relation
``_id`` or referenced field to make sure the ``DISTINCT ON`` expressions
match those at the beginning of the ``ORDER BY`` clause. For example, if
the ``Blog`` model defined an :attr:`~django.db.models.Options.ordering` by
``name``::
Entry.objects.order_by('blog').distinct('blog')
...wouldn't work because the query would be ordered by ``blog__name`` thus
mismatching the ``DISTINCT ON`` expression. You'd have to explicitly order
by the relation `_id` field (``blog_id`` in this case) or the referenced
one (``blog__pk``) to make sure both expressions match.
values values
~~~~~~ ~~~~~~

View File

@ -702,6 +702,9 @@ Models
Previously model field validation didn't prevent values out of their associated Previously model field validation didn't prevent values out of their associated
column data type range from being saved resulting in an integrity error. column data type range from being saved resulting in an integrity error.
* It is now possible to explicitly :meth:`~django.db.models.query.QuerySet.order_by`
a relation ``_id`` field by using its attribute name.
Signals Signals
^^^^^^^ ^^^^^^^

View File

@ -17,8 +17,14 @@ from django.db import models
from django.utils.encoding import python_2_unicode_compatible from django.utils.encoding import python_2_unicode_compatible
class Author(models.Model):
class Meta:
ordering = ('-pk',)
@python_2_unicode_compatible @python_2_unicode_compatible
class Article(models.Model): class Article(models.Model):
author = models.ForeignKey(Author, null=True)
headline = models.CharField(max_length=100) headline = models.CharField(max_length=100)
pub_date = models.DateTimeField() pub_date = models.DateTimeField()
@ -27,15 +33,3 @@ class Article(models.Model):
def __str__(self): def __str__(self):
return self.headline return self.headline
@python_2_unicode_compatible
class ArticlePKOrdering(models.Model):
headline = models.CharField(max_length=100)
pub_date = models.DateTimeField()
class Meta:
ordering = ('-pk',)
def __str__(self):
return self.headline

View File

@ -5,26 +5,29 @@ from operator import attrgetter
from django.test import TestCase from django.test import TestCase
from .models import Article, ArticlePKOrdering from .models import Article, Author
class OrderingTests(TestCase): class OrderingTests(TestCase):
def test_basic(self): def setUp(self):
Article.objects.create( self.a1 = Article.objects.create(
headline="Article 1", pub_date=datetime(2005, 7, 26) headline="Article 1", pub_date=datetime(2005, 7, 26)
) )
Article.objects.create( self.a2 = Article.objects.create(
headline="Article 2", pub_date=datetime(2005, 7, 27) headline="Article 2", pub_date=datetime(2005, 7, 27)
) )
Article.objects.create( self.a3 = Article.objects.create(
headline="Article 3", pub_date=datetime(2005, 7, 27) headline="Article 3", pub_date=datetime(2005, 7, 27)
) )
a4 = Article.objects.create( self.a4 = Article.objects.create(
headline="Article 4", pub_date=datetime(2005, 7, 28) headline="Article 4", pub_date=datetime(2005, 7, 28)
) )
# By default, Article.objects.all() orders by pub_date descending, then def test_default_ordering(self):
# headline ascending. """
By default, Article.objects.all() orders by pub_date descending, then
headline ascending.
"""
self.assertQuerysetEqual( self.assertQuerysetEqual(
Article.objects.all(), [ Article.objects.all(), [
"Article 4", "Article 4",
@ -35,8 +38,14 @@ class OrderingTests(TestCase):
attrgetter("headline") attrgetter("headline")
) )
# Override ordering with order_by, which is in the same format as the # Getting a single item should work too:
# ordering attribute in models. self.assertEqual(Article.objects.all()[0], self.a4)
def test_default_ordering_override(self):
"""
Override ordering with order_by, which is in the same format as the
ordering attribute in models.
"""
self.assertQuerysetEqual( self.assertQuerysetEqual(
Article.objects.order_by("headline"), [ Article.objects.order_by("headline"), [
"Article 1", "Article 1",
@ -56,8 +65,11 @@ class OrderingTests(TestCase):
attrgetter("headline") attrgetter("headline")
) )
# Only the last order_by has any effect (since they each override any def test_order_by_override(self):
# previous ordering). """
Only the last order_by has any effect (since they each override any
previous ordering).
"""
self.assertQuerysetEqual( self.assertQuerysetEqual(
Article.objects.order_by("id"), [ Article.objects.order_by("id"), [
"Article 1", "Article 1",
@ -77,7 +89,10 @@ class OrderingTests(TestCase):
attrgetter("headline") attrgetter("headline")
) )
# Use the 'stop' part of slicing notation to limit the results. def test_stop_slicing(self):
"""
Use the 'stop' part of slicing notation to limit the results.
"""
self.assertQuerysetEqual( self.assertQuerysetEqual(
Article.objects.order_by("headline")[:2], [ Article.objects.order_by("headline")[:2], [
"Article 1", "Article 1",
@ -86,8 +101,11 @@ class OrderingTests(TestCase):
attrgetter("headline") attrgetter("headline")
) )
# Use the 'stop' and 'start' parts of slicing notation to offset the def test_stop_start_slicing(self):
# result list. """
Use the 'stop' and 'start' parts of slicing notation to offset the
result list.
"""
self.assertQuerysetEqual( self.assertQuerysetEqual(
Article.objects.order_by("headline")[1:3], [ Article.objects.order_by("headline")[1:3], [
"Article 2", "Article 2",
@ -96,17 +114,20 @@ class OrderingTests(TestCase):
attrgetter("headline") attrgetter("headline")
) )
# Getting a single item should work too: def test_random_ordering(self):
self.assertEqual(Article.objects.all()[0], a4) """
Use '?' to order randomly.
# Use '?' to order randomly. """
self.assertEqual( self.assertEqual(
len(list(Article.objects.order_by("?"))), 4 len(list(Article.objects.order_by("?"))), 4
) )
# Ordering can be reversed using the reverse() method on a queryset. def test_reversed_ordering(self):
# This allows you to extract things like "the last two items" (reverse """
# and then take the first two). Ordering can be reversed using the reverse() method on a queryset.
This allows you to extract things like "the last two items" (reverse
and then take the first two).
"""
self.assertQuerysetEqual( self.assertQuerysetEqual(
Article.objects.all().reverse()[:2], [ Article.objects.all().reverse()[:2], [
"Article 1", "Article 1",
@ -115,7 +136,10 @@ class OrderingTests(TestCase):
attrgetter("headline") attrgetter("headline")
) )
# Ordering can be based on fields included from an 'extra' clause def test_extra_ordering(self):
"""
Ordering can be based on fields included from an 'extra' clause
"""
self.assertQuerysetEqual( self.assertQuerysetEqual(
Article.objects.extra(select={"foo": "pub_date"}, order_by=["foo", "headline"]), [ Article.objects.extra(select={"foo": "pub_date"}, order_by=["foo", "headline"]), [
"Article 1", "Article 1",
@ -126,8 +150,11 @@ class OrderingTests(TestCase):
attrgetter("headline") attrgetter("headline")
) )
# If the extra clause uses an SQL keyword for a name, it will be def test_extra_ordering_quoting(self):
# protected by quoting. """
If the extra clause uses an SQL keyword for a name, it will be
protected by quoting.
"""
self.assertQuerysetEqual( self.assertQuerysetEqual(
Article.objects.extra(select={"order": "pub_date"}, order_by=["order", "headline"]), [ Article.objects.extra(select={"order": "pub_date"}, order_by=["order", "headline"]), [
"Article 1", "Article 1",
@ -143,21 +170,32 @@ class OrderingTests(TestCase):
Ensure that 'pk' works as an ordering option in Meta. Ensure that 'pk' works as an ordering option in Meta.
Refs #8291. Refs #8291.
""" """
ArticlePKOrdering.objects.create( Author.objects.create(pk=1)
pk=1, headline="Article 1", pub_date=datetime(2005, 7, 26) Author.objects.create(pk=2)
) Author.objects.create(pk=3)
ArticlePKOrdering.objects.create( Author.objects.create(pk=4)
pk=2, headline="Article 2", pub_date=datetime(2005, 7, 27)
)
ArticlePKOrdering.objects.create(
pk=3, headline="Article 3", pub_date=datetime(2005, 7, 27)
)
ArticlePKOrdering.objects.create(
pk=4, headline="Article 4", pub_date=datetime(2005, 7, 28)
)
self.assertQuerysetEqual( self.assertQuerysetEqual(
ArticlePKOrdering.objects.all(), [ Author.objects.all(), [
4, 3, 2, 1
],
attrgetter("pk")
)
def test_order_by_fk_attname(self):
"""
Ensure that ordering by a foreign key by its attribute name prevents
the query from inheriting it's related model ordering option.
Refs #19195.
"""
for i in range(1, 5):
author = Author.objects.create(pk=i)
article = getattr(self, "a%d" % (5 - i))
article.author = author
article.save(update_fields={'author'})
self.assertQuerysetEqual(
Article.objects.order_by('author_id'), [
"Article 4", "Article 4",
"Article 3", "Article 3",
"Article 2", "Article 2",