#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.