From d20996b58dfde66d80d6728a70480c7e5caf7d48 Mon Sep 17 00:00:00 2001 From: Malcolm Tredinnick Date: Wed, 19 Mar 2008 11:02:22 +0000 Subject: [PATCH] queryset-refactor: Implemented a way to differentiate between filtering on a single instance and filtering on multiple instances when spanning a multi-valued relationship. git-svn-id: http://code.djangoproject.com/svn/django/branches/queryset-refactor@7317 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- django/db/models/sql/query.py | 17 ++++--- docs/db-api.txt | 62 ++++++++++++++++++++++++- tests/regressiontests/queries/models.py | 17 ++++++- 3 files changed, 83 insertions(+), 13 deletions(-) diff --git a/django/db/models/sql/query.py b/django/db/models/sql/query.py index 62a5670af8..c690117e16 100644 --- a/django/db/models/sql/query.py +++ b/django/db/models/sql/query.py @@ -809,7 +809,7 @@ class Query(object): 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 @@ class Query(object): 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 @@ class Query(object): try: 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])) return @@ -917,7 +916,7 @@ class Query(object): self.promote_alias(table) 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,7 +956,7 @@ class Query(object): else: subtree = False connector = AND - merge = False + internal = False for child in q_object.children: if isinstance(child, Node): self.where.start_subtree(connector) @@ -965,8 +964,8 @@ class Query(object): self.where.end_subtree() else: 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: self.where.end_subtree() diff --git a/docs/db-api.txt b/docs/db-api.txt index e15b0b2176..076f406aa0 100644 --- a/docs/db-api.txt +++ b/docs/db-api.txt @@ -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'``:: Blog.objects.filter(entry__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', + entry__pub_date=datetime.date.today()) + +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( + entry__pub_date=datetime.date.today()) + +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`` diff --git a/tests/regressiontests/queries/models.py b/tests/regressiontests/queries/models.py index 08f55f4640..2db5bf8a34 100644 --- a/tests/regressiontests/queries/models.py +++ b/tests/regressiontests/queries/models.py @@ -210,11 +210,24 @@ True >>> Item.objects.filter(Q(tags=t1)).order_by('name') [, ] ->>> Item.objects.filter(Q(tags=t1) & Q(tags=t2)) -[] >>> Item.objects.filter(Q(tags=t1)).filter(Q(tags=t2)) [] +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, ranking__id=rank1.id) +>>> list(qs) +[] +>>> qs.query.count_active_tables() +2 +>>> qs = Author.objects.filter(ranking__rank=2).filter(ranking__id=rank1.id) +>>> qs.query.count_active_tables() +3 + Bug #4464 >>> Item.objects.filter(tags=t1).filter(tags=t2) []