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:
parent
7592d68541
commit
9b432cb67b
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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)``
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
|
|
@ -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', )
|
||||||
|
|
|
@ -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')
|
||||||
|
|
Loading…
Reference in New Issue