Django: сигнал или метод модели?

Когда нужно написать какой-либо функционал, который должен быть выполнен при сохранении django модели, у меня всегда возникал вопрос - где его лучше реализовать. В сигнале или в методе save() модели? Давайте разберемся, что и в каких ситуациях более удобно.

В каких случаях использовать методы модели save(), delete()?

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

Часто приводят аргумент в пользу сигналов, что якобы их удобно использовать в похожих случаях. Т.е. один и тот же сигнал можно прикрепить к разным моделям. Аргумент для меня довольно странный, ведь точно так же можно объявить функцию или метод в классе-миксине и использовать их в save().

В принципе, все это можно сделать и в сигналах, почему я предпочитаю метод save? Ответ простой - это нагляднее. Когда вы смотрите на код модели, вы сразу видите, что будет происходить что-то при ее сохранении. В случае сигналов, особенно если нет четкого правила, где они объявлены, логика часто ускользает из виду.

Следует отметить, что сигналы на удаление pre_delete, post_delete имеют то преимущество над методом delete(), что они вызываются при каскадном удалении объектов и при удалении всего queryset’а, чего не происходит в случае с методом модели. Тут нужно смотреть по ситуации, возможно массовым удалением можно пренебречь.

А вот при массовом создании или обновлении объектов не вызывается ни метод модели save(), ни сигналы pre_save, post_save. Тут они равнозначны. Да, если вы переопределяете save() или delete(), не забудьте вызывать метод родительского класса.

Когда лучше использовать сигналы?

Сигналы намного удобнее, если вы создаете переиспользуемое приложение. Тогда пользователи вашего приложения могут легко прикрепить ваши сигналы к своим моделям, не меняя код этих моделей.

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

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

Например, есть аппа пользователей и аппа отчетов. При создании пользователя вам нужно автоматически создать отчет. В этом случае я предпочитаю создать сигнал в той аппе, к которой относится функционал, т.е. в приложении с отчетами.

Почему так?

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

Где объявлять сигналы и где их прикреплять?

Как советует документация django (секция “Where should this code live?”), сигналы лучше хранить не в моделях и не в __init__.py, а в отдельном подмодуле signals приложения. Это уберет головную боль с импортами.

Но чтобы сигналы прикрепились, должен быть исполнен код, который их прикрепляет. Когда мы объявляем их в модуле с моделями, то код импортируется автоматически. Однако если код с сигналами объявлен в другом месте - он автоматически не выполнится. Поэтому нужно использовать метод ready() конфигурационного класса приложения.

В целом, я следую рекомендации из этого ответа на stackoverflow. Приведу пример кода для уже упомянутого случая, когда есть приложение с отчетами (report) и нам нужно создавать отчет при создании нового пользователя.

  1. Создаем в приложении подмодуль signals, в котором будет файл handlers.py

     reports/signals/__init__.py
     reports/signals/handlers.py
    
  2. Объявляем наши сигналы именно в файле handlers.py

     from django.db.models.signals import post_save
     from django.dispatch import receiver
     from django.contrib.auth import get_user_model
    
     from reports.models import Report
    
     User = get_user_model()
    
     @receiver(post_save, sender=User)
     def create_user_report(sender, instance, created, **kwargs):
         if created:
             Report.objects.create(user=instance)
    
  3. Создаем класс конфигурации приложения

     reports/apps.py
    

    С кодом:

     from django.apps import AppConfig
    
     class ReportsConfig(AppConfig):
         name = 'reports'
         verbose_name = 'Reports'
    
         def ready(self):
             import reports.signals.handlers  # noqa
    

    Таким образом мы прикрепили сигнал. В данном случаем мы использовали декоратор @receiver, поэтому нам достаточно просто сделать импорт. Вместо декоратора тут можно было явно вызвать метод connect сигнала. Кому что больше нравится.

    Не забываем указать, что наш класс ReportsConfig - это конфиг приложения. Для этого в reports/__init__.py добавляем строку:

     default_app_config = 'reports.apps.ReportsConfig'
    

    Либо указываем явно ReportsConfig в settings.INSTALLED_APPS. Смотри доки django.

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