#34433: OneToOneField can only be saved one way
-------------------------------------+-------------------------------------
               Reporter:  Alexis     |          Owner:  nobody
  Lesieur                            |
                   Type:  Bug        |         Status:  new
              Component:  Database   |        Version:  4.1
  layer (models, ORM)                |
               Severity:  Normal     |       Keywords:
           Triage Stage:             |      Has patch:  0
  Unreviewed                         |
    Needs documentation:  0          |    Needs tests:  0
Patch needs improvement:  0          |  Easy pickings:  0
                  UI/UX:  0          |
-------------------------------------+-------------------------------------
 Hi!
 I encountered this unexpected (to me) behavior for work, and I have been
 able to replicate on a bare django app (albeit with slightly different
 symptoms).

 The TLDR is that is model A has a OneToOneField to model B. The field had
 to be saved from the instance of model A, and that's not only not
 documented anywhere I could find, but counter-intuitive, and contradicts
 how other fields like ForeignKeys work.

 **Setup:**
 {{{
 ❯ python --version
 Python 3.11.2

 ❯ pip freeze | grep -i django
 Django==4.1.7

 ❯ django-admin startproject mysite

 ❯ cd mysyte/
 ❯ django-admin startapp myapp
 ❯ vim myapp/models.py
 # partially re-using your example from
 https://docs.djangoproject.com/en/4.1/topics/db/examples/one_to_one/
 ```
 from django.db import models

 class Place(models.Model):
     name = models.CharField(max_length=50)
     address = models.CharField(max_length=80)

     def __str__(self):
         return "%s the place" % self.name

 class Restaurant(models.Model):
     place = models.OneToOneField(
         Place,
         on_delete=models.CASCADE,
     )
     serves_hot_dogs = models.BooleanField(default=False)
     serves_pizza = models.BooleanField(default=False)

     def __str__(self):
         return "%s the restaurant" % self.place.name
 ```

 ❯ vim mysite/settings.py

 [...]
 INSTALLED_APPS = [
     'myapp.apps.MyappConfig',
 [...]

 ❯ python manage.py makemigrations
 ❯ python manage.py migrate
 }}}

 Creating the initial objects:
 {{{
 ❯ python manage.py shell
 ❯ from myapp.models import Place
 ❯ from myapp.models import Restaurant

 ❯ p1 = Place(name="1st place", address="1st address")
 ❯ p2 = Place(name="2nd place", address="2nd address")
 ❯ r1 = Restaurant(place=p1)
 ❯ r2 = Restaurant(place=p2)
 ❯ p1.save()
 ❯ p2.save()
 ❯ r1.save()
 ❯ r2.save()
 ❯ p3 = Place(name="3rd place", address="3rd address")
 ❯ p3.save()
 }}}

 This should give us a two restaurants with their respective places, and an
 additional place we can use to play.

 First, what works:
 {{{
 ❯ r1.place = p3
 ❯ r1.save()

 ❯ Restaurant.objects.get(id=1).place
 <Place: 3rd place the place>

 ❯ p3.restaurant
 <Restaurant: 3rd place the restaurant>

 ❯ Place.objects.get(id=1).restaurant
 [...]
 RelatedObjectDoesNotExist: Place has no restaurant.
 }}}
 This is all expected. `r1` now uses `p3`, which means that `p1` has no
 restaurant assigned.

 Now I would expect, to be able to do the other way. Assign a new
 restaurant to a place, save, and be all good.
 However that doesn't work.
 First using plain `.save()` which fails silently:
 {{{
 ❯ p1 = Place.objects.get(id=1)
 ❯ p1.restaurant = r1
 ❯ p1.save()

 ❯ Restaurant.objects.get(id=1).place
 <Place: 3rd place the place>  # this should be p1
 }}}

 And when explicitly asking to save the field:
 {{{
 ❯ p1.save(update_fields=["restaurant"])
 ❯ ValueError: The following fields do not exist in this model, are m2m
 fields, or are non-concrete fields: restaurant
 }}}

 NB: on my use case for work (django 3.2.18) I was also getting the
 following error:
 {{{
 UniqueViolation: duplicate key value violates unique constraint
 "response_timelineevent_pkey"
 DETAIL:  Key (id)=(91) already exists.
 }}}
 I'm not sure why it's different, but it doesn't work either way.

 This is problematic for a few reasons IMO:
 - Unless I missed it, the docs really don't advertise this limitation.
 - `.save()` "fails" silently, there is no way to know that the change
 didn't take, especially when this happens:
 {{{
 ❯ p1 = Place(name="1st place", address="1st address")
 ❯ p2 = Place(name="2nd place", address="2nd address")
 ❯ p3 = Place(name="3rd place", address="3rd address")
 ❯ p1.save()
 ❯ p2.save()
 ❯ p3.save()
 ❯ r1 = Restaurant(place=p1)
 ❯ r1.save()
 ❯ r2 = Restaurant(place=p2)
 ❯ r2.save()

 ❯ r1.place
 <Place: 1st place the place>
 ❯ p3.restaurant = r1
 ❯ r1.place
 <Place: 3rd place the place>
 ❯ p3.save()
 ❯ Restaurant.objects.get(id=1).place
 <Place: 1st place the place>
 }}}
 which leads to thinking the change is working and affecting both objects,
 when it's not.

 It's also problematic as foreigh keys work this way: (from my work
 example)
 {{{
 ❯ me = ExternalUser.objects.get(id=1)
 ❯ other = ExternalUser.objects.get(id=2)
 ❯ p = PinnedMessage.objects.get(id=11)

 ❯ p.author
 <ExternalUser: first.last (slack)>  # i.e. `me`

 ❯ [p.id for p in me.authored_pinnedmessage.all()]
 [1, 3, 5, 11]

 ❯ p.author = other
 ❯ p.save()

 ❯ [p.id for p in
 ExternalUser.objects.get(id=1).authored_pinnedmessage.all()]
 [1, 3, 5]

 ❯ me.authored_pinnedmessage.add(p)
 ❯ me.save()

 ❯ PinnedMessage.objects.get(id=11).author
 <ExternalUser: first.last (slack)>
 }}}


 Hopefully this is all enough explanation / details.
 Let me know if you need anything else from me!
 Thank you for your help.

-- 
Ticket URL: <https://code.djangoproject.com/ticket/34433>
Django <https://code.djangoproject.com/>
The Web framework for perfectionists with deadlines.

-- 
You received this message because you are subscribed to the Google Groups 
"Django updates" group.
To unsubscribe from this group and stop receiving emails from it, send an email 
to [email protected].
To view this discussion on the web visit 
https://groups.google.com/d/msgid/django-updates/010701870f4a244a-937560d8-a624-41c7-99e3-b340d76a25ce-000000%40eu-central-1.amazonses.com.

Reply via email to