diff options
| author | Samriddha9619 <sumitkumartripathi0@gmail.com> | 2026-01-05 11:18:40 +0530 |
|---|---|---|
| committer | Jacob Walls <jacobtylerwalls@gmail.com> | 2026-01-13 13:18:14 -0500 |
| commit | 040bb3eba72eb45020dd025d3f83094a0fcaf22f (patch) | |
| tree | 8641fd38c6d952ece408fb047017c4124b311edc | |
| parent | 73c5e94521c5b97e27cd2fe2d5b5c2e65f402755 (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.py | 25 | ||||
| -rw-r--r-- | tests/defer/tests.py | 15 |
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) |
