Browse files

queryset-refactor: Implemented a way to differentiate between filteri…

…ng on a

single instance and filtering on multiple instances when spanning a
multi-valued relationship.

git-svn-id: bcc190cf-cafb-0310-a4f2-bffc1f526a37
  • Loading branch information...
1 parent c085697 commit d20996b58dfde66d80d6728a70480c7e5caf7d48 @malcolmt malcolmt committed Mar 19, 2008
Showing with 83 additions and 13 deletions.
  1. +8 −9 django/db/models/sql/
  2. +60 −2 docs/db-api.txt
  3. +15 −2 tests/regressiontests/queries/
@@ -809,7 +809,7 @@ def fill_related_selections(self, opts=None, root_alias=None, cur_depth=1,
used, next, restricted)
def add_filter(self, filter_expr, connector=AND, negate=False, trim=False,
- merge_negated=False):
+ single_filter=False):
Add a single filter to the query. The 'filter_expr' is a pair:
(filter_string, value). E.g. ('name__contains', 'fred')
@@ -818,9 +818,8 @@ def add_filter(self, filter_expr, connector=AND, negate=False, trim=False,
automatically trim the final join group (used internally when
constructing nested queries).
- If 'merge_negated' is True, this negated filter will be merged with the
- existing negated where node (if it exists). This is used when
- constructing an exclude filter from combined subfilters.
+ If 'single_filter' is True, we are processing a component of a
+ multi-component filter (e.g. filter(Q1, Q2)).
arg, value = filter_expr
parts = arg.split(LOOKUP_SEP)
@@ -849,7 +848,7 @@ def add_filter(self, filter_expr, connector=AND, negate=False, trim=False,
field, target, opts, join_list, last = self.setup_joins(parts, opts,
- alias, (connector == AND), allow_many)
+ alias, (connector == AND) and not single_filter, allow_many)
except MultiJoin, e:
self.split_exclude(filter_expr, LOOKUP_SEP.join(parts[:e.level]))
@@ -917,7 +916,7 @@ def add_filter(self, filter_expr, connector=AND, negate=False, trim=False,
entry = (alias, col, field, lookup_type, value)
- if merge_negated:
+ if negate and single_filter:
# This case is when we're doing the Q2 filter in exclude(Q1, Q2).
# It's different from exclude(Q1).exclude(Q2).
for node in self.where.children:
@@ -957,16 +956,16 @@ def add_q(self, q_object):
subtree = False
connector = AND
- merge = False
+ internal = False
for child in q_object.children:
if isinstance(child, Node):
self.add_filter(child, connector, q_object.negated,
- merge_negated=merge)
- merge = q_object.negated
+ single_filter=internal)
+ internal = True
connector = q_object.connector
if subtree:
@@ -1559,7 +1559,7 @@ equivalent::
model's primary key in queries.
Lookups that span relationships
Django offers a powerful and intuitive way to "follow" relationships in
lookups, taking care of the SQL ``JOIN``\s for you automatically, behind the
@@ -1582,8 +1582,66 @@ whose ``headline`` contains ``'Lennon'``::
+Spanning multi-valued relationships
+**New in Django development version**
+.. note::
+ This is an experimental API and subject to change prior to
+ queryset-refactor being merged into trunk.
+When you are filtering an object based on a ``ManyToManyField`` or a reverse
+``ForeignKeyField``, there are two different sorts of filter you may be
+interested in. Consider the ``Blog``/``Entry`` relationship (``Blog`` to
+``Entry`` is a one-to-many relation). We might be interested in finding blogs
+that have an entry which has both *"Lennon"* in the headline and was published
+today. Or we might want to find blogs that have an entry with *"Lennon"* in
+the headline as well as an entry that was published today. Since there are
+multiple entries associated with a single ``Blog``, both of these queries are
+possible and make sense in some situations.
+The same type of situation arises with a ``ManyToManyField``. For example, if
+an ``Entry`` has a ``ManyToManyField`` called ``tags``, we might want to find
+entries linked to tags called *"music"* and *"bands"* or we might want an
+entry that contains a tag with a name of *"music"* and a status of *"public"*.
+To handle both of these situations, Django has a consistent way of processing
+``filter()`` and ``exclude()`` calls. Everything inside a single ``filter()``
+call is applied simultaneously to filter out items matching all those
+requirements. Successive ``filter()`` calls further restrict the set of
+objects, but for multi-valued relations, they apply to any object linked to
+the primary model, not necessarily those objects that were selected by an
+earlier ``filter()`` call.
+That may sound a bit confusing, so hopefully an example will clarify. To
+select all blogs that contains entries with *"Lennon"* in the headline and
+were published today, we would write::
+ Blog.objects.filter(entry__headline__contains='Lennon',
+To select all blogs that contain an entry with *"Lennon"* in the headline
+**as well as** an entry that was published today, we would write::
+ Blog.objects.filter(entry__headline__contains='Lennon').filter(
+In this second example, the first filter restricted the queryset to all those
+blogs linked to that particular type of entry. The second filter restricted
+the set of blogs *further* to those that are also linked to the second type of
+entry. The entries select by the second filter may or may not be the same as
+the entries in the first filter. We are filtering the ``Blog`` items with each
+filter statement, not the ``Entry`` items.
+All of this behaviour also applies to ``exclude()``: all the conditions in a
+single ``exclude()`` statement apply to a single instance (if those conditions
+are talking about the same multi-valued relation). Conditions in subsequent
+``filter()`` or ``exclude()`` calls that refer to the same relation may end up
+filtering on different linked objects.
Escaping percent signs and underscores in LIKE statements
The field lookups that equate to ``LIKE`` SQL statements (``iexact``,
``contains``, ``icontains``, ``startswith``, ``istartswith``, ``endswith``
@@ -210,11 +210,24 @@ class Meta:
>>> Item.objects.filter(Q(tags=t1)).order_by('name')
[<Item: one>, <Item: two>]
->>> Item.objects.filter(Q(tags=t1) & Q(tags=t2))
-[<Item: one>]
>>> Item.objects.filter(Q(tags=t1)).filter(Q(tags=t2))
[<Item: one>]
+Each filter call is processed "at once" against a single table, so this is
+different from the previous example as it tries to find tags that are two
+things at once (rather than two tags).
+>>> Item.objects.filter(Q(tags=t1) & Q(tags=t2))
+>>> qs = Author.objects.filter(ranking__rank=2,
+>>> list(qs)
+[<Author: a2>]
+>>> qs.query.count_active_tables()
+>>> qs = Author.objects.filter(ranking__rank=2).filter(
+>>> qs.query.count_active_tables()
Bug #4464
>>> Item.objects.filter(tags=t1).filter(tags=t2)
[<Item: one>]

0 comments on commit d20996b

Please sign in to comment.