先说结论
在 Odoo 里,事务不是一句“出错就回滚”就能讲完的。
更准确地说:
- 整个请求 / 操作通常跑在一个数据库事务里
rollback()是把整个当前事务回退savepoint()是在事务内部切一个“局部可撤回的检查点”commit()才是把当前事务真正落地
所以如果要用一句话概括:
rollback 管整笔交易,savepoint 管交易里的一个可撤销小片段。
官方源码里 savepoint 是怎么定义的
在 /home/ubuntu/odoo-temp/odoo/sql_db.py 里,Cursor 的接口非常直接:
commit():提交当前事务rollback():回滚当前事务savepoint(flush=True):进入一个新的 savepoint 上下文
源码注释还专门强调了一点:
flush=True是默认值- 会自动处理相关 flush / hook
这其实已经在提醒你:
savepoint 不只是 SQL 层的“打个点”,它还要和 Odoo ORM 的缓存、flush、hook 节奏协调。
这也是为什么很多内部实现会在非常明确的场景下才用 flush=False。
为什么 Odoo 需要 savepoint,而不是只靠整单回滚
如果系统里只有两种状态:
- 全成功
- 全失败
那当然只要事务 + rollback 就够了。
但真实业务开发经常不是这样。
比如:
- 一批数据里,某一条校验失败,不想把前面准备动作也全部抹掉
- 某个“尝试性动作”可能失败,但失败本身是允许的
- 你想先尝试一段可能触发约束的逻辑,失败了就局部撤销,再走备用分支
这时候如果没有 savepoint,你只能:
- 要么把整笔事务全回滚
- 要么提前 commit(这通常更危险)
而 savepoint 的价值,就是提供第三种选择:
在同一个大事务里,允许你对局部步骤做“失败即撤回”。
官方源码里哪些地方在用 savepoint
你在 /home/ubuntu/odoo-temp 里会看到非常多官方场景都在用它,例如:
odoo/modules/loading.pyodoo/orm/registry.pyaddons/base_import/models/base_import.pyaddons/website/controllers/form.pyaddons/stock/models/stock_quant.pyaddons/account/models/sequence_mixin.py
这几个场景本身就很有代表性:
1. 模块加载 / 注册阶段
模块安装升级不是“做一步就永远落库一步”,中间很多步骤都需要可控地失败、恢复、继续。
2. 导入场景
导入很容易遇到坏行、约束失败、格式异常。
如果没有局部回退,整批导入的容错体验会非常差。
3. 网站表单 / 外部入口
外部输入天然不稳定。
有时你需要先尝试创建或匹配对象,失败后再转备用逻辑,而不是一失败就把整次请求的所有准备动作都炸掉。
最容易搞错的一点:savepoint 不是 commit
很多人潜意识会把 savepoint 想成“先临时保存一下”。
这很危险。
因为 savepoint 不是提交。
它只是说:
- 到这里先做一个检查点
- 后面这一小段如果失败,可以回退到这里
但只要外层事务最后整体失败,这些内容仍然可能全部不算数。
也就是说:
savepoint解决的是局部可撤销commit解决的是全局正式落地
这两个层级完全不同。
再容易搞错的一点:局部失败被吞掉,不代表系统“没有回滚”
看下面这种典型结构:
with self.env.cr.savepoint():
do_something_risky()
如果 do_something_risky() 抛错:
- savepoint 内的数据库变更会被撤销
- 外层事务可以继续活着
如果你又在外层把异常捕获掉,很多人就会误以为:
“报错了但系统没回滚。”
其实不是没回滚,而是:
- 局部已经回滚
- 外层事务没有被整体打断
所以排查这类问题时,你一定要分清:
- 是哪一层失败
- 是哪一层在兜底
- 最终有没有整体 commit
为什么默认是 flush=True
这点非常关键,但平时最容易被忽略。
Odoo ORM 并不是每次字段赋值都立刻打到数据库。
很多时候它会:
- 延后 flush
- 维护缓存
- 延后某些计算与钩子
如果 savepoint 和这些状态脱节,你会得到很难理解的结果:
- SQL 以为状态 A
- ORM 缓存以为状态 B
- 回滚时又只回了数据库层的一部分认知
所以 savepoint(flush=True) 的默认设计,其实是在保护:
savepoint 前后的数据库状态与 ORM 认知尽量同步。
这也是为什么官方只有在非常明确知道自己在做什么时,才会传 flush=False。
实战里什么时候该用 savepoint
适合用的场景
- 尝试性逻辑,失败允许降级
- 批量处理中,单步失败不想拖垮整批
- 需要探测某个约束 / 唯一性 / 可行性
- 外部输入不稳定,但请求主流程还要继续
不适合用的场景
- 想用它替代正常的业务校验
- 想用它掩盖经常发生的结构性错误
- 想用“局部回滚”掩饰事务边界设计混乱
- 明明应该在外层统一失败,却硬拆成很多 savepoint
换句话说,savepoint 是精细化控制工具,不是创可贴。
一个很常见的误区:为了保住前面结果,随手 commit
有些开发看到“后面可能失败”,第一反应不是 savepoint,而是:
- 先 commit 前面已经做好的
- 后面再继续做
这通常比你想的危险得多。
因为这样会把原本一笔业务事务,拆成多个已经落地的碎片:
- 前面成功已提交
- 后面失败却回不去了
- 最后系统可能留下半成品状态
而 savepoint 恰恰提供了更安全的中间层:
- 不提前正式提交
- 只在大事务内做局部试错
这才是更符合 Odoo 事务风格的思路。
和现有文章怎么区分
这篇不是在重复讲 flush / invalidate / modified 的 ORM 缓存机制。
那篇更偏缓存与重算协作。
而本文的切入点是:
- 事务是什么层级
- savepoint 为什么不是 commit
- 局部回滚和整单回滚的边界
- 官方为什么在导入、模块加载、外部表单等场景广泛使用它
也就是从事务控制语义去讲,而不是从字段缓存语义去讲。
最后一句话
在 Odoo 开发里,真正成熟的事务观不是“报错了就回滚”这么粗。
而是要分清三层:
- savepoint:局部撤回
- rollback:整笔撤回
- commit:正式生效
一旦这三层想清楚,很多“为什么这里失败了却没全炸”“为什么这里看着保存了最后又没了”的问题,就都顺了。
DISCUSSION
评论区