django目前模型是不带逻辑删除的,但是实际业务场景可能会用到逻辑删除,目前用以下方法实现
逻辑删除主要有两种方案,都是通过添加一个is_delete字段
方案1:添加is_delete字段为BooleanField,True代表删除,False代表没删除
方案2:添加is_delete字段与主键同类型,is_delete为默认值时表示未删除,is_delete==pk代表已删除
方案1存在unique问题,考虑以下模型:
School模型
字段名称 | 类型 | 是否唯一 |
---|---|---|
id | AutoField | yes |
name | CharField | yes |
avatar | UrlField | no |
如果添加is_delete为True/False 我添加两条记录(1,清华大学,www.baidu.com,True),(1,清华大学,www.baidu.com,False)会报错,即使添加unique_together=(name,is_delete)数据库里也最多出现上面两条记录,所以采用方案2
字段名称 | 类型 | 是否唯一 |
---|---|---|
id | AutoField | yes |
name | CharField | yes |
avatar | UrlField | no |
is_delete | IntegerField | no |
所有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之间的关系逻辑删除了,也会被查出来,这个问题很严重,暂时无法解决,希望看了这篇文章的同学尝试复现一下,看能不能解决。