Хорошо, вот решение, на котором я остановился, хотя оно далеко от удовлетворения.
Я добавил абстрактный базовый класс для всех своих моделей:
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)
Я надеюсь, что это кому-то поможет, и код можно будет перенести обратно во что-то более полезное без особых проблем.
Любые предложения о том, как улучшить это, приветствуются.