[1.7.x] Fixed #22650 -- Fixed regression on prefetch_related.

Regression from f51c1f59 when using select_related then prefetch_related
on the reverse side of an O2O:

Author.objects.select_related('bio').prefetch_related('bio__books')

Thanks Aymeric Augustin for the report and tests. Refs #17001.

Backport of bdf3473e64 from master
This commit is contained in:
Loic Bistuer 2014-05-19 03:43:13 +07:00
parent 0007a43198
commit 24a41ecc35
3 changed files with 77 additions and 11 deletions

View File

@ -1791,15 +1791,6 @@ def prefetch_related_objects(result_cache, related_lookups):
done_queries[prefetch_to] = obj_list done_queries[prefetch_to] = obj_list
auto_lookups.extend(normalize_prefetch_lookups(additional_lookups, prefetch_to)) auto_lookups.extend(normalize_prefetch_lookups(additional_lookups, prefetch_to))
followed_descriptors.add(descriptor) followed_descriptors.add(descriptor)
elif isinstance(getattr(first_obj, through_attr), list):
# The current part of the lookup relates to a custom Prefetch.
# This means that obj.attr is a list of related objects, and
# thus we must turn the obj.attr lists into a single related
# object list.
new_list = []
for obj in obj_list:
new_list.extend(getattr(obj, through_attr))
obj_list = new_list
else: else:
# Either a singly related object that has already been fetched # Either a singly related object that has already been fetched
# (e.g. via select_related), or hopefully some other property # (e.g. via select_related), or hopefully some other property
@ -1816,7 +1807,13 @@ def prefetch_related_objects(result_cache, related_lookups):
continue continue
if new_obj is None: if new_obj is None:
continue continue
new_obj_list.append(new_obj) # We special-case `list` rather than something more generic
# like `Iterable` because we don't want to accidentally match
# user models that define __iter__.
if isinstance(new_obj, list):
new_obj_list.extend(new_obj)
else:
new_obj_list.append(new_obj)
obj_list = new_obj_list obj_list = new_obj_list

View File

@ -66,6 +66,11 @@ class BookWithYear(Book):
AuthorWithAge, related_name='books_with_year') AuthorWithAge, related_name='books_with_year')
class Bio(models.Model):
author = models.OneToOneField(Author)
books = models.ManyToManyField(Book, blank=True)
@python_2_unicode_compatible @python_2_unicode_compatible
class Reader(models.Model): class Reader(models.Model):
name = models.CharField(max_length=50) name = models.CharField(max_length=50)
@ -196,6 +201,10 @@ class Person(models.Model):
# Assume business logic forces every person to have at least one house. # Assume business logic forces every person to have at least one house.
return sorted(self.houses.all(), key=lambda house: -house.rooms.count())[0] return sorted(self.houses.all(), key=lambda house: -house.rooms.count())[0]
@property
def all_houses(self):
return list(self.houses.all())
class Meta: class Meta:
ordering = ['id'] ordering = ['id']

View File

@ -9,7 +9,7 @@ from django.test import TestCase, override_settings
from django.utils import six from django.utils import six
from django.utils.encoding import force_text from django.utils.encoding import force_text
from .models import (Author, Book, Reader, Qualification, Teacher, Department, from .models import (Author, Bio, Book, Reader, Qualification, Teacher, Department,
TaggedItem, Bookmark, AuthorAddress, FavoriteAuthors, AuthorWithAge, TaggedItem, Bookmark, AuthorAddress, FavoriteAuthors, AuthorWithAge,
BookWithYear, BookReview, Person, House, Room, Employee, Comment, BookWithYear, BookReview, Person, House, Room, Employee, Comment,
LessonEntry, WordEntry, Author2) LessonEntry, WordEntry, Author2)
@ -192,6 +192,20 @@ class PrefetchRelatedTests(TestCase):
["Amy"], ["Amy"],
["Amy", "Belinda"]]) ["Amy", "Belinda"]])
def test_reverse_one_to_one_then_m2m(self):
"""
Test that we can follow a m2m relation after going through
the select_related reverse of a o2o.
"""
qs = Author.objects.prefetch_related('bio__books').select_related('bio')
with self.assertNumQueries(1):
list(qs.all())
Bio.objects.create(author=self.author1)
with self.assertNumQueries(2):
list(qs.all())
def test_attribute_error(self): def test_attribute_error(self):
qs = Reader.objects.all().prefetch_related('books_read__xyz') qs = Reader.objects.all().prefetch_related('books_read__xyz')
with self.assertRaises(AttributeError) as cm: with self.assertRaises(AttributeError) as cm:
@ -452,6 +466,52 @@ class CustomPrefetchTests(TestCase):
) )
self.assertEqual(lst1, lst2) self.assertEqual(lst1, lst2)
def test_traverse_single_item_property(self):
# Control lookups.
with self.assertNumQueries(5):
lst1 = self.traverse_qs(
Person.objects.prefetch_related(
'houses__rooms',
'primary_house__occupants__houses',
),
[['primary_house', 'occupants', 'houses']]
)
# Test lookups.
with self.assertNumQueries(5):
lst2 = self.traverse_qs(
Person.objects.prefetch_related(
'houses__rooms',
Prefetch('primary_house__occupants', to_attr='occupants_lst'),
'primary_house__occupants_lst__houses',
),
[['primary_house', 'occupants_lst', 'houses']]
)
self.assertEqual(lst1, lst2)
def test_traverse_multiple_items_property(self):
# Control lookups.
with self.assertNumQueries(4):
lst1 = self.traverse_qs(
Person.objects.prefetch_related(
'houses',
'all_houses__occupants__houses',
),
[['all_houses', 'occupants', 'houses']]
)
# Test lookups.
with self.assertNumQueries(4):
lst2 = self.traverse_qs(
Person.objects.prefetch_related(
'houses',
Prefetch('all_houses__occupants', to_attr='occupants_lst'),
'all_houses__occupants_lst__houses',
),
[['all_houses', 'occupants_lst', 'houses']]
)
self.assertEqual(lst1, lst2)
def test_custom_qs(self): def test_custom_qs(self):
# Test basic. # Test basic.
with self.assertNumQueries(2): with self.assertNumQueries(2):