最稳妥方式是在模型Meta.constraints中使用UniqueConstraint,Django 2.2+官方推荐,兼顾数据库级约束与ORM层校验;禁用已废弃的unique_together,因其仅做Python层检查、不建DB索引,易被绕过。

用 Meta.constraints 加复合唯一约束最稳妥
直接在模型的 Meta 类里加 UniqueConstraint,这是 Django 2.2+ 官方推荐方式,生成的迁移会同时作用于数据库和 ORM 层校验。别再用已废弃的 unique_together——它不触发数据库级约束,只做 Python 层检查,容易被绕过。
常见错误是写成 unique_together = ('field_a', 'field_b'),这在 Django 4.2 里虽还能跑,但迁移不会建 DB 级唯一索引,INSERT 冲突时抛的是数据库原生错误(比如 PostgreSQL 的 IntegrityError: duplicate key value violates unique constraint),而不是 Django 友好的 ValidationError。
正确写法示例:
class Order(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
product_id = models.CharField(max_length=32)
status = models.CharField(max_length=20)
<pre class='brush:python;toolbar:false;'>class Meta:
constraints = [
models.UniqueConstraint(
fields=['user', 'product_id'],
name='unique_user_product'
)
]-
name参数必须提供,否则迁移会报错:Django 要求每个约束显式命名 - 字段顺序影响查询性能:把高选择性字段(如
user)放前面,利于数据库索引利用 - 如果字段含
NULL,注意不同数据库行为:PostgreSQL 把多个NULL视为不重复,MySQL 5.7+ 默认也如此,但旧版本可能不同
需要条件唯一?用 condition 参数加过滤
比如「同一个用户对同一商品只能有一个未完成订单」,就得排除已关闭/已完成状态。这时候不能靠应用层判断,得让数据库强制保证。
立即学习“Python免费学习笔记(深入)”;
UniqueConstraint 支持 condition,但仅限 PostgreSQL 和 SQLite 3.30+(MySQL 不支持条件唯一约束)。
图片提示词生成器?不止如此。 马甲系统 —— 把脑海中的画面,翻译成AI能理解的专业表达。 用得越多,它越懂你:首次需要多问几句确认方向,用久了几乎一说就懂。 用得越多,它越快:缓存机制让后续对话越来越省。 RAG进化:成功案例持续入库,越跑越聪明。 输入「新手指南」查看完整功能介绍
示例:
class Order(models.Model):
# ... 字段同上
<pre class='brush:python;toolbar:false;'>class Meta:
constraints = [
models.UniqueConstraint(
fields=['user', 'product_id'],
name='unique_active_order',
condition=models.Q(status__in=['pending', 'processing'])
)
]-
condition必须是Q对象,不能用普通布尔表达式 - 加了
condition后,该约束变成部分索引(partial index),只对满足条件的行生效 - SQLite 用户注意:低于 3.30 版本不支持
CREATE UNIQUE INDEX ... WHERE,迁移会失败
已有数据表怎么安全加约束?
直接运行迁移大概率失败:数据库会检查全表,只要存在违反约束的历史数据,ALTER TABLE ... ADD CONSTRAINT 就报错,比如 PostgreSQL 报 ERROR: could not create unique index。
必须先清理或合并重复数据,再加约束。不要跳过这步——ORM 层的 get_or_create 或事务重试都挡不住并发写入导致的竞态。
- 用 raw SQL 找出重复项:
SELECT user_id, product_id, COUNT(*) FROM myapp_order GROUP BY user_id, product_id HAVING COUNT(*) > 1; - 决定策略:删旧留新?合并字段?人工确认?选好后再写数据修复脚本
- 修复完再运行
python manage.py makemigrations,Django 会生成带AddConstraint的迁移 - 生产环境建议在低峰期执行,并提前备份
为什么不用数据库原生命令手动建索引?
可以,但不推荐。手动在数据库里执行 CREATE UNIQUE INDEX,Django 迁移系统完全不知道这个索引存在,下次你改模型、重命名字段、甚至只是运行 makemigrations,都可能产生冲突或覆盖。
更麻烦的是,ORM 层无法感知这个约束:调用 model.full_clean() 不会校验它,Admin 表单提交重复数据也不会提前提示,只有 DB 抛异常后才回滚。
- 除非极端场景(比如要兼容老版本 Django 或特殊索引类型),否则一律走
Meta.constraints - 如果真要用原生命令,务必同步在
Meta.indexes里声明一个Index(哪怕不设unique=True),至少让 Django 知道索引存在 - 记住:Django 的约束 ≠ 数据库索引,但唯一约束会自动创建唯一索引;反过来,只建索引不加约束,ORM 校验就失效
真正容易被忽略的是约束名的可维护性——别用默认名如 myapp_order_user_id_product_id_2f3a8b_uniq,它随字段顺序和类型变化而变,diff 迁移时难以识别语义。每次手动指定 name,且保持命名风格统一,比如 uniq_{model}_{field1}_{field2}。

















