Запретить удаление в модели Django
У меня есть такая настройка (упрощенная для этого вопроса):
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
. Это предотвращает удаление, но оно не обрабатывается изящно формами и т.д.
Это похоже на ситуацию, с которой столкнулись другие. Я надеюсь, что кто-то может указать на более элегантное решение.
Ответы
Ответ 1
Я искал ответ на эту проблему, не смог найти хороший, который будет работать для обеих моделей. Model.delete() и QuerySet.delete(). Я пошел и, вроде, выполнил решение Стива К. Я использовал это решение, чтобы убедиться, что объект (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)
Ответ 2
Если вы знаете, что никаких попыток удаления массовых сотрудников никогда не будет, вы можете просто переопределить 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
Это приведет к завершению решения из реализации в моем приложении. Некоторый код представляет собой ответ LWN.
Есть четыре ситуации, когда ваши данные удаляются:
- SQL-запрос
- Вызов
delete()
в экземпляре модели: project.delete()
- Вызов
delete()
на основе QuerySet: Project.objects.all().delete()
- Удалено поле ForeignKey на другой модели
Хотя в первом случае вы ничего не можете сделать, остальные три могут контролироваться мелкозернистым.
Один из советов заключается в том, что в большинстве случаев вы никогда не должны удалять сами данные, поскольку эти данные отражают историю и использование нашего приложения. Настройка на active
Булево поле предпочтительнее.
Чтобы предотвратить delete()
в экземпляре Model, подкласс 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 в поле Model:
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 данных следует хранить в качестве ссылки.
Ответ 4
У меня есть предложение, но я не уверен, что это лучше вашей текущей идеи. Взглянув на ответ здесь для отдаленной, но не связанной с этим проблемы, вы можете переопределить в 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, как я, просто создайте эту проверку в логике пользовательского интерфейса, прежде чем разрешить пользователю удалять объект.
Ответ 5
Я хотел бы предложить еще один вариант ответов 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
как я только что открыл. Стоит проверить. Приходит с некоторыми другими удобными вещами.
Ответ 6
Для тех, кто ссылается на эти вопросы с той же проблемой с отношением ForeignKey
, правильным ответом будет использование поля Djago 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)
Документацию можно найти ЗДЕСЬ.