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
select_related stores the profile object in
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
related_name defines the attribute on the *other* model (User in this case) and is what we need to refer to when we use
Querying for all user instances and user profiles at the same time would look like:
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.