开发者

How to add Check Constraints for Django Model fields?

开发者 https://www.devze.com 2022-12-20 05:43 出处:网络
While subclassing db.models.Model, s开发者_Python百科ometimes it\'s essential to add extra checks/constraints.

While subclassing db.models.Model, s开发者_Python百科ometimes 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?


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')


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',
            ),
        ]


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" 


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.


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.


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 :(

0

精彩评论

暂无评论...
验证码 换一张
取 消