django实现逻辑删除/软删除

django目前模型是不带逻辑删除的,但是实际业务场景可能会用到逻辑删除,目前用以下方法实现
逻辑删除主要有两种方案,都是通过添加一个is_delete字段
方案1:添加is_delete字段为BooleanField,True代表删除,False代表没删除
方案2:添加is_delete字段与主键同类型,is_delete为默认值时表示未删除,is_delete==pk代表已删除

方案1存在unique问题,考虑以下模型:
School模型

字段名称类型是否唯一
idAutoFieldyes
nameCharFieldyes
avatarUrlFieldno

如果添加is_delete为True/False 我添加两条记录(1,清华大学,www.baidu.com,True),(1,清华大学,www.baidu.com,False)会报错,即使添加unique_together=(name,is_delete)数据库里也最多出现上面两条记录,所以采用方案2

字段名称类型是否唯一
idAutoFieldyes
nameCharFieldyes
avatarUrlFieldno
is_deleteIntegerFieldno

所有django模型继承于以下模型:

_DELETE_DEFAULT_VALUE = 'f4f5151bebc447bc8f63f3687a0bd8a1'
def process_relates(related, instance, get_related_queryset):
    field = related.field
    related_model = related.related_model
    if field.remote_field.on_delete == CASCADE:
        get_related_queryset(related_model, field, instance).delete()
    elif field.remote_field.on_delete == SET_NULL:
        get_related_queryset(related_model, field, instance).update(**{field.name: None})
    elif field.remote_field.on_delete == SET_DEFAULT:
        get_related_queryset(related_model, field, instance).update(**{field.name: field.remote_field.default})
    elif field.remote_field.on_delete == PROTECT:
        raise ProtectedError(
            "Cannot delete some instances of model '%s' because they are "
            "referenced through a protected foreign key: '%s'" % (
                field.remote_field.model.__name__, field.name
            ),
            None
        )
    elif field.remote_field.on_delete == DO_NOTHING:
        pass
    elif field.remote_field.on_delete == RESTRICT:
        # TODO 需要重新实现一下
        pass
    elif field.remote_field.on_delete == SET:
        pass
        # todo 这里有点问题
        # related_model.objects.filter(**{field.name: instance.pk}).update(
        #     **{field.name: field.remote_field.set_value})

class BaseModel(models.Model):
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    create_time = models.DateTimeField(verbose_name='创建时间', auto_now_add=True)
    update_time = models.DateTimeField(verbose_name='更新时间', auto_now=True)

    # 如果是默认值则没删除,如果删除了,is_delete字段和主键相等
    is_delete = models.UUIDField(default=_DELETE_DEFAULT_VALUE, verbose_name='逻辑删除')

    objects = CustomManager().from_queryset(BaseQuerySet)()

    @cached_property
    def has_delete(self) -> bool:
        return self.id == self.is_delete

    class Meta:
        abstract = True
        
    def delete(self, using=None, keep_parents=False, force=False):
        if force:
            return super().delete(using, keep_parents)
        relates = get_candidate_relations_to_delete(self._meta)

        def get_related_queryset(related_model, field, instance):
            return related_model.objects.filter(**{field.name: instance.pk})

        for related in relates:
            process_relates(related, self, get_related_queryset)
        # 处理信号
        if not self._meta.auto_field:
            signals.pre_delete.send(sender=self.__class__, instance=self)

        self.is_delete = self.pk
        if not self._meta.auto_created:
            signals.post_delete.send(sender=self.__class__, instance=self)

        return self.save(update_fields=['is_delete'], using=using)



其中id使用的是UUIDField,如果使用AutoField,那么is_delete改成IntegerField,并指定默认值为-1
这里使用UUIDField,默认值使用的是_DELETE_DEFAULT_VALUE 常量(不安全,有待改进)
上面的处理方法可以解决CASECADE级联删除问题,还有信号也妥善处理了
CustomManager和BaseQuerySet在下文中

然后重写manager

class CustomManager(models.Manager):
    def __init__(self, *args, **kwargs):
        self.__add_is_del_filter = True
        super(CustomManager, self).__init__(*args, **kwargs)

    def get_queryset(self, *args, **kwargs):
        if self.__add_is_del_filter:
            return BaseQuerySet(self.model, using=self._db).exclude(is_delete=F('pk'))
        return super().get_queryset()

    def filter(self, *args, **kwargs):
        # 考虑是否主动传入is_delete
        if not kwargs.get('is_delete') is None:
            self.__add_is_del_filter = False
        return super(CustomManager, self).filter(*args, **kwargs)

重写QuerySet

class BaseQuerySet(models.QuerySet):
    def delete(self, force=False):
        if force:
            return super(BaseQuerySet, self).delete()
        if not self:
            return
        # self是一个QuerySet对象
        relates = get_candidate_relations_to_delete(self.model._meta)

        def get_related_queryset(related_model, field, instance):
            return related_model.objects.filter(query_utils.Q(**{'%s__in' % field.name: instance}))

        for related in relates:
            process_relates(related, self, get_related_queryset)

        def handle_signal(signal):
            if not self.model._meta.auto_created:
                for obj in self:
                    signal.send(
                        sender=self.model, instance=obj, using=self.using
                    )

        handle_signal(signals.pre_delete)
        self.update(is_delete=F('pk'))
        handle_signal(signals.post_delete)
        return self.count(), {self.model: self.count()}

之后每次新建模型继承BaseModel,即可实现逻辑删除,删除的时候直接queryset.delete()或者instance.delete(),因为重写了delete方法,所以使用序列化器也没问题

但是使用以上方法还是有点问题:
问题1:在使用django admin时,列表可能刷新后把 is_delete的项目也刷新出来了,再刷新一次又没有了,暂时无法定位问题
问题2:使用级联查询的时候有问题
比如说,一本书有多个作者,每个作者有多个电脑
我要查有电脑A的作者的书列表
Book.objects.filter(authors__computer=A)
那么你即使把book和author之间的关系逻辑删除了,也会被查出来,这个问题很严重,暂时无法解决,希望看了这篇文章的同学尝试复现一下,看能不能解决。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值