Ось твій урок про Django Signals, створений у стилі CS50: енергійний, з аналогіями та акцентом на "чому це важливо".
🎓 CS50: Django Signals — Магія подій "за кадром"
Всім привіт! Це CS50, і сьогодні ми поговоримо про те, як змусити ваш код спілкуватися між собою, не перетворюючи його на спагеті.
1. 🔥 Вступ: Проблема "Все в одному місці"
Уявіть, що ви будуєте величезний інтернет-магазин. У вас є модель Order (Замовлення).
Коли клієнт натискає кнопку "Купити", відбувається збереження замовлення в базу даних. Все просто, правда?
Але зачекайте! Що ще має статися в цей момент? 1. Потрібно надіслати email клієнту ("Дякуємо за покупку!"). 2. Потрібно списати товар зі складу. 3. Потрібно нарахувати бонуси лояльності. 4. Потрібно надіслати повідомлення менеджеру в Slack.
Де ми напишемо цей код?
У методі save() моделі? У views.py?
Риторичне запитання: Якщо ми запихнемо всю цю логіку в метод save() замовлення, то наша модель Order раптом повинна знати про email-сервер, про Slack, про склад і про систему бонусів. Чи це хороша архітектура?
Звісно, ні! Це називається High Coupling (сильна зв’язність). Якщо зламається Slack, ваше замовлення може не зберегтися. Це катастрофа.
Аналогія: Уявіть, що ви купуєте каву. Бариста робить каву. Чи повинен бариста сам бігти на вулицю і кричати перехожим "Ми продали ще одну чашку!", потім бігти в бухгалтерію записувати прибуток, а потім дзвонити постачальнику молока? Ні. Бариста просто каже: "Замовлення готове!". А інші люди (або системи), почувши це, роблять свою роботу.
Ось тут на сцену виходять Django Signals. Це спосіб сказати: "Гей, щось трапилося!", і дозволити іншим частинам коду відреагувати на це, не знаючи одне про одного.
2. 🧠 Теоретична база: Як це працює "під капотом"
Сигнали — це реалізація патерну Observer (Спостерігач) або Pub/Sub (Publisher/Subscriber).
У нас є три головні дійові особи:
- Sender (Відправник): Той, хто кричить "Подія сталася!" (наприклад, модель
User, коли її зберегли). - Signal (Сигнал): Саме "радіоповідомлення", яке летить в ефір.
- Receiver (Отримувач): Функція, яка "підписана" на цей сигнал. Вона чекає, поки він прилетить, і тоді виконує код.
⚠️ Що треба обов'язково запам'ятати:
Сигнали в Django — СИНХРОННІ!
Це найпоширеніша помилка новачків. Коли ви відправляєте сигнал, Django зупиняє виконання основного потоку, біжить виконувати всі функції-отримувачі, і тільки коли вони завершаться, продовжує роботу.
Інтуїтивно: Це не як відправити листа поштою (кинув і забув). Це як передати естафетну паличку. Поки функція-отримувач не закінчить роботу, ваше збереження в базу даних (або відповідь користувачу) буде "висіти".
Найпопулярніші вбудовані сигнали:
pre_save/post_save— перед або після збереження об'єкта.pre_delete/post_delete— перед або після видалення.m2m_changed— коли змінюється Many-to-Many поле.
3. 🧪 Приклади: Від простого до реального
Приклад 1: "Hello World" у світі сигналів
Давайте створимо функцію, яка просто друкує в консоль повідомлення кожного разу, коли створюється або оновлюється будь-який користувач.
Припустимо, у нас є файл signals.py:
from django.db.models.signals import post_save
from django.dispatch import receiver
from django.contrib.auth.models import User
# @receiver — це декоратор, який "підписує" функцію на сигнал
@receiver(post_save, sender=User)
def announce_user_save(sender, instance, created, **kwargs):
if created:
print(f"🎉 Ура! Новий юзер {instance.username} щойно народився!")
else:
print(f"🔄 Юзер {instance.username} оновив свої дані.")
Що ви очікуєте побачити? Коли ви зайдете в адмінку і створите юзера, в консолі сервера з'явиться наше повідомлення.
Чому це круто? Ми не чіпали код самої моделі User (яка взагалі вбудована в Django). Ми розширили функціонал ззовні!
Приклад 2: Класика жанру — Автоматичний профіль
У реальних проєктах часто є модель User (для авторизації) і модель Profile (для аватарки, біографії тощо). Ми хочемо, щоб коли людина реєструється (User), для неї автоматично створювався порожній Profile.
# models.py
class Profile(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE)
bio = models.TextField(blank=True)
# signals.py
@receiver(post_save, sender=User)
def create_user_profile(sender, instance, created, **kwargs):
if created:
Profile.objects.create(user=instance)
Питання: А що буде, якщо я не додам перевірку if created:?
Відповідь: Кожного разу, коли ви просто змінюєте ім'я користувача і тиснете "Save", код намагатиметься створити ще один профіль для того ж юзера. А оскільки у нас OneToOneField, база даних викине помилку (IntegrityError). Тому created — це ваш найкращий друг.
Приклад 3: Запобігання дії (pre_save)
Припустимо, ми хочемо заборонити реєстрацію користувачів з email-адресами на домені @evil-corp.com.
Для цього ідеально підходить pre_save (перед збереженням).
from django.db.models.signals import pre_save
from django.core.exceptions import ValidationError
@receiver(pre_save, sender=User)
def ban_evil_corp(sender, instance, **kwargs):
if instance.email.endswith('@evil-corp.com'):
raise ValidationError("Ми не працюємо зі злодіями! 🚫")
Результат: Запис в базу навіть не потрапить. Процес перерветься до того, як SQL-запит буде виконано.
4. 🛠 Практична частина
Час кодити! Відкривайте IDE.
Завдання 1: "Слідчий"
Створіть сигнал post_delete для моделі Product (або будь-якої іншої). Коли продукт видаляється, записуйте у файл log.txt рядок: "Товар [назва] було знищено!".
Завдання 2: "Чистоплюй"
Використайте pre_save на моделі, яка має текстове поле (наприклад, comment). Зробіть так, щоб перед збереженням текст автоматично очищався від зайвих пробілів на початку і в кінці (strip()), а перша літера ставала великою.
Завдання 3: "Один раз — не..."
Напишіть сигнал, який спрацьовує при збереженні User, але... зробіть так, щоб він спрацьовував тільки якщо у юзера в полі is_staff стоїть True. (Підказка: просто додайте if всередину функції).
Завдання 4: Міні-кейс "Зміна статусу"
У вас є модель Order з полем status (pending, shipped, delivered).
Напишіть сигнал, який відстежує зміну статусу.
Умова: Якщо статус змінився саме на "shipped", виведіть в консоль "🚚 Відправляємо посилку!".
Підказка: У pre_save ви можете дістати стару версію об'єкта з бази через Order.objects.get(pk=instance.pk) і порівняти її status з новим instance.status.
Завдання із зірочкою (Bug Hunt):
Студент написав код сигналу у файлі signals.py, але він не працює. Нічого не відбувається. Код правильний. Чому Django його не бачить?
(Відповідь: Файл signals.py треба імпортувати в методі ready() у файлі apps.py, інакше Django просто пройде повз нього).
5. 💡 Мислення як у розробника
Окей, ви навчилися користуватися молотком. Тепер головне — не бити ним по вікнах.
🚫 Типова помилка новачка: Безкінечна петля (Infinite Loop)
Подивіться на цей код. Що з ним не так?
@receiver(post_save, sender=Order)
def update_order(sender, instance, **kwargs):
instance.total_price = 100
instance.save() # 😱 УВАГА!
Аналіз:
1. Order зберігається.
2. Викликається сигнал post_save.
3. Всередині сигналу ми робимо instance.save().
4. Це знову викликає post_save.
5. ...Рекурсія. Stack Overflow. Сервер падає.
Як думає про: "Якщо мені треба змінити дані в тій самій моделі, я використаю pre_save і просто зміню значення поля. save() викликати не треба, воно саме збережеться далі по ланцюжку."
🕵️♂️ Пастка bulk_create
Якщо ви зробите User.objects.bulk_create([user1, user2]), сигнали save НЕ спрацюють!
SQL-запит піде напряму в базу для швидкості.
Мислення про: "Якщо я використовую масові операції, я не можу покладатися на сигнали. Мені доведеться викликати логіку вручну або переписувати підхід".
🧘 Zen of Python: "Explicit is better than implicit"
Сигнали — це магія. Магія — це неявно. Якщо у вас в проєкті 50 сигналів, ви можете зберегти юзера і запустити ланцюгову реакцію, яка покладе сервер, і ви навіть не зрозумієте, звідки прилетіло. Порада: Використовуйте сигнали тільки для side effects (побічних ефектів), які не критичні для основної логіки, або для розчеплення (decoupling) незалежних додатків. Не будуйте на них бізнес-логіку ядра.
6. 🧩 Підсумок
Отже, що ми маємо в сухому залишку:
- Сигнали дозволяють реагувати на події в Django (збереження, видалення).
- Вони допомагають розділяти код (Decoupling).
- Вони працюють синхронно (блокують виконання).
- Ви тепер вмієте автоматично створювати профілі, валідувати дані та "шпигувати" за подіями.
🤔 Тизер наступного уроку
Ми згадали, що сигнали синхронні. Але що, якщо відправка вітального email займає 5 секунд? Невже користувач має дивитися на білий екран і чекати? Це неприпустимо! На наступному уроці ми дізнаємося, як викинути важкі задачі з основного потоку у фоновий режим. Готуйтеся, тема: Celery та асинхронність!
А поки що — це був CS50. Щасти вам з кодом!