Сигналы pre_save и post_save
Эти сигналы позволяют выполнять код до и после сохранения объектов модели. Они являются мощным инструментом для автоматизации бизнес-логики и поддержания целостности данных.
Основы работы с сигналами
Django предоставляет несколько типов сигналов для моделей:
- pre_save - выполняется перед сохранением объекта
- post_save - выполняется после сохранения объекта
- pre_delete - выполняется перед удалением объекта
- post_delete - выполняется после удаления объекта
- m2m_changed - выполняется при изменении ManyToMany отношений
Простой пример с pre_save
1from django.db.models.signals import pre_save
2from django.dispatch import receiver
3from django.utils.text import slugify
4from .models import Article, Book, Product
5
6@receiver(pre_save, sender=Article)
7def article_pre_save(sender, instance, **kwargs):
8 """Автоматическое создание slug для статьи"""
9 if not instance.slug:
10 instance.slug = slugify(instance.title)
11
12 # Автоматическое обновление поля updated_at
13 from django.utils import timezone
14 instance.updated_at = timezone.now()
15
16 # Логирование изменений
17 if instance.pk: # Обновление существующего объекта
18 try:
19 old_instance = Article.objects.get(pk=instance.pk)
20 if old_instance.title != instance.title:
21 print(f'Заголовок изменен с "{old_instance.title}" на "{instance.title}"')
22 except Article.DoesNotExist:
23 pass
24
25@receiver(pre_save, sender=Book)
26def book_pre_save(sender, instance, **kwargs):
27 """Автоматическое форматирование названия книги"""
28 # Приводим название к правильному формату
29 instance.title = instance.title.title()
30
31 # Автоматическое вычисление цены со скидкой
32 if instance.discount_percent > 0:
33 instance.final_price = instance.price * (1 - instance.discount_percent / 100)
34 else:
35 instance.final_price = instance.price
36
37 # Валидация данных
38 if instance.price < 0:
39 raise ValueError("Цена не может быть отрицательной")
40
41 if instance.discount_percent > 100:
42 raise ValueError("Скидка не может превышать 100%")
43
44@receiver(pre_save, sender=Product)
45def product_pre_save(sender, instance, **kwargs):
46 """Автоматическое обновление статуса продукта"""
47 # Если остаток равен 0, помечаем как недоступный
48 if instance.stock == 0:
49 instance.is_available = False
50 elif instance.stock > 0 and not instance.is_available:
51 instance.is_available = True
52
53 # Автоматическое обновление поля last_updated
54 from django.utils import timezone
55 instance.last_updated = timezone.now()
Сложные сценарии с post_save
1from django.db.models.signals import post_save
2from django.dispatch import receiver
3from django.core.cache import cache
4from django.core.mail import send_mail
5from django.template.loader import render_to_string
6from .models import User, Order, Notification, UserProfile
7
8@receiver(post_save, sender=User)
9def user_post_save(sender, instance, created, **kwargs):
10 """Автоматические действия после создания/обновления пользователя"""
11 if created:
12 # Создаем профиль пользователя
13 UserProfile.objects.create(
14 user=instance,
15 bio=f"Профиль пользователя {instance.username}",
16 avatar="default_avatar.png"
17 )
18
19 # Отправляем приветственное письмо
20 try:
21 context = {
22 'username': instance.username,
23 'email': instance.email
24 }
25 html_message = render_to_string(
26 'emails/welcome.html',
27 context
28 )
29
30 send_mail(
31 subject='Добро пожаловать!',
32 message=f'Привет, {instance.username}!',
33 from_email='noreply@example.com',
34 recipient_list=[instance.email],
35 html_message=html_message
36 )
37 except Exception as e:
38 # Логируем ошибку, но не прерываем процесс
39 print(f"Ошибка отправки email: {e}")
40
41 # Создаем уведомление
42 Notification.objects.create(
43 user=instance,
44 title="Добро пожаловать!",
45 message="Ваш аккаунт успешно создан.",
46 type="welcome"
47 )
48
49 # Очищаем кеш пользователя
50 cache_key = f'user_profile_{instance.id}'
51 cache.delete(cache_key)
52
53@receiver(post_save, sender=Order)
54def order_post_save(sender, instance, created, **kwargs):
55 """Обработка заказов после сохранения"""
56 if created:
57 # Отправляем уведомление о новом заказе
58 Notification.objects.create(
59 user=instance.user,
60 title="Новый заказ",
61 message=f"Ваш заказ #{instance.id} принят в обработку.",
62 type="order_created"
63 )
64
65 # Отправляем email администратору
66 try:
67 admin_emails = User.objects.filter(
68 is_staff=True
69 ).values_list('email', flat=True)
70
71 if admin_emails:
72 send_mail(
73 subject=f'Новый заказ #{instance.id}',
74 message=f'Получен новый заказ на сумму {instance.total_amount}',
75 from_email='noreply@example.com',
76 recipient_list=list(admin_emails)
77 )
78 except Exception as e:
79 print(f"Ошибка отправки email администратору: {e}")
80
81 # Обновляем статистику пользователя
82 try:
83 profile = instance.user.userprofile
84 profile.total_orders += 1
85 profile.total_spent += instance.total_amount
86 profile.save(update_fields=['total_orders', 'total_spent'])
87 except Exception as e:
88 print(f"Ошибка обновления статистики: {e}")
89
90 # Очищаем кеш заказов
91 cache.delete(f'user_orders_{instance.user.id}')
Сигналы с условной логикой
1from django.db.models.signals import pre_save, post_save
2from django.dispatch import receiver
3from django.db.models import F
4from .models import Comment, Post, UserActivity
5
6@receiver(pre_save, sender=Comment)
7def comment_pre_save(sender, instance, **kwargs):
8 """Предварительная обработка комментария"""
9 # Автоматическое создание slug для комментария
10 if not instance.slug:
11 from django.utils.text import slugify
12 instance.slug = slugify(f"{instance.post.title}-{instance.author.username}")
13
14 # Проверка на спам (простая логика)
15 spam_words = ['spam', 'реклама', 'купить', 'заказать']
16 content_lower = instance.content.lower()
17 if any(word in content_lower for word in spam_words):
18 instance.is_moderated = False
19 instance.moderation_note = "Автоматически помечен как спам"
20 else:
21 instance.is_moderated = True
22
23@receiver(post_save, sender=Comment)
24def comment_post_save(sender, instance, created, **kwargs):
25 """Обработка комментария после сохранения"""
26 if created:
27 # Увеличиваем счетчик комментариев в посте
28 Post.objects.filter(id=instance.post.id).update(
29 comment_count=F('comment_count') + 1
30 )
31
32 # Создаем запись активности
33 UserActivity.objects.create(
34 user=instance.author,
35 action="comment_created",
36 target_model="Post",
37 target_id=instance.post.id,
38 description=f"Оставил комментарий к посту '{instance.post.title}'"
39 )
40
41 # Отправляем уведомление автору поста
42 if instance.author != instance.post.author:
43 Notification.objects.create(
44 user=instance.post.author,
45 title="Новый комментарий",
46 message=f"{instance.author.username} оставил комментарий к вашему посту",
47 type="new_comment"
48 )
49
50 # Обновляем статистику пользователя
51 user = instance.author
52 user.comment_count = user.comments.count()
53 user.save(update_fields=['comment_count'])
54
55@receiver(post_save, sender=Post)
56def post_post_save(sender, instance, created, **kwargs):
57 """Обработка поста после сохранения"""
58 if created:
59 # Создаем запись активности
60 UserActivity.objects.create(
61 user=instance.author,
62 action="post_created",
63 target_model="Post",
64 target_id=instance.id,
65 description=f"Создал пост '{instance.title}'"
66 )
67
68 # Отправляем уведомления подписчикам
69 followers = instance.author.followers.all()
70 for follower in followers:
71 Notification.objects.create(
72 user=follower,
73 title="Новый пост",
74 message=f"{instance.author.username} опубликовал новый пост",
75 type="new_post"
76 )
77
78 # Обновляем статистику автора
79 author = instance.author
80 author.post_count = author.posts.count()
81 author.save(update_fields=['post_count'])
Сигналы с транзакциями и обработкой ошибок
1from django.db.models.signals import pre_save, post_save
2from django.dispatch import receiver
3from django.db import transaction
4from django.core.exceptions import ValidationError
5import logging
6
7logger = logging.getLogger(__name__)
8
9@receiver(pre_save, sender=Order)
10def order_pre_save(sender, instance, **kwargs):
11 """Валидация заказа перед сохранением"""
12 try:
13 # Проверяем наличие товаров
14 if instance.items.count() == 0:
15 raise ValidationError("Заказ должен содержать хотя бы один товар")
16
17 # Проверяем достаточность средств
18 if instance.user.balance < instance.total_amount:
19 raise ValidationError("Недостаточно средств на счете")
20
21 # Автоматическое вычисление скидки
22 if instance.total_amount > 10000:
23 instance.discount = 0.1 # 10% скидка
24 elif instance.total_amount > 5000:
25 instance.discount = 0.05 # 5% скидка
26 else:
27 instance.discount = 0
28
29 # Применяем скидку
30 instance.final_amount = instance.total_amount * (1 - instance.discount)
31
32 except Exception as e:
33 logger.error(f"Ошибка валидации заказа: {e}")
34 raise
35
36@receiver(post_save, sender=Order)
37def order_post_save(sender, instance, created, **kwargs):
38 """Обработка заказа после сохранения с транзакциями"""
39 if created:
40 try:
41 with transaction.atomic():
42 # Списываем средства с баланса пользователя
43 user = instance.user
44 user.balance -= instance.final_amount
45 user.save(update_fields=['balance'])
46
47 # Создаем запись о транзакции
48 Transaction.objects.create(
49 user=user,
50 amount=-instance.final_amount,
51 type="order_payment",
52 description=f"Оплата заказа #{instance.id}"
53 )
54
55 # Обновляем статистику продаж
56 for item in instance.items.all():
57 product = item.product
58 product.sold_count += item.quantity
59 product.stock -= item.quantity
60 product.save(update_fields=['sold_count', 'stock'])
61
62 # Отправляем уведомления
63 self.send_order_notifications(instance)
64
65 except Exception as e:
66 logger.error(f"Ошибка обработки заказа {instance.id}: {e}")
67 # Можно отправить уведомление администратору
68 self.notify_admin_about_error(instance, str(e))
69
70def send_order_notifications(self, order):
71 """Отправка уведомлений о заказе"""
72 try:
73 # Уведомление пользователю
74 Notification.objects.create(
75 user=order.user,
76 title="Заказ подтвержден",
77 message=f"Ваш заказ #{order.id} успешно оформлен",
78 type="order_confirmed"
79 )
80
81 # Email подтверждения
82 context = {
83 'order': order,
84 'user': order.user
85 }
86 html_message = render_to_string(
87 'emails/order_confirmation.html',
88 context
89 )
90
91 send_mail(
92 subject=f'Подтверждение заказа #{order.id}',
93 message=f'Ваш заказ на сумму {order.final_amount} подтвержден',
94 from_email='noreply@example.com',
95 recipient_list=[order.user.email],
96 html_message=html_message
97 )
98
99 except Exception as e:
100 logger.error(f"Ошибка отправки уведомлений: {e}")
101
102def notify_admin_about_error(self, order, error_message):
103 """Уведомление администратора об ошибке"""
104 try:
105 admin_users = User.objects.filter(is_staff=True)
106 for admin in admin_users:
107 Notification.objects.create(
108 user=admin,
109 title="Ошибка обработки заказа",
110 message=f"Ошибка в заказе #{order.id}: {error_message}",
111 type="admin_error"
112 )
113 except Exception as e:
114 logger.error(f"Ошибка уведомления администратора: {e}")
Сигналы для кеширования и оптимизации
1from django.db.models.signals import post_save, post_delete
2from django.dispatch import receiver
3from django.core.cache import cache
4from django.db.models import Count, Avg
5from .models import Product, Category, Review
6
7@receiver(post_save, sender=Product)
8def product_post_save(sender, instance, created, **kwargs):
9 """Обновление кеша после изменения продукта"""
10 # Очищаем кеш продукта
11 cache.delete(f'product_{instance.id}')
12 cache.delete(f'product_slug_{instance.slug}')
13
14 # Очищаем кеш категории
15 if instance.category:
16 cache.delete(f'category_products_{instance.category.id}')
17 cache.delete(f'category_stats_{instance.category.id}')
18
19 # Очищаем общий кеш продуктов
20 cache.delete('all_products')
21 cache.delete('featured_products')
22
23 # Обновляем статистику категории
24 if instance.category:
25 self.update_category_stats(instance.category)
26
27@receiver(post_delete, sender=Product)
28def product_post_delete(sender, instance, **kwargs):
29 """Очистка кеша после удаления продукта"""
30 # Очищаем все связанные кеши
31 cache.delete(f'product_{instance.id}')
32 cache.delete(f'product_slug_{instance.slug}')
33
34 if instance.category:
35 cache.delete(f'category_products_{instance.category.id}')
36 cache.delete(f'category_stats_{instance.category.id}')
37 self.update_category_stats(instance.category)
38
39 cache.delete('all_products')
40 cache.delete('featured_products')
41
42@receiver(post_save, sender=Review)
43def review_post_save(sender, instance, created, **kwargs):
44 """Обновление рейтинга продукта после отзыва"""
45 if created:
46 # Обновляем средний рейтинг продукта
47 product = instance.product
48 avg_rating = product.reviews.aggregate(
49 avg_rating=Avg('rating')
50 )['avg_rating'] or 0
51
52 product.avg_rating = round(avg_rating, 2)
53 product.review_count = product.reviews.count()
54 product.save(update_fields=['avg_rating', 'review_count'])
55
56 # Очищаем кеш продукта
57 cache.delete(f'product_{product.id}')
58
59 # Обновляем статистику категории
60 if product.category:
61 self.update_category_stats(product.category)
62
63def update_category_stats(self, category):
64 """Обновление статистики категории"""
65 try:
66 # Вычисляем статистику
67 stats = Product.objects.filter(
68 category=category
69 ).aggregate(
70 total_products=Count('id'),
71 avg_price=Avg('price'),
72 total_reviews=Count('reviews')
73 )
74
75 # Сохраняем в кеш
76 cache.set(
77 f'category_stats_{category.id}',
78 stats,
79 3600 # 1 час
80 )
81
82 except Exception as e:
83 logger.error(f"Ошибка обновления статистики категории: {e}")
84
85@receiver(post_save, sender=Category)
86def category_post_save(sender, instance, **kwargs):
87 """Обновление кеша категории"""
88 cache.delete(f'category_{instance.id}')
89 cache.delete(f'category_slug_{instance.slug}')
90 cache.delete('all_categories')
91
92 # Обновляем статистику
93 self.update_category_stats(instance)
Отключение сигналов
1from django.db.models.signals import post_save
2from django.dispatch import receiver
3from django.core.management.base import BaseCommand
4
5class Command(BaseCommand):
6 help = 'Импорт данных с отключенными сигналами'
7
8 def handle(self, *args, **options):
9 # Отключаем сигналы для массового импорта
10 from django.db.models.signals import post_save
11 from .signals import user_post_save
12
13 # Отключаем конкретный сигнал
14 post_save.disconnect(user_post_save, sender=User)
15
16 try:
17 # Выполняем импорт
18 self.import_users()
19 finally:
20 # Восстанавливаем сигнал
21 post_save.connect(user_post_save, sender=User)
22
23 def import_users(self):
24 """Импорт пользователей без сигналов"""
25 # Здесь логика импорта
26 pass
27
28# Альтернативный способ - использование контекстного менеджера
29from contextlib import contextmanager
30
31@contextmanager
32def disable_signals(*signals):
33 """Контекстный менеджер для отключения сигналов"""
34 disconnected = []
35 try:
36 for signal, receiver, sender in signals:
37 signal.disconnect(receiver, sender=sender)
38 disconnected.append((signal, receiver, sender))
39 yield
40 finally:
41 for signal, receiver, sender in disconnected:
42 signal.connect(receiver, sender=sender)
43
44# Использование
45with disable_signals(
46 (post_save, user_post_save, User),
47 (post_save, order_post_save, Order)
48):
49 # В этом блоке сигналы отключены
50 bulk_create_users()
51 bulk_create_orders()
Лучшие практики
- Используй pre_save для валидации и изменения данных - данные еще не сохранены в БД
- Используй post_save для действий после сохранения - отправка уведомлений, обновление кеша
- Обрабатывай ошибки gracefully - не прерывай основной процесс сохранения
- Используй транзакции для сложных операций - обеспечивай атомарность
- Логируй важные события - для отладки и мониторинга
- Очищай кеш после изменений - поддерживай актуальность данных
- Отключай сигналы при массовых операциях - избегай лишних вызовов
- Используй update_fields - избегай бесконечных циклов
- Тестируй сигналы отдельно - изолируй логику сигналов
- Документируй сложную логику - объясняй назначение каждого сигнала
FAQ
Q: Когда использовать pre_save vs post_save?
A: pre_save для изменения данных перед сохранением (валидация, форматирование), post_save для действий после сохранения (уведомления, обновление кеша, создание связанных объектов).
Q: Как избежать бесконечных циклов в сигналах?
A: Используй update_fields при сохранении, проверяй измененные поля, отключай сигналы при необходимости.
Q: Можно ли использовать сигналы в тестах?
A: Да, но лучше тестировать сигналы отдельно и использовать @override_settings для отключения в интеграционных тестах.
Q: Как отключить сигналы для массовых операций?
A: Используй signal.disconnect() перед операцией и signal.connect() после, или создай контекстный менеджер.
Q: Что делать, если сигнал вызывает ошибку?
A: Обрабатывай исключения в сигнале, логируй ошибки, не прерывай основной процесс сохранения.
Q: Как оптимизировать производительность сигналов?
A: Используй bulk операции, отключай сигналы при массовых операциях, кешируйте результаты, избегайте N+1 запросов.
Q: Можно ли использовать сигналы для валидации?
A: Да, но лучше использовать валидаторы моделей и форм. Сигналы подходят для бизнес-логики и автоматизации.