How to add Check Constraints for Django Model fields?
Asked Answered
E

6

54

While subclassing db.models.Model, sometimes it's essential to add extra checks/constraints.

For example, I have an Event model with start_date and end_date: I want to add validation into the fields or the model so that end_date > start_date.

At least I know this can be done outside the models.Model inside the ModelForm validation. But how to attach to the fields and the models.Model?

Endocardial answered 17/2, 2010 at 13:54 Comment(5)
what you suggest as constraint can not be defined as a sql statement so only change you'll expect from such check is in admin form. You can do that by overriding adminform save function for that class. umnik700's answer shows how you can do it.Dzoba
Actually, there is a "CHECK" constraint in SQL. PostgreSQL supports this: postgresql.org/docs/8.1/static/ddl-constraints.html However, MySQL does not support this: The CHECK clause is parsed but ignored by all storage engines (see dev.mysql.com/doc/refman/5.5/en/create-table.html)Deuterogamy
@slack3r: Thanks. I know that there's a check but I just want it at higher level, at the Django metadata declaration level. I avoid schema changes.Endocardial
Yes, I know, this was just a reply to Numenor who said that this cannot be defined as an sql statement :)Deuterogamy
In this case, I'd look at using a DateRange field instead. That will ensure start <= end.Puccini
S
61

I would not put constraints like these in the save method, it's too late. Raising an exception there, doesn't help the user who entered the data in the wrong way, because it will end up as a 500 and the user won't get the form with errors back etc.

You should really check for this in the Forms/ModelForms clean method and raise a ValidationError, so form.is_valid() returns false and you can send the errors in the form back to the user for correction.

Also note that since version 1.2, Django has had Model Validation.

It would look something like this:

class Foo(models.Model):
    #  ... model stuff...
    def clean(self):
        if self.start_date > self.end_date:
            raise ValidationError('Start date is after end date')
Stagner answered 17/2, 2010 at 14:32 Comment(3)
Thanks for the second/better(?) answer. It's just what I needed.Deformation
This, of course, only works if you're using a ModelForm or call is_valid manually. Otherwise, if you just call save, it does nothing.Calaverite
Since Django 2.2, you can also add database-level constraints from the model's Meta class. docs.djangoproject.com/en/2.2/releases/2.2/#constraintsFacetiae
P
72

As of Django 2.2, database level constraints are supported:

from django.db import models
from django.db.models import CheckConstraint, Q, F

class Event(models.Model):
    start_date = models.DatetimeField() 
    end_date = models.DatetimeField()

    class Meta:
        constraints = [
            CheckConstraint(
                check = Q(end_date__gt=F('start_date')), 
                name = 'check_start_date',
            ),
        ]
Pryer answered 29/1, 2020 at 23:44 Comment(3)
This is the, currently, right answer! People who are using Django nowadays should know this feature!Bomb
This is the answer I was looking for, for hours :) So for others, the important takeaway is that you can access the values of other fields with F('other_field') and thus make comparison constraints on DB level, awesome stuff :) Still need to validate though, because the constraint alone will raise an error (500 on server), but it guarantees the DB will refuse as a last resort.Kerseymere
@Özer I believe that as of Django 4.1, check constraints are automatically checked during model validation. If that’s correct, that my understanding would be that, at least as of 4.1, it’s no longer necessary to enforce a constraint with both constraints and cleanconstraints alone should suffice.Uncounted
S
61

I would not put constraints like these in the save method, it's too late. Raising an exception there, doesn't help the user who entered the data in the wrong way, because it will end up as a 500 and the user won't get the form with errors back etc.

You should really check for this in the Forms/ModelForms clean method and raise a ValidationError, so form.is_valid() returns false and you can send the errors in the form back to the user for correction.

Also note that since version 1.2, Django has had Model Validation.

It would look something like this:

class Foo(models.Model):
    #  ... model stuff...
    def clean(self):
        if self.start_date > self.end_date:
            raise ValidationError('Start date is after end date')
