Fixed #7314 -- Changed the way extra() bits are handled when QuerySets are merged.

Also added a section to the documentation to indicate why it's probably not a
good idea to rely on this feature for complex stuff. Garbage in, garbage out
applies even to Django code.

Thanks to erik for the test case for this one.


git-svn-id: http://code.djangoproject.com/svn/django/trunk@7791 bcc190cf-cafb-0310-a4f2-bffc1f526a37
This commit is contained in:
Malcolm Tredinnick 2008-06-30 06:24:21 +00:00
parent f9df4d1435
commit 5da47e43c7
4 changed files with 95 additions and 4 deletions

View File

@ -366,10 +366,21 @@ class Query(object):
item.relabel_aliases(change_map) item.relabel_aliases(change_map)
self.select.append(item) self.select.append(item)
self.select_fields = rhs.select_fields[:] self.select_fields = rhs.select_fields[:]
self.extra_select = rhs.extra_select.copy()
self.extra_tables = rhs.extra_tables if connector == OR:
self.extra_where = rhs.extra_where # It would be nice to be able to handle this, but the queries don't
self.extra_params = rhs.extra_params # really make sense (or return consistent value sets). Not worth
# the extra complexity when you can write a real query instead.
if self.extra_select and rhs.extra_select:
raise ValueError("When merging querysets using 'or', you "
"cannot have extra(select=...) on both sides.")
if self.extra_where and rhs.extra_where:
raise ValueError("When merging querysets using 'or', you "
"cannot have extra(where=...) on both sides.")
self.extra_select.update(rhs.extra_select)
self.extra_tables += rhs.extra_tables
self.extra_where += rhs.extra_where
self.extra_params += rhs.extra_params
# Ordering uses the 'rhs' ordering, unless it has none, in which case # Ordering uses the 'rhs' ordering, unless it has none, in which case
# the current ordering is used. # the current ordering is used.

View File

@ -443,6 +443,31 @@ This is roughly equivalent to::
Note, however, that the first of these will raise ``IndexError`` while the Note, however, that the first of these will raise ``IndexError`` while the
second will raise ``DoesNotExist`` if no objects match the given criteria. second will raise ``DoesNotExist`` if no objects match the given criteria.
Combining QuerySets
-------------------
If you have two ``QuerySet`` instances that act on the same model, you can
combine them using ``&`` and ``|`` to get the items that are in both result
sets or in either results set, respectively. For example::
Entry.objects.filter(pubdate__gte=date1) & \
Entry.objects.filter(headline__startswith="What")
will combine the two queries into a single SQL query. Of course, in this case
you could have achieved the same result using multiple filters on the same
``QuerySet``, but sometimes the ability to combine individual ``QuerySet``
instance is useful.
Be careful, if you are using ``extra()`` to add custom handling to your
``QuerySet`` however. All the ``extra()`` components are merged and the result
may or may not make sense. If you are using custom SQL fragments in your
``extra()`` calls, Django will not inspect these fragments to see if they need
to be rewritten because of changes in the merged query. So test the effects
carefully. Also realise that if you are combining two ``QuerySets`` with
``|``, you cannot use ``extra(select=...)`` or ``extra(where=...)`` on *both*
``QuerySets``. You can only use those calls on one or the other (Django will
raise a ``ValueError`` if you try to use this incorrectly).
QuerySet methods that return new QuerySets QuerySet methods that return new QuerySets
------------------------------------------ ------------------------------------------

View File

@ -0,0 +1,55 @@
import copy
from django.db import models
from django.db.models.query import Q
class RevisionableModel(models.Model):
base = models.ForeignKey('self', null=True)
title = models.CharField(blank=True, max_length=255)
def __unicode__(self):
return u"%s (%s, %s)" % (self.title, self.id, self.base.id)
def save(self):
super(RevisionableModel, self).save()
if not self.base:
self.base = self
super(RevisionableModel, self).save()
def new_revision(self):
new_revision = copy.copy(self)
new_revision.pk = None
return new_revision
__test__ = {"API_TESTS": """
### Regression tests for #7314 and #7372
>>> rm = RevisionableModel.objects.create(title='First Revision')
>>> rm.pk, rm.base.pk
(1, 1)
>>> rm2 = rm.new_revision()
>>> rm2.title = "Second Revision"
>>> rm2.save()
>>> print u"%s of %s" % (rm2.title, rm2.base.title)
Second Revision of First Revision
>>> rm2.pk, rm2.base.pk
(2, 1)
Queryset to match most recent revision:
>>> qs = RevisionableModel.objects.extra(where=["%(table)s.id IN (SELECT MAX(rev.id) FROM %(table)s AS rev GROUP BY rev.base_id)" % {'table': RevisionableModel._meta.db_table,}],)
>>> qs
[<RevisionableModel: Second Revision (2, 1)>]
Queryset to search for string in title:
>>> qs2 = RevisionableModel.objects.filter(title__contains="Revision")
>>> qs2
[<RevisionableModel: First Revision (1, 1)>, <RevisionableModel: Second Revision (2, 1)>]
Following queryset should return the most recent revision:
>>> qs & qs2
[<RevisionableModel: Second Revision (2, 1)>]
"""}