(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.
- The Django OneToOneField raises exceptions if the related model doesn’t exist.
- In order to check whether or not the related model exists, you have to use
hasattr
.
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:
-
Make sure that the related model always exists, (perhaps using the
post_save
signal). -
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.