Django удалить посторонний объект?

Если мы настроим профиль, который рекомендует Django:

class Profile(models.Model):
    user = models.ForeignKey(User, unique=True)

Затем, когда вы удаляете объект User из администратора Django, он также удаляет свой профиль. Это связано с тем, что профиль имеет внешний ключ для пользователя и он хочет защитить ссылочную целостность. Тем не менее, я хочу эту функциональность, даже если указатель идет в другую сторону. Например, в моем классе Profile у меня есть:

shipper = models.ForeignKey(Shipper, unique=True, blank=True, null=True)
carrier = models.ForeignKey(Carrier, unique=True, blank=True, null=True)
affiliat = models.ForeignKey(Affiliate, unique=True, blank=True, null=True, verbose_name='Affiliate')

И я хочу, чтобы, если вы удалите Profile, он удалит связанные объекты отправителя/перевозчика/партнера (не спрашивайте меня, почему Django сделал "affiliate" какое-то странное ключевое слово). Поскольку грузоотправители, перевозчики и ветки являются типами пользователей, и им не имеет смысла существовать без остальной части данных (никто не сможет войти в систему как один).

Причина, по которой я не клал ключи на другие объекты, заключается в том, что тогда Django пришлось бы внутренне присоединяться ко всем этим таблицам каждый раз, когда я хотел бы проверить, какой тип пользователя был...

Ответы

Ответ 1

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

Я предпочитаю вышеприведенный метод выше, однако пример, приведенный Феликс, имеет один фатальный недостаток; функция delete(), которая является переопределяющей, выглядит следующим образом:

def delete(self, using=None):
    using = using or router.db_for_write(self.__class__, instance=self)
    assert self._get_pk_val() is not None, "%s object can't be deleted because its %s attribute is set to None." % (self._meta.object_name, self._meta.pk.attname)

    collector = Collector(using=using)
    collector.collect([self])
    collector.delete()

Обратите внимание на параметр "using", в большинстве случаев мы вызываем delete() с пустыми аргументами, чтобы мы, возможно, даже знали, что он есть. В приведенном выше примере этот параметр похож на нас, переопределяя и не смотря на функциональность суперкласса, если кто-то, кто передаст параметр "использование" при удалении профиля, вызовет неожиданное поведение. Чтобы этого избежать, мы обязательно сохраним аргумент вместе с его значением по умолчанию:

class Profile(models.Model):
# ...

def delete(self, using=None):
    if self.shipper:
        self.shipper.delete()
    if self.carrier:
        self.carrier.delete()
    if self.affiliat:
        self.affiliat.delete()
    super(Profile, self).delete(using)

Однако одна ошибка заключается в том, что delete() не получает явного вызова в записи db при массовых ударах, это означает, что если вы захотите удалить несколько профилей за один раз и сохраните переопределение (например, вызов .delete() в наборе запросов django), вам нужно будет либо использовать сигнал удаления (как описано bernardo), либо вам потребуется перебирать каждую запись, удаляя их по отдельности (дорого и уродливо).

Ответ 2

Вы можете переопределить метод delete() класса Profile и удалить другие объекты в этом методе перед удалением фактического профиля.

Что-то вроде:

class Profile(models.Model):
    # ...

    def delete(self):
        if self.shipper:
            self.shipper.delete()
        if self.carrier:
            self.carrier.delete()
        if self.affiliat:
            self.affiliat.delete()
        super(Profile, self).delete()

Ответ 3

Лучший способ сделать это, и это работает с методом удаления объекта и методом удаления запроса, использует сигнал post_delete, как вы можете видеть в документации.

В вашем случае ваш код будет очень похож на этот:

from django.db import models
from django.dispatch import receiver

@receiver(models.signals.post_delete, sender=Profile)
def handle_deleted_profile(sender, instance, **kwargs):
    if instance.shipper:
        instance.shipper.delete()
    if instance.carrier:
        instance.carrier.delete()
    if instance.affiliat:
        instance.affiliat.delete()

Это работает только для Django 1.3 или выше, потому что в этой версии Django был добавлен сигнал post_delete.