防止在Django模型中删除

时间:2011-01-28 07:20:21

标签: python django django-models django-signals

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

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一样抛出异常。这样可以防止删除,但表单等不能正常处理。

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

6 个答案:

答案 0 :(得分:20)

我一直在寻找这个问题的答案,无法找到一个好的,这对两个models.Model.delete()和QuerySet.delete()都有效。我一起去实施Steve 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()

或在管理员中:

class Employee(admin.ModelAdmin):

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

答案 1 :(得分:5)

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

答案 2 :(得分:5)

这将从我的应用程序中的实现中结束解决方案。有些代码形式为LWN's answer.

有4种情况会删除您的数据:

  • SQL查询
  • 在模型实例上调用delete()project.delete()
  • 在QuerySet innstance上调用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:

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 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作为参考。

答案 3 :(得分:3)

我想就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的回答所述,请务必在模型上设置外键的on_delete property以避免级联行为,例如

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

注意:

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

答案 4 :(得分:2)

我有一个建议,但我不确定它比你现在的想法更好。看看答案here是否有一个遥远但不相关的问题,你可以在django admin中覆盖各种操作,基本上删除它们并使用你自己的操作。所以,例如,他们在哪里:

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逻辑中。

答案 5 :(得分:1)

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

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

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