Where to define and instantiate associated models in Django

For the sake of example, let’s consider the following UML diagram describing two model classes and how they are interrelated:

We can see that there are two classes, User and UserProfile, with a one-to-one association between them. That is, each instance of class User is associated with one and only one instance of UserProfile and vice versa. This type of association is frequently encountered when modelling all sorts of real-world domains.

In Django, this kind of relationship between two entities is expressed as a OneToOneField defined within some Model class and pointing to another (or the same, in the case of a reflexive relationship). This raises the question of where to put the OneToOneField: Should it be an attribute of User or UserProfile?

You might be doubting the relevance of this question as–thanks to the related_name mechanism (aka reverse references)–we can later on traverse the association in either direction. This is certainly true, but there are other considerations to the association’s semantics that should be taken into account.

Existential dependence

One of these considerations is whether one object’s existence depends on the existence of the other. In the context of our example, does a UserProfile depend on the existence of a User? In most cases that would arguably hold true. The opposite statement could more likely turn out to not be true. Depending on the application, you could argue that a User can exist without having a UserProfile.

In this case, it would make sense to define the reference as an attribute of the UserProfile class. This way, you can express that whenever we delete a User instance, the associated UserProfile will be deleted as well. This would result in the following two Django model definitions:

class User(models.Model):
    pass

class UserProfile(models.Model):
    user = models.OneToOneField('User', on_delete=models.CASCADE)

If we defined the association within the User model class, the result would be semantically different:

class User(models.Model):
    profile = models.OneToOneField('User', on_delete=models.CASCADE)

class UserProfile(models.Model):
    pass

In the latter code example, the existence of a User instance depends on the existence of its associated UserProfile. Per se, neither piece of code is in any aspect “wronger” than the other. To know right from wrong, we would simply need to know more about the modeled domain.

Order of instantiation

Another factor to consider when deciding on how to associate models is the order of instantiation. In neither of the above model definitions is it possible to create both a User and a UserProfile instance at the same time. It is therefore necessary to decide which object to instantiate first.

As an example, let’s consider two possible scenarios of a user registration system. In the first scenario, a user completes the registration process and can later on fill out an optional UserProfile. In the second system, potential users are first asked to complete a profile as part of an application process. Only after they have been approved will an actual User instance be created.

Where to instantiate the associated objects

Once one has decided on where to put the associating attribute, it’s time to think about where to actually create the model instances. In the spirit of our example, we would like to create one and only one UserProfile whenever a new User has succesfully registered. At first glance, multiple places look like promising candidates for this functionality.

The __init__(…) magic method

Arguably the most obvious candidate is Python’s __init__(…) constructor method. After all, we would like to create a UserProfile whenever a new User is added or, in other words, initialized. However, this logic disregards the distinction between what happens in the database and what happens on the Python level.

__init__(…) is a Python construct and will be executed whenever a model instance is created in Python. This happens when we create a new User object for the first time, but it also happens whenever we retrieve an already existing instance from the database. In other words, if we were to instantiate a user’s UserProfile within the User model’s __init__(…) method, before long we would end up creating more than one profile instance per user!

The __new__(…) magic method

Another of Python’s magic methods, __new__(…), poses the same problem as __init__(…), as does Django’s save(…) method. The latter is called not only when creating a User object, but also when updating it. This behavior is not what we are looking for.

Signals to the rescue

Luckily, there is another mechanism that we can leverage to achieve what we want. Django features a variety of built-in signals which are a way for a piece of code to get notified when actions occur elsewhere in the framework. Beyond the built-in signals, a developer can easily define custom ones for application-specific events.

post_save is one of Django’s built-in signals. As its name strongly suggests, it is fired whenever an object has been saved to the application database. Though it’s not exactly what we need, we can still go ahead and base a new custom signal on the existing one:

# In myapp/signals.py
from django.db.models.signals import post_save
from django.dispatch import receiver, Signal

post_create = Signal()

@receiver(post_save)
def send_post_create(created, **kwargs):
    if created:
        post_create.send(**kwargs)

In Django, all signals are instances of the django.dispatch.Signal class. All you have to do to define a new signal is to name and instantiate a Signal instance. To actually fire the signal, we hook into post_save. Upon receiving an instance of this built-in signal and checking the created flag, we simply send a post_create signal. From now on we can react to the instantiation of a new object simply by defining another @receiver function with post_create as its signal.

Published