该用 post_save 而不是视图处理业务逻辑,因其在数据真正落库后触发,保障事务一致性,支持解耦、独立测试与多应用监听;需注意重复注册、反向关系访问限制及与 celery 配合时的事务边界控制。

什么时候该用 post_save 而不是直接写在视图里?
业务逻辑塞进视图函数看似快,但一改就牵连多个接口。比如用户注册后要发邮件、创建默认配置、更新统计表——这些和「注册成功」这个事实强相关,但和「哪个接口触发的注册」弱相关。post_save 正是用来捕捉「模型实例已落库」这一确定状态的钩子。
- 只在数据真正写入数据库后触发(事务提交后),避免因事务回滚导致信号侧逻辑误执行
- 信号接收器可独立测试,不依赖请求上下文或视图生命周期
- 多个应用可各自监听同一模型事件,互不侵入对方代码
- 注意:
sender参数必须精确指定模型类(如User),不能用字符串或泛型;否则信号根本不会被调用
from django.db.models.signals import post_save from django.dispatch import receiver from myapp.models import Order <p>@receiver(post_save, sender=Order) def handle_order_created(sender, instance, created, **kwargs): if created: send_receipt_email(instance) # 独立函数,无Django请求依赖</p>
disconnect 不手动调用会引发什么问题?
Django 的信号是全局注册的,模块导入即绑定。如果在测试或热重载场景下反复导入含 @receiver 的模块,同一个函数会被重复注册多次——结果就是一条订单创建,触发 3 次邮件发送。
- 单元测试中务必在
setUp里用post_save.disconnect(receiver=xxx, sender=Order)清理 - 开发时用
manage.py shell调试信号,先查当前绑定:post_save.receivers(返回一个长列表,看长度是否异常) - 更稳妥的做法:在接收器开头加 guard 判断是否已处理过,例如用
instance._signal_handled属性标记(需配合save(..., update_fields=...)避免覆盖)
为什么 pre_delete 里不能访问外键反向关系?
pre_delete 触发时,数据库记录尚未删除,但 Django 已经把该实例从相关缓存和反向关系管理器中移除了。常见报错是 RelatedObjectDoesNotExist 或空的 QuerySet。
- 想清理关联数据?改用
post_delete,此时主对象还在内存里,只是 DB 记录没了 - 必须在删除前检查关联项?提前在视图或服务层做
obj.related_set.all()并保存结果到局部变量 - 性能敏感场景慎用:反向查询本身可能触发额外 SQL,
pre_delete里再查一次等于双倍开销
Signals 和 Celery 结合时,事务边界怎么卡准?
直接在信号里调用 task.delay() 很危险:如果信号在事务内触发,而任务立刻消费,可能读到未提交的数据;更糟的是,若事务随后回滚,任务却已发出,造成状态不一致。
- 正确做法:用
transaction.on_commit()包一层 - 示例中
send_receipt_email.delay()必须等事务 commit 成功才进队列 - 注意:Celery 任务函数本身不能再开启新事务去读刚保存的模型——它看到的是最终一致状态,但要避免在任务里做「乐观锁校验」之类依赖中间态的操作
from django.db import transaction <p>@receiver(post_save, sender=Order) def queue_email_on_commit(sender, instance, created, **kwargs): if created: transaction.on_commit(lambda: send_receipt_email.delay(instance.id))</p>
信号不是万能胶,它把耦合从代码调用转成了事件依赖。真正难的不是注册几个 @receiver,而是厘清「谁负责保证事件不丢」「谁负责幂等」「谁兜底失败重试」——这些往往得靠 Celery + 监控 + 补偿任务来填。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











