In Django 6.1, how do you declare a CheckConstraint in Meta.constraints, and on which code paths is it enforced?
answer
- a rule on one row's values
- Q object, not a string
- the keyword was renamed
- database always, full_clean sometimes
basics
~20 sAdd models.CheckConstraint(condition=Q(...), name=...) to Meta.constraints. The database rejects any write that breaks it with IntegrityError, whatever the code path; full_clean() also checks it and raises ValidationError. The old check= keyword was removed in Django 6.0.
solid answer
~40 sA `CheckConstraint` states a rule about the values within one row, such as `seats >= 1` or "a subscription ends after it starts". In Django 6.1 you write `models.CheckConstraint(condition=Q(seats__gte=1), name="subscription_seats_positive")`; `condition` must be a `Q` or boolean expression, and the older `check=` keyword was deprecated in 5.1 and removed in 6.0. The migration adds a SQL `CHECK`, so every write path, including `save()`, `update()`, `bulk_create()` and raw SQL, is rejected with `IntegrityError` when it breaks the rule. In Python, `full_clean()` calls `validate_constraints()`, which evaluates the condition and raises `ValidationError` with the constraint's message, so `ModelForm`s show a proper error. Like SQL `CHECK`, a condition that evaluates to unknown because of a NULL passes.
code
python · 13 linesfrom django.core.exceptions import ValidationError
from django.db import IntegrityError
sub = Subscription(user=user, plan=team, status="active", seats=0, started_at=now)
try:
sub.full_clean()
except ValidationError as exc:
print(exc.messages) # ['Constraint “subscription_seats_positive” is violated.']
try:
Subscription.objects.filter(pk=existing.pk).update(seats=0)
except IntegrityError:
pass # update() skips model validation, but the CHECK still rejects itgo deeper
Recall that CheckConstraint goes in Meta.constraints with a Q condition and a name, and that the database enforces it.
Explain which paths raise IntegrityError and which raise ValidationError, the check-to-condition rename, and the difference from field validators.
Plan adding a check to a populated table, handle NULL semantics deliberately, and make API errors readable with violation messages and codes.
Decide which invariants belong in database constraints so every writer, including scripts and other services, is held to them.
## What a check constraint is for Some rules involve only the values in a **single row**: a subscription has at least one seat, a discount is between 0 and 100, an end date comes after a start date. A database `CHECK` constraint enforces exactly this kind of rule. Django declares one with `models.CheckConstraint` in `Meta.constraints`. ## Declaring one in Django 6.1 ```python from django.db import models from django.db.models import F, Q class Meta: constraints = [ models.CheckConstraint( condition=Q(seats__gte=1), name="subscription_seats_positive", ), models.CheckConstraint( condition=Q(ended_at__isnull=True) | Q(ended_at__gt=F("started_at")), name="subscription_ends_after_start", violation_error_message="A subscription must end after it starts.", ), ] ``` API points: - **`condition`** is keyword-only and must be a `Q` object or a boolean expression; anything else raises `TypeError`. - **`name`** is required and must be unique in the database; on an abstract model use `%(app_label)s` and `%(class)s`. - `violation_error_message` and, since Django 5.0, `violation_error_code` control the `ValidationError` raised during model validation. - The keyword used to be `check=`. It was deprecated in Django 5.1 and **removed in 6.0**, so older tutorials that write `CheckConstraint(check=Q(...))` fail on current Django. ## Where it is enforced | Code path | Result when the rule is broken | |---|---| | `save()`, `create()` | database rejects: `IntegrityError` | | `QuerySet.update()`, `bulk_create()`, `bulk_update()` | database rejects: `IntegrityError` | | raw SQL, other services writing the table | database rejects the statement | | `full_clean()` / `validate_constraints()` | `ValidationError` with the constraint's message, before any write | | `ModelForm.is_valid()` | the same model validation, reported as a form error | The database is the guarantee: it catches every writer, including the bulk paths that skip `save()` entirely. Model validation is the courtesy layer that lets forms and APIs show a clear message instead of a server error. ## Two subtleties 1. **NULL passes.** In SQL a `CHECK` fails only when the condition is false, not when it is unknown. `Q(ended_at__gt=F("started_at"))` alone would therefore accept a row with `ended_at` NULL anyway. Django's `validate()` mirrors that on backends that support it by treating an unknown result as passing. Writing the NULL case explicitly, as in the example, makes the intent readable. 2. **Excluded fields skip validation.** When `full_clean()` is called with `exclude` (a `ModelForm` excludes fields it does not show), constraints whose condition mentions an excluded field are skipped in Python. The database still enforces them on save. ## Rolling one out on existing data A new `CheckConstraint` is validated against every existing row when the migration runs. A safe sequence: 1. Query for violators first, for example `Subscription.objects.filter(seats__lt=1)`; remember that rows where the compared column is NULL do not violate a `CHECK`. 2. Fix or archive those rows in a data migration or a script. 3. Add the constraint in its own migration, so a failure points clearly at the data rather than at unrelated schema changes. Keeping the condition in a module-level `Q` object lets the violator query and the constraint share one definition. ## CheckConstraint versus field validators A field's `validators` (for example `MinValueValidator(1)` on `seats`) run only during model and form validation; they never reach the database, so `update()` or a bulk load can bypass them. A `CheckConstraint` is enforced by the database for every writer and is also checked by `full_clean()`. Teams often use both: validators for field-level messages in forms, a constraint for the invariant that must never be broken. Cross-field rules, like start before end, belong in a constraint (or `clean()`), since a single field's validator cannot see the other field. ## Backend support Most backends support check constraints. Where one does not, Django's system checks emit `models.W027` and no constraint is created. Adding a constraint to a table that already holds violating rows fails when the migration runs, so clean the data first.
- An older Django tutorial writes CheckConstraint(check=Q(seats__gte=1), name=...). What happens on Django 6.1?It fails: the `check` keyword was deprecated in Django 5.1 and removed in 6.0, and `condition` is a required keyword-only argument, so constructing the constraint raises a `TypeError`. Rename the argument to `condition=`; the generated SQL is unchanged, and migrations written with the new keyword serialize it as `condition`.
saying these in an interview costs you the question
- CheckConstraint is only checked by Django forms, not by the database.
- QuerySet.update() bypasses CheckConstraint because it skips save().
- CheckConstraint(check=Q(...)) is the current Django 6.1 syntax.
- A CHECK condition fails when a compared column is NULL.
- save() calls full_clean() automatically, so constraints raise ValidationError there.