Fixed #8046 -- The first filter() call on a related manager for many-to-many

fields no longer creates duplicate copies of the join table(s). Basically, this
means filters on the join table (for ManyToManyField(through=...)) and complex
filters in the normal (non-through) case don't produce incorrect or duplicate
results.


git-svn-id: http://code.djangoproject.com/svn/django/trunk@8472 bcc190cf-cafb-0310-a4f2-bffc1f526a37
This commit is contained in:
Malcolm Tredinnick 2008-08-22 22:00:28 +00:00
parent 6e36ce1429
commit d4d7bc175d
4 changed files with 80 additions and 43 deletions

View File

@ -376,7 +376,7 @@ def create_many_related_manager(superclass, through=False):
raise ValueError("%r instance needs to have a primary key value before a many-to-many relationship can be used." % instance.__class__.__name__) raise ValueError("%r instance needs to have a primary key value before a many-to-many relationship can be used." % instance.__class__.__name__)
def get_query_set(self): def get_query_set(self):
return superclass.get_query_set(self).filter(**(self.core_filters)) return superclass.get_query_set(self)._next_is_sticky().filter(**(self.core_filters))
# If the ManyToMany relation has an intermediary model, # If the ManyToMany relation has an intermediary model,
# the add and remove methods do not exist. # the add and remove methods do not exist.

View File

@ -121,6 +121,7 @@ class QuerySet(object):
self.query = query or sql.Query(self.model, connection) self.query = query or sql.Query(self.model, connection)
self._result_cache = None self._result_cache = None
self._iter = None self._iter = None
self._sticky_filter = False
######################## ########################
# PYTHON MAGIC METHODS # # PYTHON MAGIC METHODS #
@ -589,7 +590,10 @@ class QuerySet(object):
def _clone(self, klass=None, setup=False, **kwargs): def _clone(self, klass=None, setup=False, **kwargs):
if klass is None: if klass is None:
klass = self.__class__ klass = self.__class__
c = klass(model=self.model, query=self.query.clone()) query = self.query.clone()
if self._sticky_filter:
query.filter_is_sticky = True
c = klass(model=self.model, query=query)
c.__dict__.update(kwargs) c.__dict__.update(kwargs)
if setup and hasattr(c, '_setup_query'): if setup and hasattr(c, '_setup_query'):
c._setup_query() c._setup_query()
@ -607,6 +611,17 @@ class QuerySet(object):
except StopIteration: except StopIteration:
self._iter = None self._iter = None
def _next_is_sticky(self):
"""
Indicates that the next filter call and the one following that should
be treated as a single filter. This is only important when it comes to
determining when to reuse tables for many-to-many filters. Required so
that we can filter naturally on the results of related managers.
"""
obj = self._clone()
obj._sticky_filter = True
return obj
def _merge_sanity_check(self, other): def _merge_sanity_check(self, other):
""" """
Checks that we are merging two comparable QuerySet classes. By default Checks that we are merging two comparable QuerySet classes. By default

View File

@ -58,6 +58,8 @@ class Query(object):
self.select_fields = [] self.select_fields = []
self.related_select_fields = [] self.related_select_fields = []
self.dupe_avoidance = {} self.dupe_avoidance = {}
self.used_aliases = set()
self.filter_is_sticky = False
# SQL-related attributes # SQL-related attributes
self.select = [] self.select = []
@ -78,7 +80,7 @@ class Query(object):
# These are for extensions. The contents are more or less appended # These are for extensions. The contents are more or less appended
# verbatim to the appropriate clause. # verbatim to the appropriate clause.
self.extra_select = SortedDict() # Maps col_alias -> col_sql. self.extra_select = SortedDict() # Maps col_alias -> (col_sql, params).
self.extra_tables = () self.extra_tables = ()
self.extra_where = () self.extra_where = ()
self.extra_params = () self.extra_params = ()
@ -185,6 +187,11 @@ class Query(object):
obj.extra_where = self.extra_where obj.extra_where = self.extra_where
obj.extra_params = self.extra_params obj.extra_params = self.extra_params
obj.extra_order_by = self.extra_order_by obj.extra_order_by = self.extra_order_by
if self.filter_is_sticky and self.used_aliases:
obj.used_aliases = self.used_aliases.copy()
else:
obj.used_aliases = set()
obj.filter_is_sticky = False
obj.__dict__.update(kwargs) obj.__dict__.update(kwargs)
if hasattr(obj, '_setup_query'): if hasattr(obj, '_setup_query'):
obj._setup_query() obj._setup_query()
@ -1148,12 +1155,11 @@ class Query(object):
Can also be used to add anything that has an 'add_to_query()' method. Can also be used to add anything that has an 'add_to_query()' method.
""" """
if used_aliases is None: if used_aliases is None:
used_aliases = set() used_aliases = self.used_aliases
if hasattr(q_object, 'add_to_query'): if hasattr(q_object, 'add_to_query'):
# Complex custom objects are responsible for adding themselves. # Complex custom objects are responsible for adding themselves.
q_object.add_to_query(self, used_aliases) q_object.add_to_query(self, used_aliases)
return else:
if self.where and q_object.connector != AND and len(q_object) > 1: if self.where and q_object.connector != AND and len(q_object) > 1:
self.where.start_subtree(AND) self.where.start_subtree(AND)
subtree = True subtree = True
@ -1173,6 +1179,8 @@ class Query(object):
self.where.negate() self.where.negate()
if subtree: if subtree:
self.where.end_subtree() self.where.end_subtree()
if self.filter_is_sticky:
self.used_aliases = used_aliases
def setup_joins(self, names, opts, alias, dupe_multis, allow_many=True, def setup_joins(self, names, opts, alias, dupe_multis, allow_many=True,
allow_explicit_fk=False, can_reuse=None): allow_explicit_fk=False, can_reuse=None):

View File

@ -158,4 +158,18 @@ AttributeError: Cannot use create() on a ManyToManyField which specifies an inte
</object> </object>
</django-objects> </django-objects>
## Regression test for #8046:
Check that we don't involve too many copies of the intermediate table when
doing a join.
>>> bob = Person.objects.create(name='Bob')
>>> jim = Person.objects.create(name='Jim')
>>> rock = Group.objects.create(name='Rock')
>>> roll = Group.objects.create(name='Roll')
>>> _ = Membership.objects.create(person=bob, group=rock)
>>> _ = Membership.objects.create(person=jim, group=rock, price=50)
>>> _ = Membership.objects.create(person=bob, group=roll, price=50)
>>> rock.members.filter(membership__price=50)
[<Person: Jim>]
"""} """}