防止在 Django 模型中删除

Posted

技术标签:

【中文标题】防止在 Django 模型中删除【英文标题】:Prevent delete in Django model 【发布时间】:2011-06-17 02:01:04 【问题描述】:

我有这样的设置(针对这个问题进行了简化):

class Employee(models.Model):
    name = models.CharField(name, unique=True)

class Project(models.Model):
    name = models.CharField(name, unique=True)
    employees = models.ManyToManyField(Employee)

当一个员工即将被删除时,我想检查他是否与任何项目相关联。如果是这样,删除应该是不可能的。

我知道信号以及如何使用它们。我可以连接到pre_delete 信号,并让它抛出像ValidationError 这样的异常。这可以防止删除,但不会被表单等优雅地处理。

这似乎是其他人会遇到的情况。我希望有人能指出一个更优雅的解决方案。

【问题讨论】:

这仅使用Python代码是不可行的;数据库本身也需要修改。 感谢您的评论。我首先在寻找 Python/Django 部分,看看它在我的应用程序中能走多远。 【参考方案1】:

我一直在寻找这个问题的答案,但找不到一个对models.Model.delete() 和QuerySet.delete() 都适用的好的答案。我继续前进,并在某种程度上实施了 Steve K 的解决方案。我使用此解决方案来确保不能以任何方式从数据库中删除对象(在此示例中为员工),而是将其设置为非活动状态。

这是一个迟到的答案.. 只是为了其他人看我把我的解决方案放在这里。

代码如下:

class CustomQuerySet(QuerySet):
    def delete(self):
        self.update(active=False)


class ActiveManager(models.Manager):
    def active(self):
        return self.model.objects.filter(active=True)

    def get_queryset(self):
        return CustomQuerySet(self.model, using=self._db)


class Employee(models.Model):
    name = models.CharField(name, unique=True)
    active = models.BooleanField(default=True, editable=False)

    objects = ActiveManager()

    def delete(self):
        self.active = False
        self.save()

用法:

Employee.objects.active() # use it just like you would .all()

或在管理员中:

class Employee(admin.ModelAdmin):

    def queryset(self, request):
        return super(Employee, self).queryset(request).filter(active=True)

【讨论】:

我不明白您是如何删除任何员工的,因为我知道您在没有对项目进行任何检查的情况下设置了标志,但是如果该员工不涉及任何项目,则该问题想要删除(或停用)员工您没有对此进行任何检查。 @MohsenTamiz 这个解决方案是关于在 Django 中防止删除的基本原则(和优雅的方式)。重写 delete 方法可以很容易地适应询问者的用例。 感谢您的回复,这可能是一个起点,但我以另一种方式这样做,对此有一些疑问。如果您能查看我的question 并获得一些反馈,我将不胜感激。 您建议的方法不适合删除,因为您必须检查查询集删除内的project 字段,或者您必须在查询集删除函数中为每个对象调用delete 方法在查询中,它们都不是大规模数据模型中可行且有效的方法(例如,当您必须检查许多ManyToMany 字段时) 当使用额外的查询检查约束时,这种方法显然不适合大型数据集。应根据场景谨慎优化此类场景。在 Django 中,任务服务器或原始查询之类的解决方案通常适用于这种情况。【参考方案2】:

对于那些在 ForeignKey 关系中引用此问题的人,正确的答案是在 ForeignKey 关系上使用 Djago 的 on_delete=models.PROTECT 字段。这将防止删除任何具有外键链接的对象。这不适用于ManyToManyField 关系(如this 问题中所述),但适用于ForeignKey 字段。

因此,如果模型是这样的,这将有助于防止删除 任何有一个或多个 Project 对象关联的 Employee 对象:

class Employee(models.Model):
    name = models.CharField(name, unique=True)

class Project(models.Model):
    name = models.CharField(name, unique=True)
    employees = models.ForeignKey(Employee, on_delete=models.PROTECT)

可以在HERE 找到文档。

【讨论】:

请注意,PROTECT 仍会运行查询以查找链接的对象,尽管它们不会被删除。【参考方案3】:

