diff --git a/django/db/models/query.py b/django/db/models/query.py index c0951d4091..07389388be 100644 --- a/django/db/models/query.py +++ b/django/db/models/query.py @@ -236,10 +236,11 @@ def parse_lookup(kwarg_items, opts): # is set to True, which means the kwarg was bad. # Example: choices.get_list(poll__exact='foo') throw_bad_kwarg_error(kwarg) - # Try many-to-many relationships first... + # Try many-to-many relationships in the direction in which they are + # originally defined (i.e., the class that defines the ManyToManyField) for f in current_opts.many_to_many: if f.name == current: - rel_table_alias = backend.quote_name(current_table_alias + LOOKUP_SEPARATOR + current) + rel_table_alias = backend.quote_name("m2m_" + current_table_alias + LOOKUP_SEPARATOR + current) joins[rel_table_alias] = ( backend.quote_name(f.get_m2m_db_table(current_opts)), @@ -275,6 +276,46 @@ def parse_lookup(kwarg_items, opts): param_required = True current_opts = f.rel.to._meta raise StopIteration + # Try many-to-many relationships first in the reverse direction + # (i.e., from the class does not have the ManyToManyField) + for f in current_opts.get_all_related_many_to_many_objects(): + if f.name == current: + rel_table_alias = backend.quote_name("m2m_" + current_table_alias + LOOKUP_SEPARATOR + current) + + joins[rel_table_alias] = ( + backend.quote_name(f.field.get_m2m_db_table(f.opts)), + "INNER JOIN", + '%s.%s = %s.%s' % + (backend.quote_name(current_table_alias), + backend.quote_name(current_opts.pk.column), + rel_table_alias, + backend.quote_name(current_opts.object_name.lower() + '_id')) + ) + + # Optimization: In the case of primary-key lookups, we + # don't have to do an extra join. + if lookup_list and lookup_list[0] == f.opts.pk.name and lookup_type == 'exact': + where.append(get_where_clause(lookup_type, rel_table_alias+'.', + f.opts.object_name.lower()+'_id', kwarg_value)) + params.extend(f.field.get_db_prep_lookup(lookup_type, kwarg_value)) + lookup_list.pop() + param_required = False + else: + new_table_alias = current_table_alias + LOOKUP_SEPARATOR + current + + joins[backend.quote_name(new_table_alias)] = ( + backend.quote_name(f.opts.db_table), + "INNER JOIN", + '%s.%s = %s.%s' % + (rel_table_alias, + backend.quote_name(f.opts.object_name.lower() + '_id'), + backend.quote_name(new_table_alias), + backend.quote_name(f.opts.pk.column)) + ) + current_table_alias = new_table_alias + param_required = True + current_opts = f.opts + raise StopIteration for f in current_opts.fields: # Try many-to-one relationships... if f.rel and f.name == current: diff --git a/tests/modeltests/many_to_many/models.py b/tests/modeltests/many_to_many/models.py index 6ca51f4376..825743a03c 100644 --- a/tests/modeltests/many_to_many/models.py +++ b/tests/modeltests/many_to_many/models.py @@ -28,6 +28,8 @@ API_TESTS = """ >>> p1.save() >>> p2 = Publication(id=None, title='Science News') >>> p2.save() +>>> p3 = Publication(id=None, title='Science Weekly') +>>> p3.save() # Create an Article. >>> a1 = Article(id=None, headline='Django lets you build Web apps easily') @@ -50,14 +52,14 @@ False True >>> a2.set_publications([p1.id]) True ->>> a2.set_publications([p1.id, p2.id]) +>>> a2.set_publications([p1.id, p2.id, p3.id]) True # Article objects have access to their related Publication objects. >>> a1.get_publication_list() [The Python Journal] >>> a2.get_publication_list() -[The Python Journal, Science News] +[The Python Journal, Science News, Science Weekly] # Publication objects have access to their related Article objects. >>> p2.get_article_list() @@ -65,10 +67,27 @@ True >>> p1.get_article_list(order_by=['headline']) [Django lets you build Web apps easily, NASA uses Python] +# We can perform kwarg queries across m2m relationships +>>> Article.objects.get_list(publications__pk=1) +[Django lets you build Web apps easily, NASA uses Python] + +>>> Article.objects.get_list(publications__title__startswith="Science") +[NASA uses Python, NASA uses Python] + +>>> Article.objects.get_list(publications__title__startswith="Science", distinct=True) +[NASA uses Python] + +# Reverse m2m queries (i.e., start at the table that doesn't have a ManyToManyField) +>>> Publication.objects.get_list(articles__headline__startswith="NASA") +[The Python Journal, Science News, Science Weekly] + +>>> Publication.objects.get_list(articles__pk=1) +[The Python Journal] + # If we delete a Publication, its Articles won't be able to access it. >>> p1.delete() >>> Publication.objects.get_list() -[Science News] +[Science News, Science Weekly] >>> a1 = Article.objects.get_object(pk=1) >>> a1.get_publication_list() [] diff --git a/tests/modeltests/one_to_one/models.py b/tests/modeltests/one_to_one/models.py index 32e9b3ea1c..a45d1a75f0 100644 --- a/tests/modeltests/one_to_one/models.py +++ b/tests/modeltests/one_to_one/models.py @@ -66,6 +66,8 @@ DoesNotExist: Restaurant does not exist for {'place__id__exact': ...} >>> Restaurant.objects.get_object(place__id__exact=1) Demon Dogs the restaurant +>>> Restaurant.objects.get_object(place__name__startswith="Demon") +Demon Dogs the restaurant >>> Restaurant.objects.get_object(pk=1) Demon Dogs the restaurant