post_save信号应在apps.py的ready()方法中注册;receiver内避免直接调用instance.save()以防循环触发;bulk操作不触发该信号;事务中需用on_commit保证副作用与事务一致。

post_save 信号到底该在哪儿注册
信号必须在 Django 应用准备好之后、模型类定义完成但尚未被大量引用前注册,否则 post_save.connect() 可能失效或漏触发。最稳妥的位置是应用的 apps.py 的 ready() 方法里——不是 models.py 顶层,也不是 views.py 里随便 import 的地方。
常见错误现象:post_save 完全不触发,查日志也没调用痕迹;或者只在开发服务器重启后第一次保存生效,之后就静默。
- 别把
@receiver(post_save, sender=MyModel)放在models.py末尾:模块加载顺序不稳定,尤其有跨模型依赖时容易失效 - 别在
__init__.py或settings.py里注册:Django 尚未完成模型扫描,sender类可能还没被识别为 ORM 模型 - 确保
INSTALLED_APPS中包含该应用,且其AppConfig已启用(比如在__init__.py中指定default_app_config)
receiver 函数里不能直接改 instance.save() 吗
可以改,但必须小心循环触发——post_save 是“保存后”触发,如果你在里面又调用 instance.save(),会再次触发同一信号,形成无限递归,最终爆栈或被数据库事务拦截。
典型使用场景:同步字段(如生成 slug)、发通知、更新关联统计,但这些操作本身不该再触发本模型的 post_save。
- 要避免重复保存,改完数据后用
instance.save(update_fields=[...]),并确保 receiver 不监听update_fields的变化逻辑 - 如果真需要二次保存(比如补全外键),加个标记字段或用
django.db.transaction.on_commit()延迟到事务提交后执行 - 别在 receiver 里做耗时操作(如 HTTP 请求、大文件写入),它运行在主请求线程中,会拖慢响应;应转给 Celery 或异步任务
为什么有时候 post_save 没收到 created=False
created 参数由 Django 内部根据 model._state.adding 判断,但它可能被手动篡改或受批量操作影响。最常见原因是用了 bulk_create()、bulk_update() 或 raw SQL 插入——这些绕过 ORM 正常生命周期,post_save 根本不会触发。
提供使用 django-unfold (v0.56+) 构建高级 Django 管理界面的专业指导。当用户提及 django-unfold、unfold admin 等内容时使用此技能。
另一个隐蔽坑:模型继承(AbstractBaseUser 或多表继承)下,子类实例的 _state.adding 可能在某些查询路径中被意外覆盖。
- 检查是否用了
bulk_create(..., ignore_conflicts=True):即使冲突被忽略,也不会触发post_save - 确认 model 实例是通过
.save()创建的,而不是Model.objects.create()的变体(它本质还是.save(),没问题) - 调试时打印
instance._state.adding和created,对比是否一致;不一致说明 ORM 状态异常,优先排查自定义save()方法是否误设了_state.adding = False
信号和事务一起用要注意什么
post_save 在事务内触发,但如果事务回滚,receiver 里的副作用(比如发邮件、写日志文件、调外部 API)不会自动撤销——这会造成状态不一致。
比如用户注册成功发欢迎邮件,但后续支付失败导致整个事务回滚,用户却已收到邮件。
- 关键副作用必须等事务真正提交后再执行,用
django.db.transaction.on_commit(lambda: send_welcome_email(instance)) - 不要在 receiver 里手动开新数据库事务(
transaction.atomic()),它嵌套在原事务中,无法独立提交或回滚 - 如果 receiver 需要读取刚保存的数据,注意默认隔离级别下可能读不到未提交变更;必要时用
select_for_update()或显式指定using数据库别名
信号不是万能解耦工具,它把耦合从代码调用变成了事件隐式依赖。一旦 receiver 报错,整个 save() 会中断——这点比直接调用函数更难 debug。上线前务必测好所有 receiver 的异常路径。










