django事务回滚必须由transaction.atomic()显式控制,单纯try-except无法保证一致性;未捕获异常触发自动回滚,吞掉异常则部分操作已提交,atomic是唯一可靠事务边界。

事务回滚必须显式控制,Django默认不自动回滚
Django 的 transaction.atomic() 是唯一可靠的事物边界控制方式;单纯靠 try-except 捕获异常后不加 atomic,数据库可能已提交部分操作,根本无法回滚。
常见错误是写成这样:
try:
obj1.save()
obj2.save() # 这里抛出 IntegrityError
except IntegrityError:
pass # ❌ 以为能回滚,其实 obj1 已落库
正确做法必须用 atomic 包裹整个逻辑块:
-
atomic是上下文管理器,也是装饰器,二者语义一致 - 只要块内任意位置抛出未捕获的异常(包括
raise、IntegrityError、ValueError),Django 就触发 ROLLBACK - 异常被捕获后主动 re-raise 或不处理,才能保证回滚;吞掉异常(如
except: pass)等于放弃事务保护
嵌套 atomic 怎么影响回滚?看 savepoint 参数
多个 atomic 嵌套时,默认开启 savepoint(子事务点),外层失败会连带回滚所有内层;但内层单独失败,只回滚自己,不影响外层 —— 这个行为常被误认为“事务隔离”,其实是 savepoint 机制。
例如:
@atomic
def outer():
A.objects.create(name="a")
with atomic(): # 默认 savepoint=True
B.objects.create(name="b")
raise ValueError("boom") # ✅ 只 B 回滚,A 仍存在
C.objects.create(name="c")
如果想让内层失败也拖垮外层,得关掉 savepoint:
- 显式传
savepoint=False:此时内层异常直接冒泡,触发外层回滚 - 但要注意:MySQL 不支持在 savepoint=False 的嵌套 atomic 中混用不同数据库连接
- 生产环境慎用
savepoint=False,容易因局部错误导致大范围数据不一致
手动 rollback 仅限底层 connection,且极不推荐
有人试图用 connection.rollback() 强制回滚,这绕过了 Django 事务管理器,极易出错:
- 它只作用于当前线程的
connection,而atomic可能绑定了特定的 connection alias - 在
atomic块里调用rollback()会导致后续 SQL 报错TransactionManagementError: Transaction managed externally - ORM 缓存、信号、post_save 钩子等都不会感知手动 rollback,状态严重不一致
真正需要“提前退出事务”的场景,应该用 raise Exception() 主动中断,而不是手撕 connection。
测试时事务回滚失效?检查 TestCase 和 TransactionTestCase 的区别
Django 测试中,普通 TestCase 自动用 atomic 包裹每个 test 方法,并在末尾强制 rollback —— 所以你看不到数据残留;但如果你手动用了 atomic,又在其中 catch 了异常,就可能干扰这个机制。
更麻烦的是 TransactionTestCase:它不使用 savepoint,而是每次 test 后 truncate 表,速度慢、粒度粗。只有当你测试 atomic 内部行为(比如 signal 触发时机)、或涉及多线程/数据库级锁时才需它。
- 95% 的业务逻辑测试用
TestCase足够,别为“模拟回滚”强行换 TransactionTestCase - 若真要验证回滚效果,直接 assert 数据库记录数或查对象是否存在,比 inspect connection 状态靠谱得多
事务不是开关,是契约;你写的每一行 atomic,都在和数据库约定“要么全成,要么全废”。漏掉一个装饰器、吞掉一个异常、跨层乱用 savepoint,都可能让契约失效——而这种失效,往往在线上跑几天才暴露。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











