先说结论
很多人把 Odoo ORM 想成这样:
- 我给字段赋值了
- 数据库就立刻是最新的
- 其他字段也会立刻全部同步好
- 我再跑一条 SQL,一切自然一致
现实不是这样。
Odoo ORM 更像一个带缓存、带延迟刷新、带重算队列的协调系统。
其中最容易混淆的 3 个动作是:
flush_*():把待落库的数据真正推到数据库invalidate_*():告诉 ORM“缓存别信了,重新读”modified():告诉 ORM“这些字段变了,相关依赖要准备重算”
这三件事看起来都像“同步”,但它们处理的是完全不同的问题。
第一层:为什么 ORM 需要缓存
在 odoo/orm/models.py 里,recordset 本身带 _prefetch_ids,而 RecordCache 则表示单条记录当前有哪些字段值已经进了缓存。
这背后的目的很简单:
- 一次读 80 条记录,不要每条都单独查库
- 同一批记录的相关字段,尽量一起预取
- 后续访问时优先用缓存,减少 SQL 次数
所以你平时写:
partners = self.env['res.partner'].search(...)
for partner in partners:
print(partner.name)
你看到的是“像普通对象一样读属性”,但底下并不是每次都实时查库。
这就是为什么 Odoo 性能能撑住复杂业务;也正因为有这层缓存,同步时机才变得重要。
第二层:flush_model() 解决的是“数据库还没跟上”
源码里,flush_model() 的注释写得很直白:
处理待执行的计算与数据库更新。
它会先:
- 触发相关重算
- 再把脏字段
_flush()到数据库
所以 flush_model() 处理的问题是:
ORM 里已经知道值变了,但数据库表里可能还不是最终状态。
这在你准备直接跑 SQL 时尤其关键。
例如:
- 你前面刚改过记录
- 还没 flush
- 你马上
SELECT那张表
这时你看到的,可能是数据库旧值,不是 ORM 当前准备提交的值。
第三层:invalidate_model() 解决的是“ORM 缓存还没跟上”
反过来,invalidate_model() 处理的是另一个方向的问题。
源码注释写的是:
当缓存值已不再对应数据库值时,使缓存失效。
这通常出现在你绕过 ORM 直接改数据库的时候。
例如你手工执行:
UPDATE sale_order SET state = 'cancel' WHERE id = 10;
数据库已经变了,但 ORM 还可能记着旧值。
这时如果你不做失效处理,后续 Python 代码可能继续读到缓存里的旧状态。
所以:
flush是把 ORM 往数据库推invalidate是让 ORM 承认数据库已经变了
方向完全相反。
第四层:modified() 解决的是“依赖字段要不要重算”
modified() 的源码注释更关键:
通知某些字段已被修改,并准备相关 stored compute 字段的重算。
也就是说,modified() 不等于“立刻把值算出来”,而是:
- 标记依赖链
- 让 ORM 知道谁需要后续重算
- 必要时配合缓存失效
举个最简单的心智模型:
- A 改了
- B 是 store=True 的 compute,depends(A)
- 你得告诉系统:B 之后要重算
这个“告诉系统”的动作,就是 modified() 干的核心工作之一。
所以如果你绕过 ORM 直接 UPDATE 了底表,但没调用相应同步逻辑,最容易出的问题不是“当前字段不对”,而是:
依赖这个字段的那串 store 计算字段,后面也全可能不对。
为什么“直接 SQL”最容易把人带沟里
很多开发者会说:
- 我只是跑一条 UPDATE
- 反正数据库已经改了
- ORM 下次读自然会对
这句话只对了一半。
对的部分
数据库确实已经变了。
不对的部分
ORM 世界里可能还有:
- 旧缓存
- 尚未失效的字段值
- 尚未通知的依赖重算
- 尚未 flush 的其他脏数据
所以官方推荐的典型顺序其实是:
- 先
flush_model()—— 先把 ORM 自己手头待落库的东西落下去 - 再跑 SQL —— 改数据库
- 再
invalidate_model()/invalidate_recordset()—— 告诉 ORM 别信旧缓存 - 必要时
modified()—— 告诉 ORM 有依赖链要重算
很多“为什么我 SQL 改完了页面还是旧值”的问题,基本都死在第 3 步。
很多“为什么依赖字段几小时后才怪怪地出 bug”的问题,往往死在第 4 步。
一个最小例子
假设你有:
- 字段
amount - 字段
total = compute(store=True),依赖amount
现在你不用 ORM,而是手工:
UPDATE my_model SET amount = 200 WHERE id = 1;
如果此时你什么都不做,可能发生三种错位:
错位 1:Python 还读到旧的 amount
因为缓存没失效。
错位 2:total 还是旧值
因为依赖重算没被通知。
错位 3:别的 ORM 待写数据后面 flush 时又覆盖了你
因为你在 SQL 前没有先 flush,ORM 手里还握着旧的脏写计划。
所以“SQL 直接改”不是不能做,而是你要承担桥接两个世界的责任。
flush_recordset() 和 flush_model() 的区别
这两个也常被混。
flush_model()
关注的是:
- 整个模型层面
- 至少保证这些字段被 flush
适合在你要对整张模型相关数据做 SQL 时用。
flush_recordset()
关注的是:
- 当前这批记录
- 至少保证这些记录上的这些字段被 flush
适合你只想精确处理某个 recordset。
简单说:
model版更广recordset版更细
invalidate_model() 和 invalidate_recordset() 也一样有范围差异
同理:
invalidate_model():整个模型缓存失效invalidate_recordset():只失效当前记录集
如果你 SQL 只改了几条记录,优先用 recordset 级别,会更稳、更省。
开发里最实用的判断口诀
以后遇到 ORM / SQL 混用场景,可以先问自己 3 个问题:
1)我现在担心的是数据库没更新,还是缓存没更新?
- 数据库没更新 → 想
flush - 缓存没更新 → 想
invalidate
2)我改的是当前字段本身,还是会影响依赖链?
- 影响 store compute 依赖 → 还要考虑
modified
3)我操作的是整模型,还是一小批记录?
- 整模型 →
*_model() - 小批记录 →
*_recordset()
最常见的 4 个误区
误区 1:把 flush 当成“刷新页面”
不是。它是 ORM → DB。
误区 2:把 invalidate 当成“自动重算一切”
也不是。它主要是 缓存作废,不是依赖拓扑广播器。
误区 3:直接 SQL 后只 invalidate,不 modified
这样依赖链可能不完整。
误区 4:直接 SQL 前不 flush
这样 ORM 后续落库有机会把你刚改的值又覆盖掉。
一句话总结
Odoo ORM 不是“字段一赋值就全世界同步”的即时系统,而是“缓存、flush、失效、重算协同工作”的分层机制。凡是直接 SQL,你都得主动思考:先不先 flush、要不要 invalidate、依赖链需不需要 modified。
DISCUSSION
评论区