这将在我的应用程序中完成解决方案。一些代码是LWN's answer.

您的数据被删除有四种情况:

SQL 查询 在模型实例上调用delete()project.delete() 在 QuerySet 实例上调用 delete()Project.objects.all().delete() 已被其他模型上的 ForeignKey 字段删除

虽然您对第一种情况无能为力,但可以对其他三种情况进行细粒度控制。 一个建议是,在大多数情况下,您永远不应该删除数据本身,因为这些数据反映了我们应用程序的历史和使用情况。首选设置 active 布尔字段。

为了防止模型实例上出现delete(),请在模型声明中将delete() 子类化:

    def delete(self):
        self.active = False
        self.save(update_fields=('active',))

虽然 QuerySet 实例上的 delete() 需要像 LWN's answer. 那样使用自定义对象管理器进行一些设置

将其包装成一个可重用的实现:

class ActiveQuerySet(models.QuerySet):
    def delete(self):
        self.save(update_fields=('active',))


class ActiveManager(models.Manager):
    def active(self):
        return self.model.objects.filter(active=True)

    def get_queryset(self):
        return ActiveQuerySet(self.model, using=self._db)


class ActiveModel(models.Model):
    """ Use `active` state of model instead of delete it
    """
    active = models.BooleanField(default=True, editable=False)
    class Meta:
        abstract = True

    def delete(self):
        self.active = False
        self.save()

    objects = ActiveManager()

用法,只是子类ActiveModel类:

class Project(ActiveModel):
    ...

如果它的任何一个 ForeignKey 字段被删除,我们的对象仍然可以被删除:

class Employee(models.Model):
    name = models.CharField(name, unique=True)

class Project(models.Model):
    name = models.CharField(name, unique=True)
    manager = purchaser = models.ForeignKey(
        Employee, related_name='project_as_manager')

>>> manager.delete() # this would cause `project` deleted as well

这可以通过在 Model 字段中添加on_delete argument 来防止:

class Project(models.Model):
    name = models.CharField(name, unique=True)
    manager = purchaser = models.ForeignKey(
        Employee, related_name='project_as_manager',
        on_delete=models.PROTECT)

on_delete 的默认值为CASCADE,这将导致您的实例被删除,而使用PROTECT 将引发ProtectedErrorIntegrityError 的子类)。这样做的另一个目的是要保留数据的 ForeignKey 作为参考。

【讨论】:

这是一个很好的总结,但是当这个错误出现时会发生什么?删除员工会失败吗?我们如何允许删除但仍然让依赖关系消失并保护项目 @strangetimes 我现在也有同样的问题。 Django 将看到 PROTECT 并且从不检查“已删除”键,因此它将阻止删除任何引用 PROTECT 的内容。你有没有找到一种方法让它查询/检查已删除的字段然后继续前进?我有一种感觉,它只需要在引用模型的自定义删除检查中完成。【参考方案4】:

如果您知道永远不会有任何大规模的员工删除尝试,您可以在您的模型上覆盖 delete,并且仅在合法操作时调用 super

不幸的是,任何可能调用 queryset.delete() 的东西都将直接转到 SQL: http://docs.djangoproject.com/en/dev/topics/db/queries/#deleting-objects

但我认为这不是什么大问题,因为您是编写此代码的人,并且可以确保员工永远不会有任何queryset.delete()。手动拨打delete()

我希望删除员工的情况相对较少。

def delete(self, *args, **kwargs):
    if not self.related_query.all():
        super(MyModel, self).delete(*args, **kwargs)

【讨论】:

谢谢。我知道这一点,如果 pre_delete 信号不起作用,它可能会是我想要的解决方案。 +1 用于描述这一点的优缺点。 您可以通过编写 2 个类来处理批量删除:一个继承 models.Manager,另一个继承 models.query.QuerySet 第一个将覆盖 get_query_set,返回第二个类的实例。 QuerySet 派生类将覆盖 delete() 方法。此删除方法将遍历类实例并在每个项目上调用 delete()。希望这很清楚。【参考方案5】:

