在Django模型中防止删除
我有一个这样的设置(为了这个问题简化了):
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 个回答
这段内容总结了我在应用中实现的解决方案。一些代码来自于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
的一个子类)。这样做的另一个目的就是保持外键数据作为引用。
我在寻找这个问题的答案时,没能找到一个适合同时处理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)
对于那些遇到同样问题的人,涉及到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)
你可以在这里找到相关文档。