在Django模型中防止删除

30 投票
9 回答
29705 浏览
提问于 2025-04-16 10:46

我有一个这样的设置(为了这个问题简化了):

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

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

当一个Employee(员工)快要被删除时,我想检查一下他是否和任何项目有联系。如果有的话,就不应该允许删除。

我知道有信号(signals)这个概念,也知道怎么使用它们。我可以连接到pre_delete信号,然后让它抛出一个像ValidationError这样的异常。这样就能阻止删除,但在表单等地方处理起来就不太优雅了。

这看起来是个其他人也可能遇到的情况。我希望有人能指出一个更优雅的解决方案。

9 个回答

12

这段内容总结了我在应用中实现的解决方案。一些代码来自于LWN的回答

你的数据可能会在以下四种情况下被删除:

  • 通过SQL查询
  • 在模型实例上调用delete()project.delete()
  • 在查询集实例上调用delete()Project.objects.all().delete()
  • 通过其他模型的外键字段删除

对于第一种情况,你几乎无能为力,但后面三种情况可以更细致地控制。一个建议是,在大多数情况下,你应该尽量不要直接删除数据,因为这些数据反映了我们应用的历史和使用情况。更好的做法是设置一个active的布尔字段来表示数据是否有效。

为了防止在模型实例上调用delete(),你可以在模型声明中重写delete()方法:

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

而在查询集实例上调用delete()则需要稍微设置一下,使用自定义对象管理器,就像在LWN的回答中提到的那样。

将这些内容整理成一个可重用的实现:

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):
    ...

不过,如果任何外键字段被删除,我们的对象仍然会被删除:

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

可以通过在模型字段中添加on_delete参数来防止这种情况:

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,则会引发ProtectedError(这是IntegrityError的一个子类)。这样做的另一个目的就是保持外键数据作为引用。

31

我在寻找这个问题的答案时,没能找到一个适合同时处理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)
21

对于那些遇到同样问题的人,涉及到ForeignKey关系时,正确的做法是使用Django的on_delete=models.PROTECT字段。这种设置可以防止删除任何与之有外键关联的对象。不过,这种方法不适用于ManyToManyField关系(在这个问题中有讨论),但对于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)

你可以在这里找到相关文档。

撰写回答