Fixed #5768 -- Added support for ManyToManyFields and reverse relations in values() and values_list(). Thanks to mrmachine for the patch.

git-svn-id: http://code.djangoproject.com/svn/django/trunk@14655 bcc190cf-cafb-0310-a4f2-bffc1f526a37
This commit is contained in:
Carl Meyer 2010-11-21 02:28:25 +00:00
parent 7592d68541
commit 9b432cb67b
4 changed files with 110 additions and 14 deletions

View File

@ -870,7 +870,7 @@ class ValuesQuerySet(QuerySet):
self.query.select = [] self.query.select = []
if self.extra_names is not None: if self.extra_names is not None:
self.query.set_extra_mask(self.extra_names) self.query.set_extra_mask(self.extra_names)
self.query.add_fields(self.field_names, False) self.query.add_fields(self.field_names, True)
if self.aggregate_names is not None: if self.aggregate_names is not None:
self.query.set_aggregate_mask(self.aggregate_names) self.query.set_aggregate_mask(self.aggregate_names)

View File

@ -398,11 +398,8 @@ Example::
>>> Blog.objects.values('id', 'name') >>> Blog.objects.values('id', 'name')
[{'id': 1, 'name': 'Beatles Blog'}] [{'id': 1, 'name': 'Beatles Blog'}]
A couple of subtleties that are worth mentioning: A few subtleties that are worth mentioning:
* The ``values()`` method does not return anything for
:class:`~django.db.models.ManyToManyField` attributes and will raise an
error if you try to pass in this type of field to it.
* If you have a field called ``foo`` that is a * If you have a field called ``foo`` that is a
:class:`~django.db.models.ForeignKey`, the default ``values()`` call :class:`~django.db.models.ForeignKey`, the default ``values()`` call
will return a dictionary key called ``foo_id``, since this is the name will return a dictionary key called ``foo_id``, since this is the name
@ -453,6 +450,28 @@ followed (optionally) by any output-affecting methods (such as ``values()``),
but it doesn't really matter. This is your chance to really flaunt your but it doesn't really matter. This is your chance to really flaunt your
individualism. individualism.
.. versionchanged:: 1.3
The ``values()`` method previously did not return anything for
:class:`~django.db.models.ManyToManyField` attributes and would raise an error
if you tried to pass this type of field to it.
This restriction has been lifted, and you can now also refer to fields on
related models with reverse relations through ``OneToOneField``, ``ForeignKey``
and ``ManyToManyField`` attributes::
Blog.objects.values('name', 'entry__headline')
[{'name': 'My blog', 'entry__headline': 'An entry'},
{'name': 'My blog', 'entry__headline': 'Another entry'}, ...]
.. warning::
Because :class:`~django.db.models.ManyToManyField` attributes and reverse
relations can have multiple related rows, including these can have a
multiplier effect on the size of your result set. This will be especially
pronounced if you include multiple such fields in your ``values()`` query,
in which case all possible combinations will be returned.
``values_list(*fields)`` ``values_list(*fields)``
~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~

View File

@ -7,11 +7,23 @@ This demonstrates features of the database API.
from django.db import models, DEFAULT_DB_ALIAS, connection from django.db import models, DEFAULT_DB_ALIAS, connection
from django.conf import settings from django.conf import settings
class Author(models.Model):
name = models.CharField(max_length=100)
class Meta:
ordering = ('name', )
class Article(models.Model): class Article(models.Model):
headline = models.CharField(max_length=100) headline = models.CharField(max_length=100)
pub_date = models.DateTimeField() pub_date = models.DateTimeField()
author = models.ForeignKey(Author, blank=True, null=True)
class Meta: class Meta:
ordering = ('-pub_date', 'headline') ordering = ('-pub_date', 'headline')
def __unicode__(self): def __unicode__(self):
return self.headline return self.headline
class Tag(models.Model):
articles = models.ManyToManyField(Article)
name = models.CharField(max_length=100)
class Meta:
ordering = ('name', )

View File