我想对LWN 和anhdat's 的答案提出另一种变体,其中我们使用deleted 字段而不是active 字段,并且我们从默认查询集中排除“已删除”对象,以便除非我们特别包含它们,否则将这些对象视为不再存在。

class SoftDeleteQuerySet(models.QuerySet):
    def delete(self):
        self.update(deleted=True)


class SoftDeleteManager(models.Manager):
    use_for_related_fields = True

    def with_deleted(self):
        return SoftDeleteQuerySet(self.model, using=self._db)

    def deleted(self):
        return self.with_deleted().filter(deleted=True)

    def get_queryset(self):
        return self.with_deleted().exclude(deleted=True)


class SoftDeleteModel(models.Model):
    """ 
    Sets `deleted` state of model instead of deleting it
    """
    deleted = models.NullBooleanField(editable=False)  # NullBooleanField for faster migrations with Postgres if changing existing models
    class Meta:
        abstract = True

    def delete(self):
        self.deleted = True
        self.save()

    objects = SoftDeleteManager()


class Employee(SoftDeleteModel):
    ...

用法:

Employee.objects.all()           # will only return objects that haven't been 'deleted'
Employee.objects.with_deleted()  # gives you all, including deleted
Employee.objects.deleted()       # gives you only deleted objects

如 anhdat 的回答中所述,请确保在模型上的 ForeignKeys 上设置 on_delete property 以避免级联行为,例如

class Employee(SoftDeleteModel):
    latest_project = models.ForeignKey(Project, on_delete=models.PROTECT)

注意:

我刚刚发现,django-model-utilsSoftDeletableModel 中包含类似的功能。值得一试。附带一些其他方便的东西。

【讨论】:

【参考方案6】:

我有一个建议,但我不确定它是否比您当前的想法更好。查看答案here 的一个遥远但并非不相关的问题,您可以通过基本上删除它们并使用您自己的来覆盖 django 管理员中的各种操作。因此,例如,他们在哪里:

def really_delete_selected(self, request, queryset):
    deleted = 0
    notdeleted = 0
    for obj in queryset:
        if obj.project_set.all().count() > 0:
            # set status to fail
            notdeleted = notdeleted + 1
            pass
        else:
            obj.delete()
            deleted = deleted + 1
    # ...

如果您没有像我一样使用 django admin,那么只需在您的 UI 逻辑中构建检查,然后再允许用户删除对象。

【讨论】:

谢谢。我没有为此使用 Django admin,尽管包含 Django admin 和自定义 UI 代码的解决方案会很棒。如果它只是 Django 管理员,那么您的解决方案和参考将非常好。为此 +1。【参考方案7】:

对于发现此问题并想知道如何将 PROTECT 添加到模型字段但让它忽略任何软删除对象的任何人,您可以通过简单地覆盖 Django 附带的 PROTECT 来做到这一点:

def PROTECT(collector, field, sub_objs, using):
if sub_objs.filter(deleted=False).count() > 0:
    raise ProtectedError(
        "Cannot delete some instances of model '%s' because they are "
        "referenced through a protected foreign key: '%s.%s'"
        % (
            field.remote_field.model.__name__,
            sub_objs[0].__class__.__name__,
            field.name,
        ),
        sub_objs.filter(deleted=False),
    )

这将检查是否有没有被软删除的对象,并且只返回错误中的那些对象。这还没有优化。

【讨论】:

【参考方案8】:

不敢相信我问这个问题已经 10 年了。类似的问题再次出现,我们最终将我们的解决方案打包在一个我们内部使用的小工具包中。它添加了一个与此处提出的问题相关的ProtectedModelMixin。见https://github.com/zostera/django-marina

【讨论】:

以上是关于防止在 Django 模型中删除的主要内容,如果未能解决你的问题,请参考以下文章

防止 Django 模板表单中的重定向

Django rest框架:防止一个用户删除/编辑/查看ModelViewSet中的其他用户

防止 Django 重新创建默认权限

防止django管理员通过创建LogEntry进行更改

Django模型基于两个变量防止重复[重复]

如何防止 Django 基本内联自动转义