直接重写save()方法做日志不可靠,因无法捕获bulk_create、bulk_update、raw SQL更新及外键反向操作等场景,导致漏记;信号机制虽官方推荐,但post_save在事务提交前触发易产生幽灵日志,post_delete中实例字段不可查,且信号无请求上下文,须配合transaction.on_commit()、threading.local(或contextvars)及合理字段筛选才能确保日志准确、安全、高效。

为什么直接重写 save() 方法做日志不可靠
因为 save() 不会捕获 bulk_create、bulk_update、raw SQL 更新、外键反向操作(比如 user.profile.delete())等场景,日志必然漏记。信号机制是 Django 官方推荐的模型变更监听方式,但必须避开几个典型陷阱。
-
post_save信号在事务提交前触发,如果后续事务回滚,日志记录就变成“幽灵日志”——实际数据没变,日志却写了 -
pre_delete/post_delete拿不到被删对象的字段值(post_delete中实例已从 DB 解绑,pre_delete可用但需手动查一次 DB) - 信号接收器里不能依赖
request对象——它根本不在信号调用栈里,Django 信号是纯模型层的,和 HTTP 请求无绑定
用 django.db.transaction.on_commit() 确保日志与数据一致
这是解决“幽灵日志”的核心。所有审计日志写入必须延迟到事务真正提交之后执行,否则日志和 DB 状态不一致。
from django.db import transaction from django.db.models.signals import post_save, post_delete <p>def log_model_change(sender, instance, created, **kwargs):</p><h1>不在这里直接写日志</h1><pre class='brush:python;toolbar:false;'>transaction.on_commit(lambda: _write_audit_log(sender, instance, 'create' if created else 'update'))
def log_model_delete(sender, instance, **kwargs):
安全 Python 代码沙箱:在隔离的沙箱中执行任意 Python 代码,预装 requests、NumPy、pandas 库。返回 stdout、stderr。
立即学习“Python免费学习笔记(深入)”;
删除前先序列化关键字段,因为 post_delete 里 instance 已不可查
data = {f.name: getattr(instance, f.name) for f in instance._meta.fields}
transaction.on_commit(lambda: _write_audit_log(sender, data, 'delete'))- 务必用
transaction.on_commit()包裹日志写入逻辑,而不是直接调用 -
post_delete信号中,instance的字段仍可读(只要没被显式 del 或 reload),但不能做任何 DB 查询(如instance.related_set.all()) - 避免在 lambda 里引用闭包中可能变化的变量;建议提前提取必要字段,如上面的
data字典
如何记录谁在什么时间做了什么操作
信号本身不带用户上下文,必须从请求线程中“借”出当前用户。推荐用 threading.local + 中间件注入,而非依赖全局变量或信号参数传参(后者破坏解耦)。
# middleware.py
import threading
_local = threading.local()
<p>class AuditLogMiddleware:
def <strong>init</strong>(self, get_response):
self.get_response = get_response</p><pre class='brush:python;toolbar:false;'>def __call__(self, request):
_local.user = getattr(request, 'user', None)
_local.request_id = getattr(request, 'id', None)
response = self.get_response(request)
# 清理,防止线程复用导致污染
if hasattr(_local, 'user'):
delattr(_local, 'user')
if hasattr(_local, 'request_id'):
delattr(_local, 'request_id')
return response在信号处理器中取:
def _write_audit_log(model_class, data, action): user = getattr(_local, 'user', None) user_id = user.pk if user and hasattr(user, 'pk') else None
写入 AuditLog model...
- 中间件必须放在
AuthenticationMiddleware之后,否则request.user是AnonymousUser或未认证状态 -
threading.local在异步视图(ASGI)中不生效,Django 4.1+ 应改用contextvars.ContextVar - 不要试图在信号里调用
get_current_request()这类“黑魔法”,它不稳定且难以测试
哪些字段该记、哪些不该记、怎么避免性能崩掉
审计日志不是全量 dump,重点是“谁、何时、对哪条记录、做了什么变更”。字段选择直接影响磁盘和查询性能。
- 必记:模型名(
instance._meta.label)、主键值(instance.pk)、操作类型(create/update/delete)、操作时间(timezone.now())、操作人 ID - 慎记:大文本字段(如
TextField)、二进制字段(BinaryField)、JSON 字段(除非只记 diff 后的 key 列表) - 更新操作要对比新旧值——用
model_to_dict(instance)+ 从数据库 reload 旧值(仅对单条记录),或监听pre_save存快照(注意内存开销) - 高频写模型(如访问统计、实时消息)禁用审计信号,要么跳过,要么走异步任务(Celery)延后处理
最常被忽略的一点:日志表自己也要加索引。至少在 content_type_id、object_id、timestamp、user_id 上建联合索引,否则查某用户昨天改了哪些订单会慢到超时。

















