Using Django Check Constraints to Prevent the Storage of The Empty String2021-01-31
Here’s another use case for creating a database constraint with Django’s
By default, a model
CharField will store any string that the database supports.
This includes the empty string, which is often inappropriate.
For example, imagine we have a
Team model, representing a group that users can belong to.
We started by giving it a unique
from django.db import models class Team(models.Model): name = models.CharField(max_length=120) class Meta: constraints = [ models.UniqueConstraint( name="%(app_label)s_%(class)s_name_unique", fields=["name"], ), ]
Whilst seemingly reasonable, this model definition allows the creation of teams with an empty
Such a team object is likely to break other parts of our application.
For example, links might not display or unexpected errors may occur due to
name being falsey.
We could try to prevent the creation of empty-named teams with form validation, but that does not prevent creation of bad data through other processes, such as bulk imports. To ensure all the data in our system is valid, we need a database constraint.
Here we want to add a minimal database constraint that ensures the length of
name is greater than 0. Let’s walk through that now.
First, we need to register the
Length function for use with
This makes it available as a transform or lookup, like the default lookups such as
<field>__gt for “field greater than”.
Registering the function requires only a single function call:
from django.db import models from django.db.models.functions import Length models.CharField.register_lookup(Length)
We can add this at the top of our models file.
Django has many such functions we can register as extra” lookups/transforms - check out other references to
register_lookup() in the database functions documentation.
Second, we add our new constraint to
We use a
Q() object to represent what data is valid - where the name length is greater than 0:
from django.db import models from django.db.models.functions import Length models.CharField.register_lookup(Length) class Team(models.Model): name = models.CharField(max_length=120) class Meta: constraints = [ models.UniqueConstraint( name="%(app_label)s_%(class)s_name_unique", fields=["name"], ), models.CheckConstraint( name="%(app_label)s_%(class)s_name_not_empty", check=models.Q(name__length__gt=0), ), ]
We’d then run
makemigrations to generate a new migration:
$ ./manage.py makemigrations core Migrations for 'core': example/core/migrations/0002_team_core_team_name_not_empty.py - Create constraint core_team_name_not_empty on model team
This migration has only the one step, adding the new constraint:
from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ ("core", "0001_initial"), ] operations = [ migrations.AddConstraint( model_name="team", constraint=models.CheckConstraint( check=models.Q(name__length__gt=0), name="core_team_name_not_empty", ), ), ]
Third, we can write a test.
I like to do this for all check constraints to ensure they do what I meant.
Here our test tries to create a bad team, and asserts that the database raises an
IntegrityError mentioning the name of our constraint:
from django.db import IntegrityError from django.test import TestCase from example.core.models import Team class TeamTests(TestCase): def test_name_non_empty(self): constraint_name = "core_team_name_not_empty" with self.assertRaisesMessage(IntegrityError, constraint_name): Team.objects.create(name="")
Fourth, before we can deploy our change, we need to check that there is no empty-named team in our production database. If such a team does exist, the database will fail to apply the migration, since constraints apply to all data.
We can do this with
./manage.py shell on production:
In : from example.core.models import Team In : Team.objects.filter(name="").count() Out: 0
Great, zero results. If such a team existed, we’d want to fix it, perhaps by renaming or deleting it.
Fifth, we’d want to add some extra user-facing validation, such as in forms or API endpoints. This can present a nice error message to the user if they attempt to create such a team. Without such validation attempts to use the empty string will crash:
In : Team.objects.create(name="") --------------------------------------------------------------------------- IntegrityError Traceback (most recent call last) ... IntegrityError: CHECK constraint failed: core_team_name_not_empty
In the forms and DRF
CharFields we can do this by declaring
May you store only valid data,
🎉 My book Speed Up Your Django Tests is now up to date for Django 3.2. 🎉
Buy now on Gumroad
One summary email a week, no spam, I pinky promise.
© 2021 All rights reserved.