Предотвратить удаление в модели Django - PullRequest
14 голосов
/ 28 января 2011

У меня есть такая настройка (упрощенно для этого вопроса):

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 ]

20 голосов
/ 25 сентября 2013

Я искал ответ на эту проблему, не смог найти хорошего, который бы работал для обеих моделей. Model.delete () и QuerySet.delete ().Я пошел дальше и вроде как внедрил решение Стива К.Я использовал это решение, чтобы гарантировать, что объект (сотрудник в этом примере) не может быть удален из базы данных, в любом случае, но установлен как неактивный.

Это поздний ответ .. просто радидругих людей, ищущих, я выкладываю свое решение здесь.

Вот код:

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)
5 голосов
/ 07 июля 2016

Это будет решение из реализации в моем приложении.Код выглядит следующим образом: 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:

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 вместо этого будет поднят ProtectedError (подкласс * 1053).*).Другая цель этого состоит в том, что ForeignKey данных должен быть сохранен как ссылка.

5 голосов
/ 28 января 2011

Если вы знаете, что никогда не будет массовых попыток удаления сотрудника, вы можете просто переопределить 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)
3 голосов
/ 16 ноября 2017

Я хотел бы предложить еще один вариант ответов LWN и anhdat , в котором мы используем поле 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 свойство для ForeignKeys в вашей модели, чтобы избежать каскадного поведения, например

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

Примечание:

Аналогичные функции включеныв django-model-utils SoftDeletableModel, как я только что обнаружил.Стоит проверить.Поставляется с некоторыми другими полезными вещами.

2 голосов
/ 16 января 2018

Для тех, кто ссылается на этот вопрос с той же проблемой с отношением ForeignKey, правильным ответом будет использование поля on_delete=models.PROTECT Дьяго в отношении 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)

Документация может быть найдена ЗДЕСЬ .

2 голосов
/ 28 января 2011

У меня есть предложение, но я не уверен, что оно лучше вашей текущей идеи. Взглянув на ответ здесь для отдаленной, но не связанной с этим проблемы, вы можете переопределить в администраторе 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, как я, просто встроите эту проверку в логику вашего интерфейса, прежде чем разрешить пользователю удалять объект.

...