Skip to content
AITroveRead. Build. Understand.
Make this comfortable

Django models: database constraints survive an unchecked save

Last updated: 30 Sept 20264 min read
tutorial
IntermediateBy AITrove Editorial

A Django model maps fields and relationships to stored records, while database constraints enforce selected invariants at the storage boundary.

Download Python source kit

Operation contract

The receipt model has a unique receipt identifier, a partner foreign key and a nonnegative amount constraint. The fixture explicitly creates its schema in an in-memory SQLite database. A negative amount reaches the database through create and is rejected there. The transaction block contains the rejected write so later queries run outside the failed transaction.

Failure and ownership boundary

Model save does not automatically call full_clean. Database constraints do not replace field parsing, authorization or an application’s full domain rules. This standalone fixture uses app labels and schema_editor for a small owned schema; it is not a substitute for an installed application and migration history. Django migrations: generate and apply an owned application schema and Django forms: ASCII wire fields and cleaned domain values cover those separate layers.

Tested environment

Dependency check: this program was executed on CPython 3.14.6 with Django==5.2.17. Install these versions in a separate virtual environment. The download includes the recorded environment snapshot; no third-party package is part of the website runtime.

Working program

python
from django.conf import settings
settings.configure(INSTALLED_APPS=[], DATABASES={"default": {"ENGINE": "django.db.backends.sqlite3", "NAME": ":memory:"}},
                   DEFAULT_AUTO_FIELD="django.db.models.AutoField", USE_TZ=True)
import django
django.setup()
from django.db import connection, models, IntegrityError, transaction

class DispatchPartner(models.Model):
    region = models.CharField(max_length=3, unique=True)
    class Meta:
        app_label = "receipt_fixture"

class StoredReceipt(models.Model):
    receipt_id = models.CharField(max_length=6, unique=True)
    partner = models.ForeignKey(DispatchPartner, on_delete=models.PROTECT)
    amount = models.IntegerField()
    class Meta:
        app_label = "receipt_fixture"
        constraints = [models.CheckConstraint(condition=models.Q(amount__gte=0), name="receipt_amount_nonnegative")]

with connection.schema_editor() as schema:
    schema.create_model(DispatchPartner)
    schema.create_model(StoredReceipt)

partner = DispatchPartner.objects.create(region="DEL")
StoredReceipt.objects.create(receipt_id="R-0041", partner=partner, amount=125)
try:
    with transaction.atomic():
        StoredReceipt.objects.create(receipt_id="R-0042", partner=partner, amount=-1)
except IntegrityError:
    print("database constraint rejected negative amount")
print("stored receipts:", StoredReceipt.objects.count())
connection.close()

Output

Output
database constraint rejected negative amount
stored receipts: 1

Costs and limits

Each stored field and index adds database work and storage. Constraint checks and query plans depend on the backend. A passing SQLite fixture does not establish PostgreSQL locking, isolation or migration behavior.

Common Mistakes

  • Do not assume save invokes full_clean.
  • Catch an integrity error outside the atomic block that must roll back.

Connected lessons

Django forms: ASCII wire fields and cleaned domain values, Django migrations: generate and apply an owned application schema, Django atomic transactions: rollback the batch and defer callbacks.

Follow the service contract

Python Django database constraints: enforce receipt identity per owner.

python
django-models
Storage details