Какие есть варианты переопределения каскадного удаления в Django? - PullRequest
55 голосов
/ 19 марта 2010

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

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

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

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

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

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

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

  • Используйте сигналы. Это кажется идеальным, поскольку они вызываются при прямом удалении модели или с помощью QuerySet. Однако невозможно предотвратить удаление дочернего объекта, поэтому его невозможно использовать в ОГРАНИЧЕННОМ ОГРАНИЧЕНИИ ОГРАНИЧЕНИЯ или SET NULL.

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

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

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

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

Ответы [ 3 ]

77 голосов
/ 09 декабря 2010

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

Подробности см. В документации. django.db.models.ForeignKey.on_delete Спасибо редактору сайта "Фрагменты кода", который указал на это.

Самый простой из возможных сценариев, просто добавьте в определение вашей модели поле FK:

on_delete=models.SET_NULL
6 голосов
/ 19 марта 2010

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

Согласно обсуждению в Django Users Group наиболее адекватными решениями являются:

  • Чтобы повторить сценарий ON DELETE SET NULL - вручную выполните obj.rel_set.clear () (для каждой связанной модели) перед obj.delete ().
  • Повторить сценарий «УДАЛИТЬ ОГРАНИЧЕНИЕ» - вручную проверьте, является ли obj.rel_set пустым перед obj.delete ().
4 голосов
/ 01 апреля 2010

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

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

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 с.)

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)

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

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

...