防止在 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
将引发ProtectedError
(IntegrityError
的子类)。这样做的另一个目的是要保留数据的 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-utils
的SoftDeletableModel
中包含类似的功能。值得一试。附带一些其他方便的东西。
【讨论】:
【参考方案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 模型中删除的主要内容,如果未能解决你的问题,请参考以下文章