Django Constraints & Database Level Validation

python backend django database

Application-level validation can fail. Users can bypass your forms. Scripts can insert bad data directly. Database constraints are your last line of defense.

Django 2.2 brings native constraint support to the ORM.

Why Database Constraints?

Consider this model:

class Discount(models.Model):
    product = models.ForeignKey(Product, on_delete=models.CASCADE)
    percentage = models.IntegerField()
    start_date = models.DateField()
    end_date = models.DateField()

What stops someone from creating a 200% discount? Or an end date before the start date?

Model validation helps, but it’s not enforced at the database level. Raw SQL, management commands, other applications—they all bypass Django validation.

Check Constraints

Enforce rules at the database level:

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

class Discount(models.Model):
    product = models.ForeignKey(Product, on_delete=models.CASCADE)
    percentage = models.IntegerField()
    start_date = models.DateField()
    end_date = models.DateField()
    
    class Meta:
        constraints = [
            models.CheckConstraint(
                check=Q(percentage__gte=0) & Q(percentage__lte=100),
                name='valid_percentage_range'
            ),
            models.CheckConstraint(
                check=Q(end_date__gte=F('start_date')),
                name='end_after_start'
            ),
        ]

Now the database rejects invalid data:

>>> Discount.objects.create(percentage=150, ...)
IntegrityError: CHECK constraint failed: valid_percentage_range

Unique Constraints

More powerful than unique=True:

Basic Unique

class UserProfile(models.Model):
    user = models.OneToOneField(User, on_delete=models.CASCADE)
    username = models.CharField(max_length=100)
    
    class Meta:
        constraints = [
            models.UniqueConstraint(
                fields=['username'],
                name='unique_username'
            ),
        ]

Conditional Unique

Only enforce uniqueness under certain conditions:

class Order(models.Model):
    user = models.ForeignKey(User, on_delete=models.CASCADE)
    status = models.CharField(max_length=20)
    
    class Meta:
        constraints = [
            models.UniqueConstraint(
                fields=['user'],
                condition=Q(status='pending'),
                name='one_pending_order_per_user'
            ),
        ]

Users can have multiple completed orders, but only one pending order.

Case-Insensitive Unique

from django.db.models.functions import Lower

class Brand(models.Model):
    name = models.CharField(max_length=100)
    
    class Meta:
        constraints = [
            models.UniqueConstraint(
                Lower('name'),
                name='unique_brand_name_case_insensitive'
            ),
        ]

“Apple”, “APPLE”, and “apple” are all considered duplicates.

Composite Constraints

Multiple fields together:

class Enrollment(models.Model):
    student = models.ForeignKey(Student, on_delete=models.CASCADE)
    course = models.ForeignKey(Course, on_delete=models.CASCADE)
    semester = models.CharField(max_length=20)
    
    class Meta:
        constraints = [
            models.UniqueConstraint(
                fields=['student', 'course', 'semester'],
                name='unique_enrollment'
            ),
        ]

A student can only enroll in each course once per semester.

Combining Check Constraints

Use Q objects for complex logic:

class Product(models.Model):
    price = models.DecimalField(max_digits=10, decimal_places=2)
    sale_price = models.DecimalField(max_digits=10, decimal_places=2, null=True)
    is_on_sale = models.BooleanField(default=False)
    
    class Meta:
        constraints = [
            # Price must be positive
            models.CheckConstraint(
                check=Q(price__gt=0),
                name='positive_price'
            ),
            # If on sale, sale_price must exist and be less than price
            models.CheckConstraint(
                check=(
                    Q(is_on_sale=False) |
                    (Q(is_on_sale=True) & Q(sale_price__isnull=False) & Q(sale_price__lt=F('price')))
                ),
                name='valid_sale_price'
            ),
        ]

Migrations

Constraints are automatically included in migrations:

class Migration(migrations.Migration):
    operations = [
        migrations.AddConstraint(
            model_name='discount',
            constraint=models.CheckConstraint(
                check=models.Q(percentage__gte=0),
                name='valid_percentage',
            ),
        ),
    ]

Handling Constraint Violations

from django.db import IntegrityError

try:
    Discount.objects.create(percentage=150, ...)
except IntegrityError as e:
    if 'valid_percentage' in str(e):
        raise ValidationError('Percentage must be between 0 and 100')
    raise

Or validate before saving:

class Discount(models.Model):
    ...
    
    def clean(self):
        if self.percentage < 0 or self.percentage > 100:
            raise ValidationError('Percentage must be between 0 and 100')
        if self.end_date < self.start_date:
            raise ValidationError('End date must be after start date')

Database Support

Check which constraints your database supports:

ConstraintPostgreSQLMySQLSQLite
Check✅ (8.0+)
Unique
Conditional Unique✅ (3.9+)
Expression Index

PostgreSQL has the best support. SQLite works for development.

Constraints vs Model Validation

Use both:

class Discount(models.Model):
    percentage = models.IntegerField(
        validators=[MinValueValidator(0), MaxValueValidator(100)]
    )
    
    class Meta:
        constraints = [
            models.CheckConstraint(
                check=Q(percentage__gte=0) & Q(percentage__lte=100),
                name='valid_percentage_range'
            ),
        ]

Best Practices

  1. Name constraints descriptively: You’ll see these names in error messages
  2. Test constraints: Write tests that expect IntegrityError
  3. Keep validation in sync: Don’t have constraints without matching validators
  4. Consider performance: Complex check constraints can slow writes
  5. Use PostgreSQL: Best constraint support

Final Thoughts

Database constraints are cheap insurance. Add them for any business rule that must never be violated.

Start with obvious rules—positive prices, valid date ranges, uniqueness requirements. Your future self will thank you when a bug tries to insert invalid data.


Trust but verify. Verify at the database.

All posts