Django User Profiles and select_related optimisation


If you want to associate extra information with a user account in Django you need to create a separate "User Profile" model. To get the profile for a given User object one simply calls get_profile. This is a nice easy way of handling things and helps keep the User model in Django simple. However it does have a downside - fetching the user and user profile requires two database queries. That's not so bad when we're selecting just one user and profile, but when we are displaying a list of users we'd end up doing one query for the users and another n-queries for the profiles - the classic n+1 query problem!

To reduce the number of queries to just one we can use select_related. Prior to Django 1.2 select_related did not support the reverse direction of a OneToOneField, so we couldn't actually use it with the user and user profile models. Luckily that's no longer a problem.

If we are clever we can setup the user profile so it stills works with get_profile and does not create an extra query.

get_profile looks like this:

def get_profile(self):
        if not hasattr(self, '_profile_cache'):
            # ...
            # query to get profile and store it in _profile_cache on instance
            # so we don't need to look it up again later
            # ...
        return self._profile_cache

So if select_related stores the profile object in _profile_cache then get_profile will not need to do any more querying.

To do this we'd define the user profile model like this:

class UserProfile(models.Model):
    user = models.OneToOneField(User, related_name='_profile_cache')
    # ...
    # other model fields
    # ...

The key thing is that we have set related_name on the OneToOneField to be '_profile_cache'. The related_name defines the attribute on the *other* model (User in this case) and is what we need to refer to when we use select_related.

Querying for all user instances and user profiles at the same time would look like:

User.objects.selected_related('_profile_cache')

The only downside is that this does change the behaviour of get_profile slightly. Previously if no profile existed for a given user a DoesNotExist exception is raised. With this approach get_profile instead returns None. So you're code will need to handle this. On the upside repeated calls to get_profile, when no profile exists, won't re-query the database.

The other minor problem is that we are relying on the name of a private attribute on the User model (that little pesky underscore indicates it's not officially for public consumption). Theoretically the attribute name could change in a future version of Django. To mitigate against the name changing I'd personally just store the name in a variable or on the profile model as a class attribute and reference that whenever you need it, so at least there's only one place to make any change. Apart from that this only requires a minor modification to your user profile model and the use of select_related, so it's not a massively invasive optimisation.