Using Django Check Constraints to Prevent Self-Following2021-02-26
Another way to use Django’s
CheckConstraint class to ensure your data is valid.
Based on my answer to a question on the Django forum.
Imagine we have a user model that we’d like to introduce a social media “following” pattern to. Users can follow other users to receive updates on our site. We’d like to ensure that users do not follow themselves, since that would need special care in all our code.
We can block self-following in the UI layer, but there’s always the risk that we’ll accidentally enable it. For example, we might add an “import your contacts” feature that allows self-following.
As with all the other posts in this series - the best way to prevent bad data is to block it in the database.
Setting up the relationship
To add the followers relationship, we’ll be using
ManyToManyField creates a hidden model class to hold the relationships.
Because we want to customize our model with add an extra constraint, we’ll need to use the
through argument to define our own visible model class instead.
We can define a first version of this like so:
to="self"to define that the relationship is from
Userto itself. Django calls this a recursive relationship.
We use the string format of
through, because we’re defining
Follow. We could define
Followfirst, but then we’d need to use strings to specify
Userin its definition.
We declare the relationship as asymmetrical with
symmetrical=False. If Alice follows Bob, it does not mean Bob follows Alice. If our relationship was a mutual “friend request” style, we would instead make the relationship symmetrical.
Followclass uses two foreign keys to link up the related users.
ManyToManyFieldwill automatically use the first foreign key as the “source” of the relationship and the other as the destination.
Followcould have a third foreign key to
User, for example to track another user who suggested the follow. In this case, we’d need to use
ManyToManyField.through_fieldsto specify which foreign keys actually form the relationship.
We have already added a constraint to the model - a
UniqueConstraintto ensure that exactly one relationship exists between users. Without this, multiple follows could exist between e.g. Alice and Bob, and it would be confusing what that means. This is copying what Django’s default hidden
We use string interpolation in our constraint’s name to namespace it to our model. This prevents naming collisions with constraints on other models. Databases have only one namespace for constraints across all tables, so we need to be careful.
After adding a migration and running it, we can try our models out on the shell:
Note we were still able to have
user1 follow itself.
Let’s fix that now.
It’s time to set up our
First, we add the constraint in
check here declares that the
from_user field is not equal to the
We use an
F() object to represent the
We negate the condition with the easy-to-miss squiggle operator (
~), which turns the
The condition is therefore
from_user != to_user in Python syntax.
Second, we run
makemigrations to generate a new migration:
We check the migration looks correct:
So far so good!
Third, we can add a test that checks our check constraint works as intended:
The test tries to self-follow and ensures that this triggers a database error which contains the name of our constraint.
Fourth, we need to consider how our migration will work with existing bad data.
If we try and migrate whilst self-follow relationships exist, the migration will fail with an
We can delete any self-follow relationships in the migration with a
This allows us to use the ORM to modify the database.
We can add a module-level function for
RunPython and then reference it in the operation:
We use the same template for
forwards_funcas in the
We fetch the point-in-history version of the
apps.get_model(), rather than importing the latest version. Using the latest version would fail since it could reference fields that haven’t been added to the database yet.
We also use the current database alias. It’s best to this even if our project only uses a single database, in case it gains multiple in the future.
reverse_codeas a no-op, so that this migration is reversible. Reversing the migration won’t be able to restore deleted self-follow relationships because we aren’t backing them up anywhere.
We declare the operation as elidable. This means Django can drop the operation when squashing the migration history. This is always worth considering when writing a
RunSQLoperation, as it helps you make smaller, faster squashes.
Now we can run the migration without problem:
Fifth, we should step through our UI and API and check the ability to self-follow is not exposed anywhere.
If we have an API, we’d want to make sure it returns a sensible error message when attempting to self-follow, since otherwise it would just crash with an
You can’t follow yourself, but you can follow me on Twitter,
Want better tests? Check out my book Speed Up Your Django Tests which teaches you to write faster, more accurate tests.
One summary email a week, no spam, I pinky promise.
- Using Django Check Constraints to Limit A Model to a Single Instance
- Using Django Check Constraints to Prevent the Storage of The Empty String
- Django's Field Choices Don't Constrain Your Data
© 2021 All rights reserved.