为什么这个主题值得学
很多 Odoo 开发卡住,不是因为不会写字段,而是因为:
- 为什么数据库已经改了,界面还是旧值?
- 为什么 stored compute 没及时更新?
- 为什么 raw SQL 后面数据怪怪的?
- 为什么有时要
invalidate_recordset(),有时还要modified()?
这些问题最后都绕不开 Odoo ORM 的三件套:
flushinvalidate_recordsetmodified
你只要把这三个分清楚,很多“玄学问题”会瞬间变正常。
先记一个总图
把它们分别翻译成人话:
- flush:把内存里待落库的变更真正写进数据库
- invalidate:告诉缓存“你手里的值不可信了”
- modified:告诉 ORM“某些字段变了,依赖它们的 stored compute 需要重新算”
这三件事不是一件事,只是经常连着出现。
env.flush_all() 在干什么
在 odoo/orm/environments.py 里,flush_all() 的逻辑很清晰:
- 先处理所有 pending recomputations
- 再找出脏字段所属模型
- 逐模型调用
flush_model() - 循环直到没有待处理项
源码注释对应的意思可以直接翻译成:
把所有挂起的计算和更新,最终都推进到数据库。
所以 flush 不是简单的“提交事务”,而是 ORM 级别的:
- 先把该补算的补算
- 再把该落库的落库
这就是为什么很多 stored compute 的值,看起来不是立刻写表,但在 flush 后就一致了。
为什么 flush 不等于 commit
这点非常重要。
- flush:把当前事务中的 ORM 改动写到数据库连接层面
- commit:把事务最终提交,让其他事务也看到
也就是说:
- flush 后,当前事务内 SQL 查询通常能看到最新值
- 但不代表外部世界已经永久看见
所以你做 raw SQL 联动时,经常需要的不是先 commit,而是先 flush。
invalidate_recordset() 在干什么
在 odoo/orm/models.py 里,invalidate_recordset() 的含义很直接:
- 当前 recordset 的缓存可能已经不对
- 可选地先 flush 对应字段
- 然后把缓存作废
它不是“重新计算”,而是“撤销旧认知”。
这一步尤其常见于:
- 你刚跑了 raw SQL 更新数据库
- ORM 缓存还保留着旧值
- 如果不 invalidate,后面读出来还是旧数据
所以 invalidate 的核心不是产生新值,而是避免继续相信旧值。
modified() 为什么比 invalidate 更进一步
很多人做完 raw SQL 后只调用 invalidate_recordset(),然后发现 stored compute 依赖字段还是不对。
原因就在这里:
invalidate_recordset()只处理缓存一致性modified()还会通知依赖图
源码里的 docstring 非常关键:
fields 被修改了,需要 invalidates cache,并准备 dependent stored fields 的 recomputation。
换成人话就是:
我不只是告诉你旧值作废,我还告诉你“依赖这些字段的别的字段也要重新算”。
这就是 modified() 的价值。
一个典型正确姿势:raw SQL 后怎么办
如果你直接用 SQL 改了某模型的字段,常见安全顺序通常是:
- 先保证 ORM 该 flush 的已 flush
- 执行 SQL
- 拿到受影响记录 ids
browse(ids).invalidate_recordset(['field'])browse(ids).modified(['field'])
为什么两步都要?
因为你要同时解决两件事:
- 缓存别再信旧值
- 依赖字段要重新算
少一步,结果都可能“半新半旧”。
modified(before=False) 背后到底在推进什么
源码中 modified() 会沿着字段依赖树往后找:
- 哪些 stored compute 依赖这个字段
- 是当前记录本身依赖,还是通过关系字段反向依赖
- 应该立即标记 recompute,还是延后
这一套机制的本质,是把 Odoo 字段系统从“字段赋值”升级成“依赖传播系统”。
所以你写的:
@api.depends('line_ids.price_subtotal')
背后并不是一句装饰器而已。
它意味着当底层字段变化时,ORM 知道如何沿着依赖链把需要重算的目标字段标出来。
为什么有些值明明改了,却没有立刻重算
因为 Odoo 会尽量批处理。
它不是每改一个字段就把所有依赖链立刻同步算到底,而是会:
- 标记 dirty / tocompute
- 在合适时机批量 recompute
- 在 flush 过程中继续推进直到收敛
这对性能很重要。
否则一个复杂单据改 20 个字段,系统会疯狂重复算很多遍。
所以“不是立刻重算”通常不是 bug,而是 ORM 的性能设计。
flush=True 默认值为什么很保守
你会看到 invalidate_recordset(fnames=None, flush=True) 默认会先 flush。
原因非常现实:
如果你在还有 pending ORM 改动时就贸然失效缓存,容易把状态搞得前后不一致。
默认先 flush,属于“宁可稳一点,也别把一致性打碎”。
除非你非常知道自己在干什么,否则不建议随便把这个默认行为关掉。
开发里最容易犯的 4 个错
1. raw SQL 后不 invalidate
结果:ORM 继续读到旧缓存。
2. 只 invalidate,不 modified
结果:依赖 stored compute 不重算。
3. 需要 SQL 看最新值,却没先 flush
结果:数据库层和 ORM 层看到的状态不一致。
4. 把 flush 当成 commit
结果:对事务可见性理解错误,排查越查越乱。
一个特别好用的脑内模型
你可以这样想:
- 数据库 = 最终账本
- ORM cache = 当前会话的记忆
- recompute 队列 = 待办事项清单
那么:
flush= 把记忆里的已确认事项写到账本invalidate= 告诉自己某些记忆过期了modified= 在待办清单里补上“相关字段需要重算”
这样一想,很多 API 名字就不神秘了。
一句话记忆法
把这套机制记成一句话:
flush 负责落库,invalidate 负责废缓存,modified 负责把依赖重算这件事正式通知 ORM。
掌握这句,你处理 Odoo 高级 ORM 问题会稳很多。
有深度,又通俗易懂👍️