如何修改Django模型unique=True的行为以排除软删除对象?
软删除场景下的唯一性约束适配方案
问题根源
直接给模型字段配置unique=True时,Django会生成全表维度的数据库唯一索引,该索引不区分is_deleted的状态,只要字段值完全相同就会触发约束冲突。仅靠应用层逻辑判断容易在并发场景下出现数据不一致,需要从数据库层硬约束、应用层友好校验两个维度调整,实现“仅对未软删除的记录做唯一性校验”的目标。
具体实现步骤
1. 调整模型定义,替换全局唯一约束
移除name字段自带的全表unique=True属性,改用Django 2.2+支持的带条件的唯一约束,让数据库仅对未软删除的记录做唯一性检查:
from django.db import models class MyModel(models.Model): # 移除字段上的 unique=True 配置 name = models.CharField(max_length=20) is_deleted = models.BooleanField(default=False) class Meta: constraints = [ models.UniqueConstraint( fields=['name'], # 约束生效的条件:仅针对未软删除的记录 condition=models.Q(is_deleted=False), name='unique_active_name' ) ]
这个约束的效果是:已被标记为is_deleted=True的记录完全不参与唯一性校验,就算名称和新提交的记录完全一致,也不会触发数据库层面的冲突。
2. 补充应用层校验,提前返回友好提示
如果仅依赖数据库约束,触发冲突时会抛出底层的IntegrityError,用户体验较差。需要在Serializer层补充自定义校验逻辑,在请求到达数据库前就拦截违规操作:
from rest_framework import serializers from .models import MyModel class MySerializer(serializers.ModelSerializer): class Meta: model = MyModel fields = ['id', 'name', 'is_deleted'] def validate_name(self, value): # 查询所有未软删除、且名称和当前输入一致的记录 active_duplicate_qs = MyModel.objects.filter(name=value, is_deleted=False) # 更新场景下排除当前编辑的对象本身,避免自身校验冲突 if self.instance: active_duplicate_qs = active_duplicate_qs.exclude(pk=self.instance.pk) if active_duplicate_qs.exists(): raise serializers.ValidationError("该名称已被使用,请更换后重试") return value
3. 优化ViewSet的默认逻辑
将默认查询集调整为仅返回未软删除的记录,避免常规接口调用误操作软删数据;软删除时指定仅更新is_deleted字段,减少不必要的性能开销:
from rest_framework import viewsets, status from rest_framework.response import Response from .models import MyModel from .serializers import MySerializer class MyViewSet(viewsets.ModelViewSet): # 默认过滤已软删除的记录 serializer_class = MySerializer queryset = MyModel.objects.filter(is_deleted=False) def destroy(self, request, *args, **kwargs): instance = self.get_object() instance.is_deleted = True # 仅更新软删除标记字段,避免触发其他字段的冗余校验 instance.save(update_fields=['is_deleted']) return Response(status=status.HTTP_204_NO_CONTENT)
注意事项
- 数据库层的条件约束是数据一致性的最终保障,应用层校验仅做体验优化,两者缺一不可,否则并发场景下可能出现重复数据。
- 该方案兼容PostgreSQL、SQLite(3.8+)、MySQL(8.0+)等支持部分索引的主流数据库。
- 如果使用的低版本MySQL不支持条件唯一索引,可采用降级方案:软删除时将目标字段值追加固定后缀(如
原name__deleted_{时间戳})绕开全表唯一索引,但这种方案维护成本更高,不如部分索引优雅。 - 调整模型约束后,需要生成并执行新的数据库迁移,迁移前请先清洗历史数据中已存在的、未软删除记录间的重名问题,避免迁移报错。
内容的提问来源于stack exchange,提问作者D_P
相关产品推荐
相关产品推荐

