25

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

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. 这可以防止删除,但不会被表单等优雅地处理。

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

4

8 回答 8

28

我一直在寻找这个问题的答案,但找不到一个对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)
于 2013-09-25T14:25:31.013 回答
16

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

文档可以在这里找到。

于 2018-01-16T00:51:40.397 回答
9

这将从我的应用程序中的实现中总结出解决方案。一些代码是LWN 的答案。

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

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

虽然您对第一种情况无能为力,但可以对其他三种情况进行细粒度控制。一个建议是,在大多数情况下,您永远不应该删除数据本身,因为这些数据反映了我们应用程序的历史和使用情况。active而是首选设置布尔字段。

为了防止delete()模型实例,delete()在您的模型声明中进行子类化:

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

delete()在 QuerySet 实例上,需要像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):
    ...

如果它的任何一个 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)

默认 ofon_deleteCASCADE导致您的实例被删除,而使用PROTECT它会引发一个ProtectedError(的子类IntegrityError)。这样做的另一个目的是要保留数据的 ForeignKey 作为参考。

于 2016-07-07T10:58:45.113 回答
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)
于 2011-01-28T07:48:13.033 回答
5

我想对LWNanhdat 的答案提出另一种变体,其中我们使用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 的回答中所述,请确保在模型上设置ForeignKeyson_delete属性以避免级联行为,例如

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

笔记:

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

于 2017-11-15T22:27:35.263 回答
2

我有一个建议,但我不确定它是否比您当前的想法更好。在这里查看一个遥远但并非无关问题的答案,您可以在 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 逻辑中。

于 2011-01-28T07:44:51.340 回答
0

对于发现这个并想知道如何将 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),
    )

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

于 2021-11-17T06:28:31.710 回答
0

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

于 2021-11-18T07:50:39.157 回答