Fork me on GitHub

What problems are solved with this?

It often occurs that a certain entity, a User, a Company, etc. has a list of items, with one item being the default one. For example a User could have different Settings, with (at most) one Setting being the default one, or a Company that has multiple names, but one official one. We want to ensure there is at most one such default record, and a more efficient way to retrieve that record.

What does this pattern look like?

First we make a model to store the corresponding Settings or CompanyNames with a BooleanField() that indicates if the item is the default, for example:

from django.conf import settings
from django.db import models

class UserSetting(models.Model):
    user = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        on_delete=models.CASCADE
    )
    is_default = models.BooleanField(default=False)
    # …

We can enforce at the database level that there is at most one such element with a UniqueConstraint, this constraint makes the user unique, with as condition that is_default is True, like:

from django.conf import settings
from django.db import models
from django.db.models import Q


class UserSetting(models.Model):
    user = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        on_delete=models.CASCADE
    )
    is_default = models.BooleanField(default=False)
    # …

    class Meta:
        constraints = [
            models.UniqueConstraint(
                fields=('user',),
                condition=Q(is_default=True),
                name='one_default_setting_per_user'
            )
        ]

This now ensures there is at most one default. But this would require making an extra query to retrieve the default setting for a user. Indeed, for some_user, we would query with:

user_settings = some_user.usersetting_set.get(is_default=True)

this will then either retrieve the UserSetting record, or raise a UserSetting.DoesNotExist exception, if no such record exists. It can not raise a UserSetting.MultipleObjectsReturned exception, unless the database does not enforce the constraint, since there is always at most one default setting per item.

But this still requires making a query per User, which might not be efficient if we have to render the settings of all users immediately. We can work with a FilteredRelation for this, indeed:

from django.db.models import FilteredRelation, Q

users = User.objects.annotate(
    default_setting=FilteredRelation(
        'usersetting',
        condition=Q(usersetting__is_default=True)
    ),
).select_related('default_setting')

This will add an extra attribute .default_setting to the User objects arising from the QuerySet, which is a UserSetting object with thus the settings for that user. If no UserSetting exists, it has no such attribute.

So we can enumerate over the users, and check if such attribute exists with:

for user in users:
    try:
        setting = user.default_setting
    except AttributeError:
        setting = None

we thus fetch all the default settings in bulk, and can then post-process these.

Extra tips

What if we want only one default record for the entire table?

In that case, we can set the field that is unique, to is_default itself, like:

class GlobalSetting(models.Model):
    is_default = models.BooleanField(default=True)
    # …

    class Meta:
        constraints = [
            models.UniqueConstraint(
                fields=('is_default',),
                condition=Q(is_default=True),
                name='one_global_default_setting'
            )
        ]