Запретить удаление в модели 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. Это предотвращает удаление, но формы и тому подобное не обрабатываются корректно.

Это похоже на ситуацию, с которой сталкивались другие. Я надеюсь, что кто-то может указать более элегантное решение.


person dyve    schedule 28.01.2011    source источник
comment
Это невозможно только с использованием кода Python; сама база данных также должна быть изменена.   -  person Ignacio Vazquez-Abrams    schedule 28.01.2011
comment
Спасибо за ваш комментарий. Сначала я ищу часть Python/Django и смотрю, как далеко это продвинет меня в моем приложении.   -  person dyve    schedule 28.01.2011


Ответы (6)


Для тех, кто ссылается на этот вопрос с той же проблемой с отношением 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)

Документацию можно найти ЗДЕСЬ.

person Colton Hicks    schedule 16.01.2018

Я искал ответ на эту проблему, но не смог найти хорошего, который бы работал как для models.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)
person Elwin    schedule 25.09.2013
comment
Я не понимаю, как вы удаляете любого сотрудника, как я понял, вы устанавливаете флаг без каких-либо проверок проектов, но вопрос хочет удалить (или деактивировать) сотрудника, если он не участвует ни в каких проектах, которые вы не проверяли. . - person motam; 03.05.2016
comment
@MohsenTamiz Это решение касается основного принципа (и элегантного способа) предотвращения удаления в Django. Переопределение метода удаления позволяет легко адаптировать его к варианту использования спрашивающего. - person Elwin; 03.05.2016
comment
Спасибо за ваш ответ, может быть, это отправная точка, но я сделал это по-другому и у меня есть некоторые вопросы по этому поводу. Буду признателен, если вы сможете проверить мой вопрос и дайте мне обратную связь. - person motam; 03.05.2016
comment
Предложенный вами метод не подходит для удаления, потому что вам нужно проверить поле project внутри удаления набора запросов или вам нужно вызвать метод delete в функции удаления набора запросов для каждого объекта в запросе, который ни один из них не является возможным и эффективным способом в крупномасштабных моделях данных (например, когда вы должны проверить много полей ManyToMany) - person motam; 03.05.2016
comment
Этот метод явно не подходит для больших наборов данных при использовании дополнительных запросов для проверки ограничений. Оптимизация для таких сценариев должна быть тщательно продумана в зависимости от сценария. В Django чаще всего в таком случае применяются такие решения, как сервер задач или необработанные запросы. - person Elwin; 04.05.2016
comment
Наконец, мне пришлось использовать этот метод для реализации удаления. Но я думаю, что использование промежуточной таблицы более эффективно, потому что она решает проблему на уровне базы данных, а не на уровне django. Но у меня была проблема с этим решением, что привело меня к вопросу, о котором я упоминал ранее. - person motam; 04.05.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 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 (подкласс IntegrityError). Другая цель этого заключается в том, что внешний ключ данных должен храниться в качестве ссылки.

person anhdat    schedule 07.07.2016
comment
это хорошее резюме, но что происходит, когда возникает эта ошибка? Не получится ли удалить сотрудника? Как мы разрешаем удаление, но при этом допускаем провал зависимости и защищаем проект - person strangetimes; 24.05.2017

Если вы знаете, что никогда не будет попыток массового удаления сотрудников, вы можете просто переопределить 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)
person Yuji 'Tomita' Tomita    schedule 28.01.2011
comment
Спасибо. Я знаю об этом, и, вероятно, это будет решение, которое я выберу, если сигнал pre_delete не сработает. +1 за описание этого с плюсами и минусами. - person dyve; 28.01.2011
comment
Вы можете позаботиться о массовых удалениях, написав 2 класса: один наследует models.Manager, а другой наследует models.query.QuerySet. Первый переопределит get_query_set, возвращая экземпляр второго класса. Производный класс QuerySet переопределит метод delete(). Этот метод удаления будет перебирать экземпляр класса и вызывать delete() для каждого элемента. Надеюсь, это понятно. - person Steve K; 03.10.2012

Я хотел бы предложить еще один вариант 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 как я только что обнаружил. Стоит проверить. Поставляется с некоторыми другими удобными вещами.

person Felix Böhme    schedule 15.11.2017

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

person Community    schedule 28.01.2011
comment
Спасибо. Я не использую для этого администратора Django, хотя решение, включающее как администратора Django, так и пользовательский код пользовательского интерфейса, было бы потрясающим. Если бы это был только администратор Django, ваше решение и ссылка были бы превосходными. +1 за это. - person dyve; 28.01.2011