Opened 4 weeks ago

Closed 10 days ago

Last modified 10 days ago

#35356 closed Bug (fixed)

Issue with OneToOneField and recursive relationships in select_related() and only().

Reported by: Joshua van Besouw Owned by: Simon Charette
Component: Database layer (models, ORM) Version: 4.2
Severity: Normal Keywords:
Cc: Joshua van Besouw Triage Stage: Ready for checkin
Has patch: yes Needs documentation: no
Needs tests: no Patch needs improvement: no
Easy pickings: no UI/UX: no

Description

In Django a 4.2 project, if you have a model with a recursive relationship OneToOneField like this:

class Example(models.Model):
    name = models.CharField(max_length=32)
    source = models.OneToOneField(
        'self', related_name='destination', on_delete=models.CASCADE
    )

And then query this model using:

Example.objects.select_related(
    "source",
    "destination",
).only(
    "name",
    "source__name",
    "destination__name",
).all()

It throws the following error:

django.core.exceptions.FieldError: Field Example.source cannot be both deferred and traversed using select_related at the same time.

Expected behavior:

The queryset should apply the select_related() and only() without an exception occurring as the only() is specifying sub fields of the fields in the select_related(). Or at least this is how it used to behave.

Interestingly, if you change the queryset to the following, it works without any issues:

Example.objects.select_related("source").only("name", "source__name").all()

And vice versa also works:

Example.objects.select_related("destination").only("name", "destination__name").all()

Effected versions:

This error occurs in version 4.2+ of Django.

This worked as expected in all versions of Django 4.1.

As far as I can tell, this is a regression as a brief search doesn't indicate this functionality was explicitly changed at any point. I have not tested whether this is only an issue with recursive relationships or a general issues with reverse relationships.

Change History (8)

comment:1 by Simon Charette, 4 weeks ago

Likely related to #21204 (b3db6c8dcb5145f7d45eff517bcd96460475c879) which was merged in 4.2.

comment:2 by Simon Charette, 4 weeks ago

Triage Stage: UnreviewedAccepted
  • tests/defer_regress/models.py

    diff --git a/tests/defer_regress/models.py b/tests/defer_regress/models.py
    index dd492993b7..38ba4a622f 100644
    a b class Item(models.Model):  
    1010    text = models.TextField(default="xyzzy")
    1111    value = models.IntegerField()
    1212    other_value = models.IntegerField(default=0)
     13    source = models.OneToOneField(
     14        "self",
     15        related_name="destination",
     16        on_delete=models.CASCADE,
     17        null=True,
     18    )
    1319
    1420
    1521class RelatedItem(models.Model):
  • tests/defer_regress/tests.py

    diff --git a/tests/defer_regress/tests.py b/tests/defer_regress/tests.py
    index 10100e348d..98473e0c4f 100644
    a b def test_only_reverse_many_to_many_ignored(self):  
    309309        with self.assertNumQueries(1):
    310310            self.assertEqual(Item.objects.only("request").get(), item)
    311311
     312    def test_self_referential(self):
     313        first = Item.objects.create(name="first", value=1)
     314        second = Item.objects.create(name="second", value=2, source=first)
     315        with self.assertNumQueries(1):
     316            deferred_first, deferred_second = (
     317                Item.objects.select_related("source", "destination")
     318                .only("name", "source__name", "destination__value")
     319                .order_by("pk")
     320            )
     321        with self.assertNumQueries(0):
     322            self.assertEqual(deferred_first.name, first.name)
     323            self.assertEqual(deferred_second.name, second.name)
     324            self.assertEqual(deferred_second.source.name, first.name)
     325            self.assertEqual(deferred_first.destination.value, second.value)
     326
    312327
    313328class DeferDeletionSignalsTests(TestCase):
    314329    senders = [Item, Proxy]
Last edited 4 weeks ago by Simon Charette (previous) (diff)

comment:3 by Simon Charette, 4 weeks ago

#34612 could have fixed it but it took a different approach.

comment:4 by Simon Charette, 4 weeks ago

Has patch: set

comment:5 by Joshua van Besouw, 4 weeks ago

Thanks for the quick patch on this!

I can confirm, form my end, that this patch fixes the issue I was encountering. The FieldError was not raised and the following SQL was used in the resulting query:

SELECT
    "test_project_example"."id",
    "test_project_example"."name",
    "test_project_example"."source_id",
    T2."id",
    T2."name",
    T3."id",
    T3."name"
FROM
    "test_project_example"
    LEFT OUTER JOIN "test_project_example" T2 ON (
        "test_project_example"."source_id" = T2."id"
    )
    LEFT OUTER JOIN "test_project_example" T3 ON (
        "test_project_example"."id" = T3."source_id"
    )
ORDER BY
    "test_project_example"."id" ASC

comment:6 by Mariusz Felisiak, 3 weeks ago

Owner: changed from nobody to Simon Charette
Status: newassigned
Triage Stage: AcceptedReady for checkin

comment:7 by nessita <124304+nessita@…>, 10 days ago

Resolution: fixed
Status: assignedclosed

In 83f54782:

Fixed #35356 -- Deferred self-referential foreign key fields adequately.

While refs #34612 surfaced issues with reverse one-to-one fields
deferrals, it missed that switching to storing remote fields would break
self-referential relationships.

This change switches to storing related objects in the select mask
instead of remote fields to prevent collisions when dealing with
self-referential relationships that might have a different directional
mask.

Despite fixing #21204 introduced a crash under some self-referential
deferral conditions, it was simply not working even before that as it
aggregated the sets of deferred fields by model.

Thanks Joshua van Besouw for the report and Mariusz Felisiak for the
review.

comment:8 by nessita <124304+nessita@…>, 10 days ago

In 195d885:

Refs #35356 -- Clarified select related with masked field logic.

By always including related objects in the select mask via adjusting the
defer logic (_get_defer_select_mask()), it becomes possible for
select_related_descend() to treat forward and reverse relationships
indistinctively.

This work also simplifies and adds comments to
select_related_descend() to make it easier to understand.

Note: See TracTickets for help on using tickets.
Back to Top