Подтвердить что ты не робот

Каковы варианты переопределения поведения каскадного удаления Django?

Модели Django обычно обрабатывают поведение ON DELETE CASCADE вполне адекватно (таким образом, что это работает с базами данных, которые не поддерживают его изначально).

Тем не менее, я изо всех сил пытаюсь обнаружить, что является лучшим способом переопределить это поведение, когда это не подходит, в следующих сценариях, например:

  • ON DELETE RESTRICT (т.е. удалять объект, если он имеет дочерние записи)

  • ON DELETE SET NULL (т.е. не удалять дочернюю запись, а вместо этого установите родительский ключ в NULL, чтобы разорвать связь)

  • Обновление других связанных данных при удалении записи (например, удаление загруженного файла изображения)

Ниже приводятся возможные способы достижения этих целей, о которых я знаю:

  • Отмените метод модели delete(). Несмотря на то, что этот вид работ, он обходит стороной, когда записи удаляются через QuerySet. Кроме того, каждая модель delete() должна быть переопределена, чтобы убедиться, что код Django никогда не вызывается и super() не может быть вызван, поскольку он может использовать QuerySet для удаления дочерних объектов.

  • Используйте сигналы. Это кажется идеальным, поскольку они вызываются при прямом удалении модели или удалении через QuerySet. Тем не менее, нет возможности предотвратить удаление дочернего объекта, поэтому его нельзя использовать для ВКЛЮЧЕНА КАСКАДЫ ИЛИ УСТАНОВЛЕННОГО НУЛЬТА.

  • Использовать механизм базы данных, который обрабатывает это правильно (что делает Django в этом случае?)

  • Подождите, пока Django не поддержит его (и будет жить с ошибками до тех пор...)

Кажется, что первый вариант - единственный жизнеспособный, но он уродливый, выкидывает ребенка с водой для ванны и рискует чего-то потерять при добавлении новой модели/отношения.

Я что-то упустил? Любые рекомендации?

4b9b3361

Ответ 1

Просто примечание для тех, кто сталкивается с этой проблемой, теперь есть встроенное решение в Django 1.3.

Подробнее см. в документации django.db.models.ForeignKey.on_delete Спасибо за редактор сайта Fragments of Code, чтобы указать его.

Простейший возможный сценарий просто добавляет в ваше определение определения FK модели:

on_delete=models.SET_NULL

Ответ 2

Django только эмулирует поведение CASCADE.

В соответствии с обсуждение в Django Users Group наиболее подходящими решениями являются:

  • Повторить ON DELETE SET NULL сценарий - вручную выполните obj.rel_set.clear() (для каждой связанной модели) до obj.delete().
  • Повторить ON DELETE RESTRICT сценарий - вручную проверить obj.rel_set пустым перед obj.delete().

Ответ 3

Хорошо, следующее решение, на котором я остановился, хотя и не удовлетворяет.

Я добавил абстрактный базовый класс для всех моих моделей:

class MyModel(models.Model):
    class Meta:
        abstract = True

    def pre_delete_handler(self):
        pass

Обработчик сигналов захватывает любые события pre_delete для подклассов этой модели:

def pre_delete_handler(sender, instance, **kwargs):
    if isinstance(instance, MyModel):
        instance.pre_delete_handler()
models.signals.pre_delete.connect(pre_delete_handler)

В каждой из моих моделей я имитирую любые отношения "ON DELETE RESTRICT", выбрасывая исключение из метода pre_delete_handler, если существует дочерняя запись.

class RelatedRecordsExist(Exception): pass

class SomeModel(MyModel):
    ...
    def pre_delete_handler(self):
        if children.count(): 
            raise RelatedRecordsExist("SomeModel has child records!")

Это отменяет удаление до изменения любых данных.

К сожалению, невозможно обновить данные в сигнале pre_delete (например, эмулировать ON DELETE SET NULL), поскольку список удаляемых объектов уже был создан Django до отправки сигналов. Django делает это, чтобы не застревать в круговых ссылках и предотвращать ненужную передачу сигнала объекту несколько раз.

Обеспечение того, что удаление может быть выполнено, теперь является ответственностью вызывающего кода. Чтобы помочь в этом, каждая модель имеет метод prepare_delete(), который заботится о настройке клавиш на NULL через self.related_set.clear() или аналогичных:

class MyModel(models.Model):
    ...
    def prepare_delete(self):
        pass

Чтобы избежать слишком большого количества кода в моих views.py и models.py, метод delete() переопределяется на MyModel для вызова prepare_delete():

class MyModel(models.Model):
    ...
    def delete(self):
        self.prepare_delete()
        super(MyModel, self).delete()

Это означает, что любые удаления, явно вызванные через obj.delete(), будут работать так, как ожидалось, но если удаление было каскадировано из связанного объекта или выполнено с помощью queryset.delete(), а код вызова не обеспечил, чтобы все ссылки были повреждены где необходимо, то pre_delete_handler выдаст исключение.

И, наконец, я добавил аналогичный метод post_delete_handler к моделям, которые вызываются по сигналу post_delete и позволяют модели очистить любые другие данные (например, удаляя файлы для ImageField s.)

class MyModel(models.Model):
     ...

    def post_delete_handler(self):
        pass

def post_delete_handler(sender, instance, **kwargs):
    if isinstance(instance, MyModel):
        instance.post_delete_handler()
models.signals.post_delete.connect(post_delete_handler)

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

Любые предложения о том, как улучшить это, более чем приветствуются.