Django OneToOneFields are Hard to Use, Let’s Make Them Better

rocky

Posted by rocky

django

(All code samples are the under the Simplified BSD License)

Django’s OneToOneField is a useful tool, I like it especially for creating UserProfiles, but it’s useful in many ways.

However, it is not the easiest thing to use. In this blog post, I will explore some of the things I’ve done in the past to overcome some of the rough edges that OneToOneFields have.

  1. The Django OneToOneField raises exceptions if the related model doesn’t exist.
  2. In order to check whether or not the related model exists, you have to usehasattr.

Always raising exceptions

One problem is that when you access a related model that doesn’t exist yet, it will throw a ObjectDoesNotExist exception. Which makes sense, but is kind of hard to use.

There are a couple of solutions to this problem:

  1. Make sure that the related model always exists, (perhaps using the post_savesignal).

  2. Work around it with a property, something like this:

class User(AbstractUser):
    # ...

    @property
    def customer_profile(self):
        try:
            return self._customer_profile
        except CustomerProfile.DoesNotExist:
            return CustomerProfile.objects.create(
                user=self,
            )


class CustomerProfile(models.Model):
    user = models.OneToOneField('User', related_name='_customer_profile')

 

Both of these solutions work, but the first one feels hacky to me--what happens if I bulk_create some Users (post_save isn’t fired)? And as for the second one, requires that I have control over the User model. I would prefer a solution that does not require me to override the User model.

A possible solution

So I was thinking, why don’t we just create a OneToOneField that automatically creates the profile if it doesn’t exist? That way we won’t get exceptions. Plus! The profile is created automatically for you.

Turns out it’s not too hard. And bonus! It uses my favorite feature from Python, object descriptors!

from django.db import IntegrityError
from django.db.models.fields.related import (
    OneToOneField, SingleRelatedObjectDescriptor,
)


class AutoSingleRelatedObjectDescriptor(SingleRelatedObjectDescriptor):
    def __get__(self, instance, type=None):
        try:
            return super(AutoSingleRelatedObjectDescriptor, self).__get__(instance, type)
        except self.related.model.DoesNotExist:
            kwargs = {
                self.related.field.name: instance,
            }
            rel_obj = self.related.model._default_manager.create(**kwargs)
            setattr(instance, self.cache_name, rel_obj)
            return rel_obj


class AutoOneToOneField(OneToOneField):
    related_accessor_class = AutoSingleRelatedObjectDescriptor

 

With AutoOneToOneField, when the related model doesn’t exist, it will be created automatically:

Let’s look back at our previous example,

class User(AbstractBaseUser):
    # ...

    @property
    def customer_profile(self):
        try:
            return self._customer_profile
        except CustomerProfile.DoesNotExist:
            return CustomerProfile.objects.create(
                user=self,
            )


class CustomerProfile(models.Model):
    user = models.OneToOneField('User', related_name='_customer_profile')

becomes:

class CustomerProfile(models.Model):
    user = AutoOneToOneField('User', related_name='customer_profile')

And now when you try to access it, there are no errors thrown:

user = User.objects.all()[0]
user.customer_profile  # Always returns a customer profile

 

Caveat

Unfortunately, this is not the most appropriate solution for all situations. One assumption that it makes is that you can create the related model by just filling in one field. This of course won’t work if your related model has fields on it that are null=False and don’t provide a default.

However, it works well for those instances when you always want a profile model available for a user and can provide defaults for all of the fields.

Alternate solution

What if instead of creating the related model, the OneToOneField just returned None instead? That would be easier to deal with in your code and also wouldn’t make assumptions on how to create related models.

class SoftSingleRelatedObjectDescriptor(SingleRelatedObjectDescriptor):
    def __get__(self, *args, **kwargs):
        try:
            return super(SoftSingleRelatedObjectDescriptor, self).__get__(*args, **kwargs)
        except self.related.model.DoesNotExist:
            return None


class SoftOneToOneField(OneToOneField):
    related_accessor_class = SoftSingleRelatedObjectDescriptor

 

(Yay for more Python object descriptors!)

This makes accessing the related model not throw an exception any more, but your code now should expect a None value instead.

class CustomerProfile(models.Model):
    user = SoftOneToOneField('User', related_name='customer_profile')

class MyProfileView(DetailView):
    def get_context_data(self, **kwargs):
        kwargs = super(MyProfileView, self).get_context_data(**kwargs)
        # This will no longer raise an exception if the customer_profile does
        # not exist
        kwargs.update(my_profile=self.customer_profile)
        return kwargs

 

hasattr

Because OneToOneFields will throw an exception if the related model doesn’t exist, the Django docs suggest using hasattr to check whether or not the related model exists.

I find this somewhat counterintuitive, and the behavior is also inconsistent with null=True ForeignKey fields. In addition, as someone who is approaching the code someone has written using all the hasattr checks for a model field, I think it’s kind of hard to read.

Ideally, there would be some property on the related model that informs me whether or not the related model exists. Something like this:

class User(AbstractUser):
    # ...
    def is_customer(self):
        return hasattr(self, 'customer_profile')


class CustomerProfile(models.Model):
    user = models.OneToOneField('User', related_name='customer_profile')

 

Now I can write code like this:

if request.user.is_customer:
    # do customer stuff
else:
    # do other stuff

 

Which I find much more readable than using than

if hasattr(request.user, 'is_customer'):
    # do customer stuff
else:
    # do other stuff

 

However, this of course forces me to implement my own custom user model, boo. Additionally, if my app has more than one user type (for example Customer and Merchant for a store app), then I will start having to keep adding more properties that have almost the exact same code. Not very DRY.

Wouldn’t it be better if I could automatically add a property that tells me whether or not the related model exists?

Here’s a OneToOneField subclass that provides this feature:

class AddFlagOneToOneField(OneToOneField):
    def __init__(self, *args, **kwargs):
        self.flag_name = kwargs.pop('flag_name')
        super(AddFlagOneToOneField, self).__init__(*args, **kwargs)

    def contribute_to_related_class(self, cls, related):
        super(AddFlagOneToOneField, self).contribute_to_related_class(cls, related)

        def flag(model_instance):
            return hasattr(model_instance, related.get_accessor_name())
        setattr(cls, self.flag_name, property(flag))

    def deconstruct(self):
        name, path, args, kwargs = super(AddFlagOneToOneField, self).deconstruct()
        kwargs['flag_name'] = self.flag_name
        return name, path, args, kwargs

 

This field builds on Django’s built-in OneToOneField, but with a few modifications, it could work with SoftOneToOneField.

Now, when I create my profile models, I can add that property to the User model without touching the User model[0].

class CustomerProfile(models.Model):
    user = AddFlagOneToOneField('auth.User', related_name='customer_profile',
                                flag_name='is_customer')


class MerchantProfile(models.Model):
    user = AddFlagOneToOneField('auth.User', related_name='merchant_profile',
                                flag_name='is_merchant')


class EmployeeProfile(models.Model):
    user = AddFlagOneToOneField('auth.User', related_name='employee_profile',
                                flag_name='is_employee')

 

And now the User model has these readable flags that I can use instead of hasattr.

user = User.objects.get(email='customer@example.com')
user.is_customer  # True
user.is_merchant  # False
user.is_employee  # False

 

AddFlagOneToOneField has the benefit of not throwing exceptions and also that you don’t have modify the User model.

Thanks for reading!

[0]: If you want to read more about this, see Williams, Master of the “Come From”.


Rocky is a lead Django developer at Fusionbox, a Python Development Company in Denver, CO. 

Return to Articles & Guides