Django如何仅对新记录添加唯一约束?
这个场景我太熟悉了——给已有数据的模型加唯一约束,结果旧数据的重复直接炸了迁移,还想保留旧数据只管新记录?没问题,给你几个实用的方案,适配不同的Django版本和数据库:
方案1:分阶段迁移(通用所有数据库,最稳妥)
这个方法靠三次迁移实现“软约束到硬约束”的过渡,既保留旧数据,又能约束新记录:
- 第一步:先加代码层面的唯一校验
先修改模型添加unique=True,但让Django暂时不在数据库创建唯一约束:
执行模型修改后:
from django.db import models class Test(models.Model): name = models.CharField("Title", null=False, blank=False, max_length=300, unique=True)python manage.py makemigrations生成迁移文件,比如0002_test_name_unique.py。打开这个文件,找到AlterField操作,手动加上db_constraint=False参数:
执行operations = [ migrations.AlterField( model_name='test', name='name', field=models.CharField(blank=False, max_length=300, null=False, unique=True, verbose_name='Title', db_constraint=False), ), ]python manage.py migrate完成迁移。这时候Django的ORM会在代码层面校验新记录的唯一性,但数据库里还没加硬约束,旧数据的重复完全不受影响。 - 第二步:(可选但建议)清理旧重复数据
如果之后想开启数据库层面的硬约束,最好先处理旧重复数据——比如给重复的name加个后缀(name_1、name_2),或者合并重复记录。当然如果不想碰旧数据,这步可以跳过,保持代码层面的约束就行。 - 第三步:开启数据库硬约束(可选)
当你准备好后,再次执行python manage.py makemigrations,Django会自动生成一个添加数据库唯一约束的迁移文件(这次会去掉db_constraint=False),执行python manage.py migrate后,数据库层面也会约束新记录的唯一性。
方案2:PostgreSQL专属——部分索引约束
如果你用的是PostgreSQL,可以利用它的部分索引特性,只对新记录生效:
- 先不要在模型里加
unique=True,而是生成一个空迁移文件:
打开生成的空迁移文件,手动添加创建部分索引的SQL:python manage.py makemigrations --empty yourappnamefrom django.db import migrations, models class Migration(migrations.Migration): dependencies = [ ('yourappname', '0001_initial'), # 替换成你的初始迁移文件名 ] operations = [ migrations.RunSQL( # 假设当前最大id是旧数据的分界点,只约束id更大的新记录 "CREATE UNIQUE INDEX idx_test_name_unique_new ON test(name) WHERE id > (SELECT MAX(id) FROM test);", # 回滚时的SQL reverse_sql="DROP INDEX idx_test_name_unique_new;" ), ] - 然后在模型的
clean()方法里加代码层面的校验,确保接口提交也能拦截:
执行迁移后,数据库和代码层面都会只约束新记录,旧数据的重复完美保留。from django.core.exceptions import ValidationError class Test(models.Model): name = models.CharField("Title", null=False, blank=False, max_length=300) def clean(self): super().clean() # 只校验新创建的记录 if self._state.adding and Test.objects.filter(name=self.name).exists(): raise ValidationError("这个名称已经存在,新记录不能重复!")
方案3:Django 2.2+——带条件的UniqueConstraint
Django 2.2及以上支持带条件的唯一约束,你可以指定约束只对符合条件的新记录生效:
- 先查一下当前数据库里的最大id,作为新旧数据的分界:
假设得到的最大id是100,然后修改模型:python manage.py shell -c "from yourappname.models import Test; print(Test.objects.aggregate(models.Max('id'))['id__max'])"from django.db import models from django.db.models import Q class Test(models.Model): name = models.CharField("Title", null=False, blank=False, max_length=300) class Meta: constraints = [ models.UniqueConstraint( fields=['name'], name='unique_name_for_new_records', # 只约束id大于100的新记录 condition=Q(id__gt=100) ) ] - 同样记得在
clean()方法里添加代码层面的校验(和方案2一样),确保全面拦截。 - 最后执行
python manage.py makemigrations和migrate完成迁移即可。
内容的提问来源于stack exchange,提问作者techkuz
相关产品推荐
相关产品推荐

