ORM 机制

Odoo ORM 深处:flush、invalidate_recordset、modified 到底各管什么

很多高级 Odoo 开发问题,最后都会落到缓存、recompute、flush 和 modified 这套机制。本文不装神秘,直接把它讲成人能理解的话。

Odoo 开发
进阶 开发者 2 分钟阅读
1 评论 1 点赞 0 收藏 14 阅读

为什么这个主题值得学

很多 Odoo 开发卡住,不是因为不会写字段,而是因为:

  • 为什么数据库已经改了,界面还是旧值?
  • 为什么 stored compute 没及时更新?
  • 为什么 raw SQL 后面数据怪怪的?
  • 为什么有时要 invalidate_recordset(),有时还要 modified()

这些问题最后都绕不开 Odoo ORM 的三件套:

  • flush
  • invalidate_recordset
  • modified

你只要把这三个分清楚,很多“玄学问题”会瞬间变正常。


先记一个总图

把它们分别翻译成人话:

  • flush:把内存里待落库的变更真正写进数据库
  • invalidate:告诉缓存“你手里的值不可信了”
  • modified:告诉 ORM“某些字段变了,依赖它们的 stored compute 需要重新算”

这三件事不是一件事,只是经常连着出现。


env.flush_all() 在干什么

odoo/orm/environments.py 里,flush_all() 的逻辑很清晰:

  1. 先处理所有 pending recomputations
  2. 再找出脏字段所属模型
  3. 逐模型调用 flush_model()
  4. 循环直到没有待处理项

源码注释对应的意思可以直接翻译成:

把所有挂起的计算和更新,最终都推进到数据库。

所以 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 改了某模型的字段,常见安全顺序通常是:

  1. 先保证 ORM 该 flush 的已 flush
  2. 执行 SQL
  3. 拿到受影响记录 ids
  4. browse(ids).invalidate_recordset(['field'])
  5. 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 问题会稳很多。

DISCUSSION

评论区

想参与讨论?先 登录 再发表评论。
圣小宁 2026-03-15 10:59

有深度,又通俗易懂👍️