Is it possible to assign a OneToOneField through the reverse model in Django?

Viewed 196
class Husband(models.Model):
    wife = models.OneToOneField(Wife, related_name='husband',blank=True, null=True,on_delete=models.CASCADE)
    name = models.CharField(max_length=200, blank=True)
    
Model Wife(models.Model):
    name = models.CharField(max_length=200, blank=True)

I have a post_save signal on Wife that requires the accessing of the corresponding Husband. This is triggered when I use the .create() method. However, I'm running into some issues assigning the Husband directly in the .create().

I tried the following:

Wife.objects.create(name='bla', husband = (some Husband instance))
w = Wife(name = 'bla')
w.husband = (some Husband instance)
w.save()
w = Wife(name = 'bla')
(some Husband instance).wife = w
w.save()

None of these end up storing the relationship into the database. Ultimately I want to have Wife model created with the relationship already set so the signal can draw the husband model object from it. I'm aware that one possible solution is to move the field to the Wife Model Class, but I was hoping there could be cleaner solution than that. any suggestions?

1 Answers

This is an old question and some of the comments seemed helpful, but nobody provided an actual answer with code so I thought I'd oblige.

Code

The following code will allow you to run Wife.objects.create(husband=h) just the same as if you were running Husband.objects.create(wife=w)

# models.py

from django.core.exceptions import ObjectDoesNotExist
from django.db import models
from django.db.models.signals import post_save
from django.dispatch import receiver


class Wife(models.Model):
    name = models.CharField(max_length=200, blank=True)

class Husband(models.Model):
    wife = models.OneToOneField(Wife, related_name='husband', blank=True, null=True, on_delete=models.SET_NULL)
    name = models.CharField(max_length=200, blank=True)


@receiver(post_save, sender=Wife)
def save_husband(sender, instance, created, **kwargs):
    """
    This receiver lets you run Wife.objects.create(husband=husband)
    
    When this function triggers, the Wife object ("instance") does have a valid husband attribute that corresponds to
    a real Husband object.

    However, without this receiver callback, this relationship has not been stored in the database. 
    To do that, we need to tell the Husband object who its Wife is.
    """
    try:
        husband = instance.husband
        husband.wife = instance
        husband.save()
    except ObjectDoesNotExist:
        pass # In case you run Wife.objects.create() without a Husband parameter

Bonus Code

If you're feeling real fancy, the following will make Wife and Husband even more equal. It will make unmarried_wife.husband return None instead of throwing a RelatedObjectDoesNotExist error. It will also let you run Wife creation with an integer pk like Wife.objects.create(husband_id=pk) instead of having to do Wife.objects.create(husband=Husband.objects.get(id=pk))

class Wife(models.Model):
    name = models.CharField(max_length=200, blank=True)

    # BONUS: Adding these properties lets you do other things on the Wife model that ordinarily you could only do on the Husband model
    @property
    def husband_id(self):
        """lets you run wife.husband_id and get an integer or None"""
        try:
            return self.husband_obj.id
        except ObjectDoesNotExist:
            return None

    @husband_id.setter
    def husband_id(self, pk):
        """lets you run Wife.objects.create(husband_id=pk) and have it work"""
        self.husband = Husband.objects.get(id=pk)

    @property
    def husband(self):
        """
        makes it so that w.husband returns 'None' instead of throwing an exception if w is an unmarried Wife,
        similar to how running h.wife return None if h is an unmarried Husband.
        """
        try:
            return self.husband_obj
        except ObjectDoesNotExist:
            return None

    @husband.setter
    def husband(self, obj):
        self.husband_obj = obj

class Husband(models.Model):
    # note that I've changed the related_name to 'husband_obj' so that I can assign 
    # the getter/setter properties in the Wife class
    wife = models.OneToOneField(Wife, related_name='husband_obj', blank=True, null=True, on_delete=models.SET_NULL)
    name = models.CharField(max_length=200, blank=True)

@receiver(post_save, sender=Wife)
def reverse_save_husband(sender, instance, created, **kwargs):
    """
    Same as before
    """
    try:
        husband = instance.husband
        husband.wife = instance
        husband.save()
    except (ObjectDoesNotExist, AttributeError):
        pass # In case you run Wife.objects.create() without a Husband parameter

Additional Note: I changed the on_delete behavior to SET_NULL because I don't believe in Widow Burning :) But that particular change is not necessary; the rest of the example will work fine even if you go with CASCADE.

Related