2010-03-19 4 views
49

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

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

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

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

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

Ниже приведены возможные способы их достижения, что я знаю:

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

  • Использовать сигналы. Это кажется идеальным, поскольку они вызываются при прямом удалении модели или удалении через QuerySet. Тем не менее, нет возможности предотвратить удаление дочернего объекта, поэтому он не может использоваться в ON CASCADE RESTRICT или SET NULL.

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

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

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

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

ответ

60

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

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

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

on_delete=models.SET_NULL 
+8

+1 - согласно документации, установка 'on_delete' для полей ForeignKey потребуется также в Django 2.0. – whusterj

6

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

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

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

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

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

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() будут работать, как и ожидались, но если удаление имеет каскад из относится d или выполняется через 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) 

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

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