[1.7.x] Fixed #22650 -- Fixed regression on prefetch_related.
Regression fromf51c1f59
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 ofbdf3473e64
from master
This commit is contained in:
parent
0007a43198
commit
24a41ecc35
|
@ -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,6 +1807,12 @@ def prefetch_related_objects(result_cache, related_lookups):
|
||||||
continue
|
continue
|
||||||
if new_obj is None:
|
if new_obj is None:
|
||||||
continue
|
continue
|
||||||
|
# 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)
|
new_obj_list.append(new_obj)
|
||||||
obj_list = new_obj_list
|
obj_list = new_obj_list
|
||||||
|
|
||||||
|
|
|
@ -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']
|
||||||
|
|
||||||
|
|
|
@ -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):
|
||||||
|
|
Loading…
Reference in New Issue