Django 4.2 的 CheckConstraint 将校验下推至数据库原生 CHECK 约束,支持 PostgreSQL/SQLite/Oracle/MySQL 8.0.16+,由数据库强制执行以避免竞态条件;需用 Q 对象定义 check 表达式,显式处理 NULL,命名需唯一,且不替代应用层校验。

CheckConstraint 在 Django 4.2 中能做什么
Django 4.2 真正把 CheckConstraint 变成可用的生产级工具——它不再只是模型层校验,而是直接翻译成数据库原生的 CHECK 约束(PostgreSQL/SQLite/Oracle 支持,MySQL 8.0.16+ 也支持,但需注意其对 NULL 的处理差异)。这意味着约束在 INSERT/UPDATE 时由数据库引擎强制执行,绕过 Django ORM 层,避免竞态条件导致的数据不一致。
常见误用是把它当 validators 用:验证逻辑写在模型字段的 validators 参数里,只在 full_clean() 或表单中触发,对 raw SQL、bulk_create、admin 直接保存等场景完全失效。而 CheckConstraint 是兜底防线。
怎么定义一个带条件的 CheckConstraint
核心是写清楚 check 表达式,用 Q 对象组合字段比较,不能写 Python 函数或方法调用。Django 会把它编译为 SQL 的 CHECK (…) 子句。
-
check必须是Q对象,例如Q(status__in=["draft", "published"]) | Q(deleted_at__isnull=False) - 字段名必须带双下划线语法,且只能引用本模型字段(不支持跨表关联字段)
- 不能使用
__contains、__regex这类依赖数据库函数的查找——它们无法可靠转为 CHECK 表达式,会报NotSupportedError - NULL 值要显式处理:比如
Q(age__gte=0) | Q(age__isnull=True),否则age=NULL会让整个 CHECK 返回 UNKNOWN,被数据库视为违反约束(PostgreSQL 行为)
示例:限制文章草稿不能有发布日期
强戈环形山: q15040409-刻画家功能概述强戈环形山: q15040409-刻画家是一项面向实际任务的技能,主要用于平顶山: 维西翁: 1:1.0. 塔格斯: rap.github: 自动化。它将相关步骤、工具调用和结果整理方式集中到统一流程中,帮助使用者更快完成目标并减少重复操作。核心要点使用时应结合输入条件选择合适的执行方式,核对必要参数、依赖环境与输出内容,并按原始要求处理异常情况。该技能适合需要稳定复用相关能力的场景,可作为自动化工作流的一部分,也便于后续检查、调整和扩展。从功能定位来看,该
立即学习“Python免费学习笔记(深入)”;
class Article(models.Model):
title = models.CharField(max_length=200)
status = models.CharField(max_length=20, choices=[("draft", "Draft"), ("published", "Published")])
published_at = models.DateTimeField(null=True, blank=True)
class Meta:
constraints = [
models.CheckConstraint(
check=Q(status="draft") & Q(published_at__isnull=True) | Q(status="published"),
name="draft_must_not_have_published_at"
)
]
迁移时遇到 “constraint already exists” 或 “duplicate constraint name” 怎么办
Django 默认按 name 字段识别约束,但数据库层面约束名是全局唯一的。如果你重命名了约束、改了模型再迁移,容易撞名。更隐蔽的是:SQLite 不支持 ALTER TABLE … DROP CONSTRAINT,Django 迁移会尝试重建整张表,若数据量大可能卡住或失败。
- 先查现有约束:
python manage.py dbshell进去后,PostgreSQL 执行\d+ your_table_name,SQLite 执行PRAGMA table_info(your_table_name);和SELECT sql FROM sqlite_master WHERE type='table' AND name='your_table_name'; - 手动删旧约束(PostgreSQL):
ALTER TABLE your_table DROP CONSTRAINT old_constraint_name; - SQLite 下别硬刚,建议用
makemigrations --empty写一个自定义迁移,用RunSQL手动建新约束,再删旧约束(如果存在) - 始终在
Meta.constraints中显式指定name,不要依赖 Django 自动生成名(如article_status_check比article_9a7b3c好维护)
为什么 save() 成功但数据库报错:“new row violates check constraint”
这通常说明你在 ORM 层绕过了校验——比如用了 update()、bulk_create()、raw(),或者在 admin 中勾选了 “Skip validation”。Django 不会在这些操作中主动调用 full_clean(),但数据库约束仍生效,于是抛出原始错误(如 PostgreSQL 的 IntegrityError: new row violates check constraint "xxx")。
- 对
bulk_create(),务必提前在 Python 层做一次过滤:[a for a in articles if a.status != "draft" or a.published_at is None] - 在
ModelAdmin.save_model()中加obj.full_clean(),否则 admin 保存跳过模型验证 - 用
update()修改受约束字段时,确保WHERE条件不会意外让某行违反约束(例如把所有status="draft"的记录published_at设为非空) - 日志里看到这个错误时,第一反应不是改代码逻辑,而是检查数据库实际存了什么数据——可能已有脏数据在迁移前就存在,导致新约束无法启用
最常被忽略的一点:CHECK 约束不自动索引,也不阻止并发写入产生冲突;它只保证单次写入合法。如果业务逻辑本身存在竞争(比如两个请求同时判断“当前状态可发布”,然后都去 update),得靠数据库锁或应用层分布式锁来兜底,不能只靠 CheckConstraint。

