Stagner answered 17/2, 2010 at 14:32 Comment(3)
Thanks for the second/better(?) answer. It's just what I needed.Deformation
This, of course, only works if you're using a ModelForm or call is_valid manually. Otherwise, if you just call save, it does nothing.Calaverite
Since Django 2.2, you can also add database-level constraints from the model's Meta class. docs.djangoproject.com/en/2.2/releases/2.2/#constraintsFacetiae
S
13

Do it inside your save method of your model:

def save(self, *args, **kwargs):
    if(self.end_date > self.start_date):
        super(Foo, self).save(*args, **kwargs)
    else:
        raise Exception, "end_date should be greater than start_date" 
Scrooge answered 17/2, 2010 at 14:5 Comment(2)
in django 1.2 or later remember to add *args, **kwargs both to the definition of the overriden save() method and anywhere it's being calledSturm
IMHO, having a check in the save model is great, but insufficient. You should always impose restrictions at the lowest possible level: in this case, you'd want the database to prevent storing any values that break the constraint.Puccini
S
12

As @stefanw says, it's better user experience to check in the form's clean method.

This is enough if you're very sure that there isn't, and never will be, another way to change the value. But since you can rarely be sure of that, if database consistency is important, you can add another check (in addition to the form), one of:

  • The easier and database-independent way is in the model's save method as @umnik700 said. Note that this still doesn't prevent other users of the database (another app, or the admin interface) from creating an inconsistent state.
  • To be 'completely' sure the database is consistent, you can add a database level constraint. E.g. you can create a migration with RunSQL and SQL, something like (not tested):

    migrations.RunSQL('ALTER TABLE app_event ADD CONSTRAINT chronology CHECK (start_date > end_date);')
    

    (Not tested). This may be database dependent, which is a downside of course.

In your example, it's probably not worth it (incorrect start/end times just look a bit weird, but affect only the one inconsistent event), and you don't want manual schema changes. But it's useful in cases where consistency is critical.

EDIT: You can also just save the start time and the duration, instead of the start and end times.

Strive answered 31/10, 2015 at 16:53 Comment(2)
Note that CHECK constraints straight-up don't work in MySQLCalaverite
It may be a little late, I would like to add that as per Django documentation documentation this requires installing the sqlparse Python library. docs.djangoproject.com/en/2.1/ref/migration-operationsStaciestack
T
3

As of today, both postgres 9.4 and MS SQL Server >= 2008 support check constraints in sql. On top of this, there is django issue 11964 which seems to be ready for review since yesterday, so hopefully we'll see this integrated into django 2. The project rapilabs/django-db-constraints seems to implement this too.

Tears answered 5/2, 2018 at 10:6 Comment(0)
J
3

Summarizing the answers from before, here is a complete solution I used for a project:

from django.db import models
from django.db.models import CheckConstraint, Q, F
from django.utils.translation import gettext_lazy as _

class Event(models.Model):
    start_date = models.DatetimeField() 
    end_date = models.DatetimeField()

    class Meta:
        constraints = [
            # Ensures constraint on DB level, raises IntegrityError (500 on debug=False)
            CheckConstraint(
                check=Q(end_date__gt=F('start_date')), name='check_start_date',
            ),
        ]

    def clean(self):
        # Ensures constraint on model level, raises ValidationError
        if self.start_date > self.end_date:
            # raise error for field
            raise ValidationError({'end_date': _('End date cannot be smaller then start date.')})

Too bad there is no django.core.validators that can handle this :(

Jan answered 14/7, 2020 at 22:2 Comment(2)
This is the complete solution that I confirm works to this date. Handles database data consistency and model (UI) validation.Polyunsaturated
There's a small logical bug. The check constraint, specifies end_date > start_date as a requirement. The clean method specifies start_date > end_date as an error. The two checks are inconsistent in handling the start_date == end_date case.Stauder

© 2022 - 2024 — McMap. All rights reserved.