You need to enable JavaScript to run this app. 如何修改Django模型unique=True的行为以排除软删除对象?
优惠活动
大模型
产品
解决方案
定价
更多

如何修改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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.22 22:15:05