ORM 同步边界

Odoo ORM 为什么不是改完字段就万事大吉:flush、缓存失效、modified 与 SQL 同步讲透

从 Odoo 19 的 ORM 源码出发,讲清 flush_model、invalidate_model、modified 各自解决什么问题,以及为什么你一旦直接写 SQL,就必须主动把 ORM 世界和数据库世界重新对齐。

Odoo 开发 框架
进阶 开发者 2 分钟阅读
0 评论 0 点赞 0 收藏 5 阅读

先说结论

很多人把 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() 的注释写得很直白:

处理待执行的计算与数据库更新。

它会先:

  1. 触发相关重算
  2. 再把脏字段 _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 的其他脏数据

所以官方推荐的典型顺序其实是:

  1. flush_model() —— 先把 ORM 自己手头待落库的东西落下去
  2. 再跑 SQL —— 改数据库
  3. invalidate_model() / invalidate_recordset() —— 告诉 ORM 别信旧缓存
  4. 必要时 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

评论区

想参与讨论?先 登录 再发表评论。
还没有评论,你可以成为第一个留言的人。