@ -3,28 +3,43 @@ from operator import attrgetter
from django.core.exceptions import FieldError from django.core.exceptions import FieldError
from django.db import connection from django.db import connection
from django.test import TestCase, skipUnlessDBFeature from django.test import TestCase, skipUnlessDBFeature
from models import Article from models import Author, Article, Tag
class LookupTests(TestCase): class LookupTests(TestCase):
#def setUp(self): #def setUp(self):
def setUp(self): def setUp(self):
# Create a few Authors.
self.au1 = Author(name='Author 1')
self.au1.save()
self.au2 = Author(name='Author 2')
self.au2.save()
# Create a couple of Articles. # Create a couple of Articles.
self.a1 = Article(headline='Article 1', pub_date=datetime(2005, 7, 26)) self.a1 = Article(headline='Article 1', pub_date=datetime(2005, 7, 26), author=self.au1)
self.a1.save() self.a1.save()
self.a2 = Article(headline='Article 2', pub_date=datetime(2005, 7, 27)) self.a2 = Article(headline='Article 2', pub_date=datetime(2005, 7, 27), author=self.au1)
self.a2.save() self.a2.save()
self.a3 = Article(headline='Article 3', pub_date=datetime(2005, 7, 27)) self.a3 = Article(headline='Article 3', pub_date=datetime(2005, 7, 27), author=self.au1)
self.a3.save() self.a3.save()
self.a4 = Article(headline='Article 4', pub_date=datetime(2005, 7, 28)) self.a4 = Article(headline='Article 4', pub_date=datetime(2005, 7, 28), author=self.au1)
self.a4.save() self.a4.save()
self.a5 = Article(headline='Article 5', pub_date=datetime(2005, 8, 1, 9, 0)) self.a5 = Article(headline='Article 5', pub_date=datetime(2005, 8, 1, 9, 0), author=self.au2)
self.a5.save() self.a5.save()
self.a6 = Article(headline='Article 6', pub_date=datetime(2005, 8, 1, 8, 0)) self.a6 = Article(headline='Article 6', pub_date=datetime(2005, 8, 1, 8, 0), author=self.au2)
self.a6.save() self.a6.save()
self.a7 = Article(headline='Article 7', pub_date=datetime(2005, 7, 27)) self.a7 = Article(headline='Article 7', pub_date=datetime(2005, 7, 27), author=self.au2)
self.a7.save() self.a7.save()
# Create a few Tags.
self.t1 = Tag(name='Tag 1')
self.t1.save()
self.t1.articles.add(self.a1, self.a2, self.a3)
self.t2 = Tag(name='Tag 2')
self.t2.save()
self.t2.articles.add(self.a3, self.a4, self.a5)
self.t3 = Tag(name='Tag 3')
self.t3.save()
self.t3.articles.add(self.a5, self.a6, self.a7)
def test_exists(self): def test_exists(self):
# We can use .exists() to check that there are some # We can use .exists() to check that there are some
@ -182,6 +197,42 @@ class LookupTests(TestCase):
'id_plus_seven': self.a1.id + 7, 'id_plus_seven': self.a1.id + 7,
'id_plus_eight': self.a1.id + 8, 'id_plus_eight': self.a1.id + 8,
}], transform=identity) }], transform=identity)
# You can specify fields from forward and reverse relations, just like filter().
self.assertQuerysetEqual(
Article.objects.values('headline', 'author__name'),
[
{'headline': self.a5.headline, 'author__name': self.au2.name},
{'headline': self.a6.headline, 'author__name': self.au2.name},
{'headline': self.a4.headline, 'author__name': self.au1.name},
{'headline': self.a2.headline, 'author__name': self.au1.name},
{'headline': self.a3.headline, 'author__name': self.au1.name},
{'headline': self.a7.headline, 'author__name': self.au2.name},
{'headline': self.a1.headline, 'author__name': self.au1.name},
], transform=identity)
self.assertQuerysetEqual(
Author.objects.values('name', 'article__headline').order_by('name', 'article__headline'),
[
{'name': self.au1.name, 'article__headline': self.a1.headline},
{'name': self.au1.name, 'article__headline': self.a2.headline},
{'name': self.au1.name, 'article__headline': self.a3.headline},
{'name': self.au1.name, 'article__headline': self.a4.headline},
{'name': self.au2.name, 'article__headline': self.a5.headline},
{'name': self.au2.name, 'article__headline': self.a6.headline},
{'name': self.au2.name, 'article__headline': self.a7.headline},
], transform=identity)
self.assertQuerysetEqual(
Author.objects.values('name', 'article__headline', 'article__tag__name').order_by('name', 'article__headline', 'article__tag__name'),
[
{'name': self.au1.name, 'article__headline': self.a1.headline, 'article__tag__name': self.t1.name},
{'name': self.au1.name, 'article__headline': self.a2.headline, 'article__tag__name': self.t1.name},
{'name': self.au1.name, 'article__headline': self.a3.headline, 'article__tag__name': self.t1.name},
{'name': self.au1.name, 'article__headline': self.a3.headline, 'article__tag__name': self.t2.name},
{'name': self.au1.name, 'article__headline': self.a4.headline, 'article__tag__name': self.t2.name},
{'name': self.au2.name, 'article__headline': self.a5.headline, 'article__tag__name': self.t2.name},
{'name': self.au2.name, 'article__headline': self.a5.headline, 'article__tag__name': self.t3.name},
{'name': self.au2.name, 'article__headline': self.a6.headline, 'article__tag__name': self.t3.name},
{'name': self.au2.name, 'article__headline': self.a7.headline, 'article__tag__name': self.t3.name},
], transform=identity)
# However, an exception FieldDoesNotExist will be thrown if you specify # However, an exception FieldDoesNotExist will be thrown if you specify
# a non-existent field name in values() (a field that is neither in the # a non-existent field name in values() (a field that is neither in the
# model nor in extra(select)). # model nor in extra(select)).
@ -192,6 +243,7 @@ class LookupTests(TestCase):
self.assertQuerysetEqual(Article.objects.filter(id=self.a5.id).values(), self.assertQuerysetEqual(Article.objects.filter(id=self.a5.id).values(),
[{ [{
'id': self.a5.id, 'id': self.a5.id,
'author_id': self.au2.id,
'headline': 'Article 5', 'headline': 'Article 5',
'pub_date': datetime(2005, 8, 1, 9, 0) 'pub_date': datetime(2005, 8, 1, 9, 0)
}], transform=identity) }], transform=identity)
@ -250,6 +302,19 @@ class LookupTests(TestCase):
(self.a7.id, self.a7.id+1) (self.a7.id, self.a7.id+1)
], ],
transform=identity) transform=identity)
self.assertQuerysetEqual(
Author.objects.values_list('name', 'article__headline', 'article__tag__name').order_by('name', 'article__headline', 'article__tag__name'),
[
(self.au1.name, self.a1.headline, self.t1.name),
(self.au1.name, self.a2.headline, self.t1.name),
(self.au1.name, self.a3.headline, self.t1.name),
(self.au1.name, self.a3.headline, self.t2.name),
(self.au1.name, self.a4.headline, self.t2.name),
(self.au2.name, self.a5.headline, self.t2.name),
(self.au2.name, self.a5.headline, self.t3.name),
(self.au2.name, self.a6.headline, self.t3.name),
(self.au2.name, self.a7.headline, self.t3.name),
], transform=identity)
self.assertRaises(TypeError, Article.objects.values_list, 'id', 'headline', flat=True) self.assertRaises(TypeError, Article.objects.values_list, 'id', 'headline', flat=True)
def test_get_next_previous_by(self): def test_get_next_previous_by(self):
@ -402,7 +467,7 @@ class LookupTests(TestCase):
self.fail('FieldError not raised') self.fail('FieldError not raised')
except FieldError, ex: except FieldError, ex:
self.assertEqual(str(ex), "Cannot resolve keyword 'pub_date_year' " self.assertEqual(str(ex), "Cannot resolve keyword 'pub_date_year' "
"into field. Choices are: headline, id, pub_date") "into field. Choices are: author, headline, id, pub_date, tag")
try: try:
Article.objects.filter(headline__starts='Article') Article.objects.filter(headline__starts='Article')
self.fail('FieldError not raised') self.fail('FieldError not raised')