Django сигналы: полное руководство
Django сигналы - это мощный механизм, который позволяет выполнять код автоматически при определенных событиях в приложении. Они идеально подходят для автоматизации задач, которые должны происходить при изменениях в моделях.
Что такое Django сигналы?
Сигналы - это способ отправки уведомлений о том, что что-то произошло. Django предоставляет встроенные сигналы для различных событий жизненного цикла модели:
- pre_save - перед сохранением объекта
- post_save - после сохранения объекта
- pre_delete - перед удалением объекта
- post_delete - после удаления объекта
- m2m_changed - при изменении связей many-to-many
Базовое подключение к сигналам
Самый простой способ подключиться к сигналу - использовать декоратор @receiver
:
1from django.db.models.signals import post_save
2from django.dispatch import receiver
3from django.contrib.auth.models import User
4from .models import Profile
5
6@receiver(post_save, sender=User)
7def create_user_profile(sender, instance, created, **kwargs):
8 """Создает профиль пользователя при регистрации"""
9 if created:
10 Profile.objects.create(
11 user=instance,
12 bio='',
13 avatar='default.jpg'
14 )
15
16@receiver(post_save, sender=User)
17def save_user_profile(sender, instance, **kwargs):
18 """Сохраняет профиль при обновлении пользователя"""
19 try:
20 instance.profile.save()
21 except Profile.DoesNotExist:
22 Profile.objects.create(user=instance)
Практические примеры использования
1. Автоматическое создание связанных объектов
1from django.db.models.signals import post_save
2from django.dispatch import receiver
3from .models import Order, OrderNotification
4
5@receiver(post_save, sender=Order)
6def create_order_notification(sender, instance, created, **kwargs):
7 """Создает уведомление при создании заказа"""
8 if created:
9 OrderNotification.objects.create(
10 order=instance,
11 message=f'Создан новый заказ #{instance.id}',
12 notification_type='order_created'
13 )
2. Логирование изменений
1from django.db.models.signals import post_save, post_delete
2from django.dispatch import receiver
3from django.contrib.admin.models import LogEntry, ADDITION, CHANGE, DELETION
4from .models import Product
5
6@receiver(post_save, sender=Product)
7def log_product_changes(sender, instance, created, **kwargs):
8 """Логирует изменения в продуктах"""
9 if created:
10 LogEntry.objects.log_action(
11 user_id=1, # ID пользователя, выполняющего действие
12 content_type_id=ContentType.objects.get_for_model(Product).pk,
13 object_id=instance.pk,
14 object_repr=str(instance),
15 action_flag=ADDITION,
16 change_message="Продукт создан"
17 )
18 else:
19 LogEntry.objects.log_action(
20 user_id=1,
21 content_type_id=ContentType.objects.get_for_model(Product).pk,
22 object_id=instance.pk,
23 object_repr=str(instance),
24 action_flag=CHANGE,
25 change_message="Продукт обновлен"
26 )
3. Валидация перед удалением
1from django.db.models.signals import pre_delete
2 from django.dispatch import receiver
3 from django.core.exceptions import ValidationError
4 from .models import Category
5
6 @receiver(pre_delete, sender=Category)
7 def prevent_category_deletion(sender, instance, **kwargs):
8 """Предотвращает удаление категории с продуктами"""
9 if instance.products.exists():
10 raise ValidationError(
11 f'Нельзя удалить категорию "{instance.name}" - в ней есть продукты'
12 )
4. Обработка связей many-to-many
1from django.db.models.signals import m2m_changed
2 from django.dispatch import receiver
3 from .models import Article, Tag
4
5 @receiver(m2m_changed, sender=Article.tags.through)
6 def handle_article_tags_change(sender, instance, action, pk_set, **kwargs):
7 """Обрабатывает изменения в тегах статьи"""
8 if action == "post_add":
9 # Обновляем счетчик тегов
10 for tag_id in pk_set:
11 tag = Tag.objects.get(id=tag_id)
12 tag.usage_count += 1
13 tag.save()
14 elif action == "post_remove":
15 # Уменьшаем счетчик тегов
16 for tag_id in pk_set:
17 tag = Tag.objects.get(id=tag_id)
18 tag.usage_count = max(0, tag.usage_count - 1)
19 tag.save()
Отключение сигналов
Иногда нужно временно отключить сигналы, например, при массовых операциях:
1from django.db.models.signals import post_save
2 from django.dispatch import receiver
3
4 # Временно отключаем сигнал
5 post_save.disconnect(create_user_profile, sender=User)
6
7 # Выполняем массовые операции
8 User.objects.bulk_create([
9 User(username='user1', email='user1@example.com'),
10 User(username='user2', email='user2@example.com'),
11 ])
12
13 # Включаем сигнал обратно
14 post_save.connect(create_user_profile, sender=User)
Сигналы в Django REST Framework
При работе с DRF можно использовать сигналы для автоматической обработки:
1from django.db.models.signals import post_save
2 from django.dispatch import receiver
3 from rest_framework.authtoken.models import Token
4 from django.contrib.auth.models import User
5
6 @receiver(post_save, sender=User)
7 def create_auth_token(sender, instance=None, created=False, **kwargs):
8 """Создает токен аутентификации для новых пользователей"""
9 if created:
10 Token.objects.create(user=instance)
Лучшие практики
- Используй сигналы умеренно - они могут усложнить отладку
- Избегай сложной логики в обработчиках сигналов
- Обрабатывай исключения в обработчиках сигналов
- Документируй все обработчики сигналов
- Тестируй логику сигналов отдельно
Тестирование сигналов
Вот пример теста для проверки работы сигналов:
1from django.test import TestCase
2 from django.contrib.auth.models import User
3 from .models import Profile
4
5 class SignalTestCase(TestCase):
6 def test_profile_creation_signal(self):
7 """Тестирует создание профиля при создании пользователя"""
8 user = User.objects.create_user(
9 username='testuser',
10 email='test@example.com',
11 password='testpass123'
12 )
13
14 # Проверяем, что профиль был создан автоматически
15 self.assertTrue(hasattr(user, 'profile'))
16 self.assertIsInstance(user.profile, Profile)
17 self.assertEqual(user.profile.user, user)
Отладка сигналов
Для отладки сигналов можно использовать логирование:
1import logging
2 from django.db.models.signals import post_save
3 from django.dispatch import receiver
4
5 logger = logging.getLogger(__name__)
6
7 @receiver(post_save, sender=User)
8 def debug_user_save(sender, instance, created, **kwargs):
9 """Логирует все сохранения пользователей для отладки"""
10 if created:
11 logger.info(f'Создан новый пользователь: {instance.username}')
12 else:
13 logger.info(f'Обновлен пользователь: {instance.username}')
FAQ
Q: Когда использовать сигналы?
A: Для автоматического выполнения кода при изменениях в моделях: создание профилей, логирование, отправка уведомлений, валидация.
Q: Можно ли использовать сигналы для бизнес-логики?
A: Лучше избегать сложной бизнес-логики в сигналах. Используй их для простых автоматических действий.
Q: Как отключить сигнал для конкретной модели?
A: Используй disconnect()
с указанием sender и конкретного обработчика.
Q: Сигналы работают в транзакциях?
A: Да, сигналы выполняются в рамках транзакции. Если транзакция откатывается, изменения от сигналов тоже откатываются.
Q: Какой порядок выполнения сигналов?
A: Сигналы выполняются в порядке их подключения. Используй dispatch_uid
для контроля порядка.
Заключение
Django сигналы - это мощный инструмент для автоматизации задач. Они позволяют создавать гибкие и расширяемые приложения, но требуют осторожного использования. Следуй лучшим практикам, тестируй свою логику и документируй все обработчики сигналов.