在Django模型中防止删除

zf9nrax1  于 2023-08-08  发布在  Go
关注(0)|答案(9)|浏览(101)

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

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即将被删除时,我想检查他是否连接到任何项目。如果是这样,删除就不可能了。
我知道信号和如何使用它们。我可以连接到pre_delete信号,让它抛出一个异常,比如ValidationError。这可以防止删除,但表单等无法正常处理。
这似乎是其他人会遇到的情况。我希望有人能指出一个更优雅的解决方案。

roqulrg3

roqulrg31#

我正在寻找这个问题的答案,但没有找到一个好的,这将适用于两个模型。Model.delete()和QuerySet.delete()。我沿着了,某种程度上,实现了史蒂夫·K的解决方案。我使用这个解决方案来确保一个对象(在本例中是Employee)不能以任何一种方式从数据库中删除,而是设置为非活动。
这是一个迟来的回答。只是为了让其他人看我把我的解决方案放在这里。
代码如下:

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()


或者在admin中:

class Employee(admin.ModelAdmin):

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

o7jaxewo

o7jaxewo2#

对于那些在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)

字符串
文档可以在这里找到。

jv2fixgn

jv2fixgn3#

这将结束从我的应用程序中的实现解决方案。有些代码是LWN's answer.
有4种情况下,您的数据被删除:

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

虽然对第一种情况没有什么可做的,但其他三种情况可以细粒度控制。一个建议是,在大多数情况下,您不应该删除数据本身,因为这些数据反映了我们应用程序的历史和使用情况。首选active布尔字段的设置。
要在Model示例上防止delete(),请在Model声明中子类化delete()

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

字符串
而QuerySet示例上的delete()需要像LWN's answer.中那样使用自定义对象管理器进行一些设置
将其 Package 为可重用的实现:

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


这可以通过添加模型字段的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将引发ProtectedErrorIntegrityError的子类)。这样做的另一个目的是数据的ForeignKey应该被保留作为引用。

piv4azn7

piv4azn74#

如果您知道永远不会有任何大规模的员工删除尝试,您可以在模型上覆盖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)

字符串

rqenqsqc

rqenqsqc5#

我想提出一个关于LWNanhdat'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属性,以避免级联行为,例如。

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

备注:

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

vsnjm48y

vsnjm48y6#

我有一个建议,但我不确定它是否比你现在的想法好。看一下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
    # ...

字符串
如果您不像我一样使用djangoadmin,那么在允许用户删除对象之前,只需在UI逻辑中构建检查即可。

n9vozmp4

n9vozmp47#

如果有人发现了这一点,并想知道如何将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),
    )

字符串
这将检查是否有任何未被软删除的对象,并仅返回错误中的那些对象。这还没有优化。

kzmpq1sx

kzmpq1sx8#

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

vuv7lop3

vuv7lop39#

我也遇到了同样的问题,只是找到了一个很好的解决方案。
有两种方法可以尝试删除模型示例:通过删除示例或通过在整个查询集上调用delete。您不必担心有人在管理器上调用delete,因为delete方法不在管理器中公开。引用Django文档:
请注意,delete()是唯一没有在Manager本身上公开的QuerySet方法。这是一种安全机制,可以防止您意外地请求Entry.objects.delete()并删除所有条目。如果您确实想删除所有对象,则必须显式请求完整的查询集:第一个月
为了防止示例级删除,您可以简单地覆盖模型类中的delete方法:

from django.db import models

class Undeletable(models.Model):
    # your fields here

    def delete(self, using=None, keep_parents=False):
        raise models.ProtectedError("You can't delete this model!", self)

字符串
现在来看下一个问题:查询集删除。为了解决这个问题,我们需要了解Django的一些事情:

  • 当调用创建另一个查询集的查询集的方法时,例如queryset.filter()或queryset.exclude(),将创建一个新的查询集,但其类型与旧的查询集的类型相同。因此,如果querysetq是MyQuerySet的示例,q.filter(#filters)将创建一个MyQuerySet类型的新对象
  • models.Manager类是models.Model类的默认管理器,它有一个attribute _queryset_class,用于确定从.all()或.filter()等方法返回的查询集类型
  • 为了创建一个带有custom _queryset_class的管理器类,您可以查看models.Manager类的源代码,这是models.Model类的默认管理器:
class Manager(BaseManager.from_queryset(QuerySet)):
    pass


因此,为了防止querysets批量删除我们的模型,我们可以使用以下代码:

from django.db import models

class UndeletableQueryset(models.QuerySet):
    def delete(self):
        raise models.ProtectedError("You can't delete this model!", self)

class Undeletable(models.Model):
    objects = models.manager.BaseManager.from_queryset(UndeletableQueryset)()
    
    # your fields here

    def delete(self, using=None, keep_parents=False):
        raise models.ProtectedError("You can't delete this model!", self)


就像这样,您已经阻止了所有可以删除模型示例的方式,并且在任何时候,每当有人试图删除它们时,您都会引发错误。
请注意,这种行为是如何通过过滤器的链接来保留的,并被排除,因为返回的每个查询集仍然是覆盖delete方法的UndeletableQuery集的示例。

相关问题