summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSamriddha9619 <sumitkumartripathi0@gmail.com>2026-01-05 11:18:40 +0530
committerJacob Walls <jacobtylerwalls@gmail.com>2026-01-13 13:18:14 -0500
commit040bb3eba72eb45020dd025d3f83094a0fcaf22f (patch)
tree8641fd38c6d952ece408fb047017c4124b311edc
parent73c5e94521c5b97e27cd2fe2d5b5c2e65f402755 (diff)
Fixed #35442 -- Prevented N+1 queries in RelatedManager with only().
Co-authored-by: Simon Charette <charette.s@gmail.com>
-rw-r--r--django/db/models/query.py25
-rw-r--r--tests/defer/tests.py15
2 files changed, 26 insertions, 14 deletions
diff --git a/django/db/models/query.py b/django/db/models/query.py
index 73d6717bce..5649a83428 100644
--- a/django/db/models/query.py
+++ b/django/db/models/query.py
@@ -114,16 +114,15 @@ class ModelIterable(BaseIterable):
(
field,
related_objs,
- operator.attrgetter(
- *[
- (
- field.attname
- if from_field == "self"
- else queryset.model._meta.get_field(from_field).attname
- )
- for from_field in field.from_fields
- ]
- ),
+ attnames := [
+ (
+ field.attname
+ if from_field == "self"
+ else queryset.model._meta.get_field(from_field).attname
+ )
+ for from_field in field.from_fields
+ ],
+ operator.attrgetter(*attnames),
)
for field, related_objs in queryset._known_related_objects.items()
]
@@ -145,10 +144,14 @@ class ModelIterable(BaseIterable):
setattr(obj, attr_name, row[col_pos])
# Add the known related objects to the model.
- for field, rel_objs, rel_getter in known_related_objects:
+ for field, rel_objs, rel_attnames, rel_getter in known_related_objects:
# Avoid overwriting objects loaded by, e.g., select_related().
if field.is_cached(obj):
continue
+ # Avoid fetching potentially deferred attributes that would
+ # result in unexpected queries.
+ if any(attname not in obj.__dict__ for attname in rel_attnames):
+ continue
rel_obj_id = rel_getter(obj)
try:
rel_obj = rel_objs[rel_obj_id]
diff --git a/tests/defer/tests.py b/tests/defer/tests.py
index 29c63c566a..1a5f333fa1 100644
--- a/tests/defer/tests.py
+++ b/tests/defer/tests.py
@@ -49,9 +49,9 @@ class DeferTests(AssertionMixin, TestCase):
# of them except the model's primary key see #15494
self.assert_delayed(qs.only("pk")[0], 3)
# You can use 'pk' with reverse foreign key lookups.
- # The related_id is always set even if it's not fetched from the DB,
- # so pk and related_id are not deferred.
- self.assert_delayed(self.s1.primary_set.only("pk")[0], 2)
+ # The related_id is not set if it's not fetched from the DB,
+ # so pk is not deferred, but related_id is.
+ self.assert_delayed(self.s1.primary_set.only("pk")[0], 3)
def test_defer_only_chaining(self):
qs = Primary.objects.all()
@@ -84,6 +84,15 @@ class DeferTests(AssertionMixin, TestCase):
with self.assertRaisesMessage(TypeError, msg):
Primary.objects.only(None)
+ def test_only_related_manager_optimization(self):
+ s = Secondary.objects.create(first="one", second="two")
+ Primary.objects.bulk_create(
+ [Primary(name="p1", value="v1", related=s) for _ in range(5)]
+ )
+ with self.assertNumQueries(1):
+ for p in s.primary_set.only("pk"):
+ _ = p.pk
+
def test_defer_extra(self):
qs = Primary.objects.all()
self.assert_delayed(qs.defer("name").extra(select={"a": 1})[0], 1)